Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ parameters:
count: 1
path: src/Metadata/AttributeMetadataFactory.php

-
message: '#^Method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:inferNormalizer\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: src/Metadata/AttributeMetadataFactory.php

-
message: '#^Parameter \#1 \$enum of class Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer constructor expects class\-string\<BackedEnum\>\|null, string given\.$#'
identifier: argument.type
count: 1
path: src/Metadata/AttributeMetadataFactory.php

-
message: '#^Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class\-string\<T of object\>\|T of object, string given\.$#'
identifier: argument.type
Expand Down
51 changes: 16 additions & 35 deletions src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Patchlevel\Hydrator\Metadata;

use BackedEnum;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
Expand All @@ -25,9 +24,9 @@
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionProperty;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\NullableType;
use Symfony\Component\TypeInfo\Type\ObjectType;
Expand All @@ -36,7 +35,6 @@
use function array_key_exists;
use function array_merge;
use function array_values;
use function is_a;

final class AttributeMetadataFactory implements MetadataFactory
{
Expand Down Expand Up @@ -233,10 +231,6 @@

$normalizer = $this->findNormalizer($reflectionProperty, $type);

if (!$normalizer) {
$normalizer = $this->inferNormalizer($reflectionProperty);
}

if ($normalizer instanceof TypeAwareNormalizer) {
$normalizer->handleType($type);
}
Expand All @@ -249,32 +243,18 @@
return $normalizer;
}

