diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e9a9e84..0b9788a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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\\|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, string given\.$#' identifier: argument.type diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index c6b7458..5c2c3cf 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -4,7 +4,6 @@ namespace Patchlevel\Hydrator\Metadata; -use BackedEnum; use DateTime; use DateTimeImmutable; use DateTimeZone; @@ -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; @@ -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 { @@ -233,10 +231,6 @@ private function getNormalizer(ReflectionProperty $reflectionProperty): Normaliz $normalizer = $this->findNormalizer($reflectionProperty, $type); - if (!$normalizer) { - $normalizer = $this->inferNormalizer($reflectionProperty); - } - if ($normalizer instanceof TypeAwareNormalizer) { $normalizer->handleType($type); } @@ -249,32 +243,18 @@ private function getNormalizer(ReflectionProperty $reflectionProperty): Normaliz 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 @@ -403,7 +383,13 @@ private function findNormalizer(ReflectionProperty $reflectionProperty, Type $ty } 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) { @@ -420,16 +406,11 @@ private function findNormalizer(ReflectionProperty $reflectionProperty, Type $ty $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); diff --git a/src/Normalizer/ArrayNormalizer.php b/src/Normalizer/ArrayNormalizer.php index a42de10..3d0d8aa 100644 --- a/src/Normalizer/ArrayNormalizer.php +++ b/src/Normalizer/ArrayNormalizer.php @@ -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; @@ -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; } diff --git a/src/Normalizer/EnumNormalizer.php b/src/Normalizer/EnumNormalizer.php index bdd8867..2f06cd1 100644 --- a/src/Normalizer/EnumNormalizer.php +++ b/src/Normalizer/EnumNormalizer.php @@ -9,6 +9,7 @@ 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; @@ -68,7 +69,15 @@ public function handleReflectionType(ReflectionType|null $reflectionType): void 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; } diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index 5f9cf4e..591dfba 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -8,6 +8,7 @@ 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; @@ -78,7 +79,15 @@ public function handleReflectionType(ReflectionType|null $reflectionType): void 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; } diff --git a/src/Normalizer/ReflectionTypeAwareNormalizer.php b/src/Normalizer/ReflectionTypeAwareNormalizer.php index c55a2af..038e122 100644 --- a/src/Normalizer/ReflectionTypeAwareNormalizer.php +++ b/src/Normalizer/ReflectionTypeAwareNormalizer.php @@ -6,6 +6,7 @@ use ReflectionType; +/** @deprecated use TypeAwareNormalizer instead */ interface ReflectionTypeAwareNormalizer { /** diff --git a/src/Normalizer/ReflectionTypeUtil.php b/src/Normalizer/ReflectionTypeUtil.php index 1b430aa..b64b465 100644 --- a/src/Normalizer/ReflectionTypeUtil.php +++ b/src/Normalizer/ReflectionTypeUtil.php @@ -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 diff --git a/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php new file mode 100644 index 0000000..fae4151 --- /dev/null +++ b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php @@ -0,0 +1,24 @@ + $listArray + * @param iterable $iterableArray + * @param array $hashMap + * @param array{foo: string, bar: int, baz: list}|null $jsonArray + */ + public function __construct( + public array $defaultArray = [], + public array $listArray = [], + public iterable $iterableArray = [], + public array $hashMap = [], + public array|null $jsonArray = null, + ) { + } +} diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index 33f1a2c..9001039 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -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; @@ -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);