private function inferNormalizer(ReflectionProperty $property): Normalizer|null
private function inferNormalizer(ObjectType $type): Normalizer|null
{
$type = $property->getType();

if (!$type instanceof ReflectionNamedType) {
return null;
if ($type instanceof BackedEnumType) {
return new EnumNormalizer($type->getClassName());
}

$className = $type->getName();

$normalizer = match ($className) {
return match ($type->getClassName()) {
DateTimeImmutable::class => new DateTimeImmutableNormalizer(),
DateTime::class => new DateTimeNormalizer(),
DateTimeZone::class => new DateTimeZoneNormalizer(),
default => null,
};

if ($normalizer) {
return $normalizer;
}

if (is_a($className, BackedEnum::class, true)) {
return new EnumNormalizer($className);
}

return null;
}

private function hasIgnore(ReflectionProperty $reflectionProperty): bool
Expand Down Expand Up @@ -354,13 +334,13 @@
return $this->getFieldName($property);
}

/** @return array{bool, mixed, (callable(string, mixed):mixed)|null} */

Check failure on line 337 in src/Metadata/AttributeMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

InvalidReturnType

src/Metadata/AttributeMetadataFactory.php:337:17: InvalidReturnType: The declared return type 'list{bool, mixed, callable(string, mixed):mixed|null}' for Patchlevel\Hydrator\Metadata\AttributeMetadataFactory::getPersonalData is incorrect, got 'list{0: bool, 1: mixed|null, 2?: callable(string, mixed):mixed|null}' which is different due to additional array shape fields (2) (see https://psalm.dev/011)
private function getPersonalData(ReflectionProperty $reflectionProperty): array
{
$attributeReflectionList = $reflectionProperty->getAttributes(PersonalData::class);

if ($attributeReflectionList === []) {
return [false, null];

Check failure on line 343 in src/Metadata/AttributeMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

InvalidReturnStatement

src/Metadata/AttributeMetadataFactory.php:343:20: InvalidReturnStatement: The inferred type 'list{false, null}' does not match the declared return type 'list{bool, mixed, callable(string, mixed):mixed|null}' for Patchlevel\Hydrator\Metadata\AttributeMetadataFactory::getPersonalData (see https://psalm.dev/128)
}

$attribute = $attributeReflectionList[0]->newInstance();
Expand Down Expand Up @@ -403,7 +383,13 @@
}

if ($type instanceof ObjectType) {
return $this->findNormalizerOnClass(new ReflectionClass($type->getClassName()));
$normalizer = $this->findNormalizerOnClass(new ReflectionClass($type->getClassName()));

if ($normalizer) {
return $normalizer;
}

return $this->inferNormalizer($type);
}

if ($type instanceof CollectionType) {
Expand All @@ -420,16 +406,11 @@
$normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName()));

if ($normalizer === null) {
return null;
}

if ($normalizer instanceof TypeAwareNormalizer) {
$normalizer->handleType($valueType);
}
$normalizer = $this->inferNormalizer($valueType);

if ($normalizer instanceof ReflectionTypeAwareNormalizer) {
$reflectionPropertyType = $reflectionProperty->getType();
$normalizer->handleReflectionType($reflectionPropertyType);
if ($normalizer === null) {
return null;
}
}

return new ArrayNormalizer($normalizer);
Expand Down
9 changes: 9 additions & 0 deletions src/Normalizer/ArrayNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Patchlevel\Hydrator\Hydrator;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\NullableType;

use function array_map;
use function is_array;
Expand Down Expand Up @@ -59,6 +60,14 @@ public function setHydrator(Hydrator $hydrator): void

public function handleType(Type|null $type): void
{
if ($type === null) {
return;
}

if ($type instanceof NullableType) {
$type = $type->getWrappedType();
}

if (!$type instanceof CollectionType || !$this->normalizer instanceof TypeAwareNormalizer) {
return;
}
Expand Down
11 changes: 10 additions & 1 deletion src/Normalizer/EnumNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
use ReflectionType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\NullableType;
use Throwable;

use function is_int;
use function is_string;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
final class EnumNormalizer implements Normalizer, ReflectionTypeAwareNormalizer, TypeAwareNormalizer

Check failure on line 19 in src/Normalizer/EnumNormalizer.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

DeprecatedInterface

src/Normalizer/EnumNormalizer.php:19:13: DeprecatedInterface: Patchlevel\Hydrator\Normalizer\ReflectionTypeAwareNormalizer is marked deprecated (see https://psalm.dev/152)
{
/** @param class-string<BackedEnum>|null $enum */
public function __construct(
Expand Down Expand Up @@ -63,12 +64,20 @@
return;
}

$this->enum = ReflectionTypeUtil::classStringInstanceOf($reflectionType, BackedEnum::class);

Check failure on line 67 in src/Normalizer/EnumNormalizer.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

DeprecatedClass

src/Normalizer/EnumNormalizer.php:67:23: DeprecatedClass: Patchlevel\Hydrator\Normalizer\ReflectionTypeUtil is marked deprecated (see https://psalm.dev/098)
}

public function handleType(Type|null $type): void
{
if ($this->enum !== null || !$type instanceof BackedEnumType) {
if ($this->enum !== null || $type === null) {
return;
}

if ($type instanceof NullableType) {
$type = $type->getWrappedType();
}

if (!$type instanceof BackedEnumType) {
return;
}

Expand Down
11 changes: 10 additions & 1 deletion src/Normalizer/ObjectNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
use Patchlevel\Hydrator\Hydrator;
use ReflectionType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\NullableType;
use Symfony\Component\TypeInfo\Type\ObjectType;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
final class ObjectNormalizer implements Normalizer, ReflectionTypeAwareNormalizer, TypeAwareNormalizer, HydratorAwareNormalizer

Check failure on line 17 in src/Normalizer/ObjectNormalizer.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

DeprecatedInterface

src/Normalizer/ObjectNormalizer.php:17:13: DeprecatedInterface: Patchlevel\Hydrator\Normalizer\ReflectionTypeAwareNormalizer is marked deprecated (see https://psalm.dev/152)
{
private Hydrator|null $hydrator = null;

Expand Down Expand Up @@ -73,12 +74,20 @@
return;
}

$this->className = ReflectionTypeUtil::classString($reflectionType);

Check failure on line 77 in src/Normalizer/ObjectNormalizer.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.4, ubuntu-latest)

DeprecatedClass

src/Normalizer/ObjectNormalizer.php:77:28: DeprecatedClass: Patchlevel\Hydrator\Normalizer\ReflectionTypeUtil is marked deprecated (see https://psalm.dev/098)
}

public function handleType(Type|null $type): void
{
if ($this->className !== null || !$type instanceof ObjectType) {
if ($type === null || $this->className !== null) {
return;
}

if ($type instanceof NullableType) {
$type = $type->getWrappedType();
}

if (!$type instanceof ObjectType) {
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/Normalizer/ReflectionTypeAwareNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use ReflectionType;

/** @deprecated use TypeAwareNormalizer instead */
interface ReflectionTypeAwareNormalizer
{
/**
Expand Down
1 change: 1 addition & 0 deletions src/Normalizer/ReflectionTypeUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use function class_exists;
use function is_a;

/** @deprecated use symfony/type-info api instead */
final class ReflectionTypeUtil
{
public static function type(ReflectionType $reflectionType): string
Expand Down
24 changes: 24 additions & 0 deletions tests/Unit/Fixture/InferNormalizerWithIterablesDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Fixture;

final class InferNormalizerWithIterablesDto
{
/**
* @param Status[] $defaultArray
* @param list<Status> $listArray
* @param iterable<Status> $iterableArray
* @param array<string, Status> $hashMap
* @param array{foo: string, bar: int, baz: list<string>}|null $jsonArray
*/
public function __construct(
public array $defaultArray = [],
public array $listArray = [],
public iterable $iterableArray = [],
public array $hashMap = [],
public array|null $jsonArray = null,
) {
}
}
32 changes: 32 additions & 0 deletions tests/Unit/MetadataHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Patchlevel\Hydrator\Tests\Unit\Fixture\Email;
use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerBrokenDto;
use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto;
use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto;
use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto;
use Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto;
Expand Down Expand Up @@ -455,6 +456,37 @@ public function testHydrateWithInferNormalizerAndNullableProperties(): void
self::assertEquals($expected, $event);
}

public function testHydrateWithInferNormalizerWitIterables(): void
{
$expected = new InferNormalizerWithIterablesDto(
[Status::Draft],
[Status::Draft],
[Status::Draft],
[
'foo' => Status::Draft,
'bar' => Status::Draft,
],
[
'foo' => 'php',
'bar' => 15,
'baz' => ['test'],
],
);

$event = $this->hydrator->hydrate(
InferNormalizerWithIterablesDto::class,
[
'defaultArray' => ['draft'],
'listArray' => ['draft'],
'iterableArray' => ['draft'],
'hashMap' => ['foo' => 'draft', 'bar' => 'draft'],
'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']],
],
);

self::assertEquals($expected, $event);
}

public function testHydrateWithInferNormalizerFailed(): void
{
$this->expectException(TypeMismatch::class);
Expand Down
Loading