From 680c9aea93e6c5d503b29b8423bee81c60554b4e Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 17 Apr 2025 14:12:01 +0200 Subject: [PATCH 1/5] refactor infer & find normalizer logic --- phpstan-baseline.neon | 6 - src/Metadata/AttributeMetadataFactory.php | 132 +++++++++++----------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0b9788a..4ea58da 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -30,12 +30,6 @@ 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 diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index 5c2c3cf..c28c9f2 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -225,38 +225,6 @@ private function getFieldName(ReflectionProperty $reflectionProperty): string return $attributeReflectionList[0]->newInstance()->name(); } - private function getNormalizer(ReflectionProperty $reflectionProperty): Normalizer|null - { - $type = $this->typeResolver->resolve($reflectionProperty); - - $normalizer = $this->findNormalizer($reflectionProperty, $type); - - if ($normalizer instanceof TypeAwareNormalizer) { - $normalizer->handleType($type); - } - - if ($normalizer instanceof ReflectionTypeAwareNormalizer) { - $reflectionPropertyType = $reflectionProperty->getType(); - $normalizer->handleReflectionType($reflectionPropertyType); - } - - return $normalizer; - } - - private function inferNormalizer(ObjectType $type): Normalizer|null - { - if ($type instanceof BackedEnumType) { - return new EnumNormalizer($type->getClassName()); - } - - return match ($type->getClassName()) { - DateTimeImmutable::class => new DateTimeImmutableNormalizer(), - DateTime::class => new DateTimeNormalizer(), - DateTimeZone::class => new DateTimeZoneNormalizer(), - default => null, - }; - } - private function hasIgnore(ReflectionProperty $reflectionProperty): bool { return $reflectionProperty->getAttributes(Ignore::class) !== []; @@ -367,6 +335,28 @@ private function validate(ClassMetadata $metadata): void } } + private function getNormalizer(ReflectionProperty $reflectionProperty): Normalizer|null + { + $type = $this->typeResolver->resolve($reflectionProperty); + + $normalizer = $this->findNormalizer($reflectionProperty, $type); + + if (!$normalizer) { + $normalizer = $this->inferNormalizer($type); + } + + if ($normalizer instanceof TypeAwareNormalizer) { + $normalizer->handleType($type); + } + + if ($normalizer instanceof ReflectionTypeAwareNormalizer) { + $reflectionPropertyType = $reflectionProperty->getType(); + $normalizer->handleReflectionType($reflectionPropertyType); + } + + return $normalizer; + } + private function findNormalizer(ReflectionProperty $reflectionProperty, Type $type): Normalizer|null { $attributeReflectionList = $reflectionProperty->getAttributes( @@ -383,37 +373,7 @@ private function findNormalizer(ReflectionProperty $reflectionProperty, Type $ty } if ($type instanceof ObjectType) { - $normalizer = $this->findNormalizerOnClass(new ReflectionClass($type->getClassName())); - - if ($normalizer) { - return $normalizer; - } - - return $this->inferNormalizer($type); - } - - if ($type instanceof CollectionType) { - $valueType = $type->getCollectionValueType(); - - if ($valueType instanceof NullableType) { - $valueType = $type->getWrappedType(); - } - - if (!$valueType instanceof ObjectType) { - return null; - } - - $normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName())); - - if ($normalizer === null) { - $normalizer = $this->inferNormalizer($valueType); - - if ($normalizer === null) { - return null; - } - } - - return new ArrayNormalizer($normalizer); + return $this->findNormalizerOnClass(new ReflectionClass($type->getClassName())); } return null; @@ -451,4 +411,50 @@ private function findNormalizerOnClass(ReflectionClass $reflectionClass): Normal return null; } + + private function inferNormalizer(Type $type): Normalizer|null + { + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + } + + if ($type instanceof BackedEnumType) { + return new EnumNormalizer($type->getClassName()); + } + + if ($type instanceof CollectionType) { + $valueType = $type->getCollectionValueType(); + + if ($valueType instanceof NullableType) { + $valueType = $type->getWrappedType(); + } + + if (!$valueType instanceof ObjectType) { + return null; + } + + $normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName())); + + if ($normalizer === null) { + $normalizer = $this->inferNormalizer($valueType); + + if ($normalizer === null) { + return null; + } + } + + return new ArrayNormalizer($normalizer); + } + + if ($type instanceof ObjectType) { + return match ($type->getClassName()) { + DateTimeImmutable::class => new DateTimeImmutableNormalizer(), + DateTime::class => new DateTimeNormalizer(), + DateTimeZone::class => new DateTimeZoneNormalizer(), + default => null, + }; + } + + return null; + } } From 99218f34adb471e6f8b8de0c8449104c1bc0a3d8 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 17 Apr 2025 15:14:37 +0200 Subject: [PATCH 2/5] allow nesting --- src/Metadata/AttributeMetadataFactory.php | 16 ++++++++-------- .../Fixture/InferNormalizerWithIterablesDto.php | 2 ++ tests/Unit/MetadataHydratorTest.php | 5 +++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index c28c9f2..20bcb91 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -429,21 +429,21 @@ private function inferNormalizer(Type $type): Normalizer|null $valueType = $type->getWrappedType(); } - if (!$valueType instanceof ObjectType) { - return null; - } + $normalizer = null; - $normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName())); + if ($valueType instanceof ObjectType) { + $normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName())); + } if ($normalizer === null) { $normalizer = $this->inferNormalizer($valueType); + } - if ($normalizer === null) { - return null; - } + if ($normalizer) { + return new ArrayNormalizer($normalizer); } - return new ArrayNormalizer($normalizer); + return null; } if ($type instanceof ObjectType) { diff --git a/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php index fae4151..e6cb424 100644 --- a/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php +++ b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php @@ -11,6 +11,7 @@ final class InferNormalizerWithIterablesDto * @param list $listArray * @param iterable $iterableArray * @param array $hashMap + * @param array> $nested * @param array{foo: string, bar: int, baz: list}|null $jsonArray */ public function __construct( @@ -18,6 +19,7 @@ public function __construct( public array $listArray = [], public iterable $iterableArray = [], public array $hashMap = [], + public iterable $nested = [], public array|null $jsonArray = null, ) { } diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index 9001039..3400ad4 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -466,6 +466,10 @@ public function testHydrateWithInferNormalizerWitIterables(): void 'foo' => Status::Draft, 'bar' => Status::Draft, ], + [ + 'foo' => [Status::Draft], + 'bar' => [Status::Draft], + ], [ 'foo' => 'php', 'bar' => 15, @@ -480,6 +484,7 @@ public function testHydrateWithInferNormalizerWitIterables(): void 'listArray' => ['draft'], 'iterableArray' => ['draft'], 'hashMap' => ['foo' => 'draft', 'bar' => 'draft'], + 'nested' => ['foo' => ['draft'], 'bar' => ['draft']], 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], ], ); From 12d478652682a7fb3363433493949518959f60c7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 17 Apr 2025 15:20:00 +0200 Subject: [PATCH 3/5] improve naming --- src/Metadata/AttributeMetadataFactory.php | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index 20bcb91..d012461 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -339,10 +339,20 @@ private function getNormalizer(ReflectionProperty $reflectionProperty): Normaliz { $type = $this->typeResolver->resolve($reflectionProperty); - $normalizer = $this->findNormalizer($reflectionProperty, $type); + $normalizer = $this->findNormalizerOnProperty($reflectionProperty); if (!$normalizer) { - $normalizer = $this->inferNormalizer($type); + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + } + + if ($type instanceof ObjectType) { + $normalizer = $this->findNormalizerOnClass(new ReflectionClass($type->getClassName())); + } + } + + if (!$normalizer) { + $normalizer = $this->inferNormalizerByType($type); } if ($normalizer instanceof TypeAwareNormalizer) { @@ -357,7 +367,7 @@ private function getNormalizer(ReflectionProperty $reflectionProperty): Normaliz return $normalizer; } - private function findNormalizer(ReflectionProperty $reflectionProperty, Type $type): Normalizer|null + private function findNormalizerOnProperty(ReflectionProperty $reflectionProperty): Normalizer|null { $attributeReflectionList = $reflectionProperty->getAttributes( Normalizer::class, @@ -368,14 +378,6 @@ private function findNormalizer(ReflectionProperty $reflectionProperty, Type $ty return $attributeReflectionList[0]->newInstance(); } - if ($type instanceof NullableType) { - $type = $type->getWrappedType(); - } - - if ($type instanceof ObjectType) { - return $this->findNormalizerOnClass(new ReflectionClass($type->getClassName())); - } - return null; } @@ -412,7 +414,7 @@ private function findNormalizerOnClass(ReflectionClass $reflectionClass): Normal return null; } - private function inferNormalizer(Type $type): Normalizer|null + private function inferNormalizerByType(Type $type): Normalizer|null { if ($type instanceof NullableType) { $type = $type->getWrappedType(); @@ -436,7 +438,7 @@ private function inferNormalizer(Type $type): Normalizer|null } if ($normalizer === null) { - $normalizer = $this->inferNormalizer($valueType); + $normalizer = $this->inferNormalizerByType($valueType); } if ($normalizer) { From 58a5579da914fc9141136d894534e9d46cd0a601 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 17 Apr 2025 16:08:16 +0200 Subject: [PATCH 4/5] more improvements --- src/Metadata/AttributeMetadataFactory.php | 112 +++++++++++----------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index d012461..731e9d6 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -337,31 +337,20 @@ private function validate(ClassMetadata $metadata): void private function getNormalizer(ReflectionProperty $reflectionProperty): Normalizer|null { - $type = $this->typeResolver->resolve($reflectionProperty); - $normalizer = $this->findNormalizerOnProperty($reflectionProperty); + $type = null; if (!$normalizer) { - if ($type instanceof NullableType) { - $type = $type->getWrappedType(); - } - - if ($type instanceof ObjectType) { - $normalizer = $this->findNormalizerOnClass(new ReflectionClass($type->getClassName())); - } - } - - if (!$normalizer) { + $type = $this->typeResolver->resolve($reflectionProperty); $normalizer = $this->inferNormalizerByType($type); } if ($normalizer instanceof TypeAwareNormalizer) { - $normalizer->handleType($type); + $normalizer->handleType($type ?? $this->typeResolver->resolve($reflectionProperty)); } if ($normalizer instanceof ReflectionTypeAwareNormalizer) { - $reflectionPropertyType = $reflectionProperty->getType(); - $normalizer->handleReflectionType($reflectionPropertyType); + $normalizer->handleReflectionType($reflectionProperty->getType()); } return $normalizer; @@ -381,47 +370,24 @@ private function findNormalizerOnProperty(ReflectionProperty $reflectionProperty return null; } - /** @param ReflectionClass $reflectionClass */ - private function findNormalizerOnClass(ReflectionClass $reflectionClass): Normalizer|null + private function inferNormalizerByType(Type $type): Normalizer|null { - $attributes = $reflectionClass->getAttributes( - Normalizer::class, - ReflectionAttribute::IS_INSTANCEOF, - ); - - if ($attributes !== []) { - return $attributes[0]->newInstance(); + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); } - $parent = $reflectionClass->getParentClass(); - - if ($parent) { - $normalizer = $this->findNormalizerOnClass($parent); - - if ($normalizer !== null) { - return $normalizer; - } + if ($type instanceof BackedEnumType) { + return new EnumNormalizer($type->getClassName()); } - foreach ($reflectionClass->getInterfaces() as $interface) { - $normalizer = $this->findNormalizerOnClass($interface); + if ($type instanceof ObjectType) { + $normalizer = $this->findNormalizerOnClass($type->getClassName()); - if ($normalizer !== null) { + if ($normalizer) { return $normalizer; } - } - return null; - } - - private function inferNormalizerByType(Type $type): Normalizer|null - { - if ($type instanceof NullableType) { - $type = $type->getWrappedType(); - } - - if ($type instanceof BackedEnumType) { - return new EnumNormalizer($type->getClassName()); + return $this->guessNormalizerByObjectType($type); } if ($type instanceof CollectionType) { @@ -434,7 +400,7 @@ private function inferNormalizerByType(Type $type): Normalizer|null $normalizer = null; if ($valueType instanceof ObjectType) { - $normalizer = $this->findNormalizerOnClass(new ReflectionClass($valueType->getClassName())); + $normalizer = $this->findNormalizerOnClass($valueType->getClassName()); } if ($normalizer === null) { @@ -448,15 +414,51 @@ private function inferNormalizerByType(Type $type): Normalizer|null return null; } - if ($type instanceof ObjectType) { - return match ($type->getClassName()) { - DateTimeImmutable::class => new DateTimeImmutableNormalizer(), - DateTime::class => new DateTimeNormalizer(), - DateTimeZone::class => new DateTimeZoneNormalizer(), - default => null, - }; + return null; + } + + /** @param class-string $class */ + private function findNormalizerOnClass(string $class): Normalizer|null + { + $reflectionClass = new ReflectionClass($class); + + $attributes = $reflectionClass->getAttributes( + Normalizer::class, + ReflectionAttribute::IS_INSTANCEOF, + ); + + if ($attributes !== []) { + return $attributes[0]->newInstance(); + } + + $parent = $reflectionClass->getParentClass(); + + if ($parent) { + $normalizer = $this->findNormalizerOnClass($parent->getName()); + + if ($normalizer !== null) { + return $normalizer; + } + } + + foreach ($reflectionClass->getInterfaces() as $interface) { + $normalizer = $this->findNormalizerOnClass($interface->getName()); + + if ($normalizer !== null) { + return $normalizer; + } } return null; } + + private function guessNormalizerByObjectType(ObjectType $type): Normalizer|null + { + return match ($type->getClassName()) { + DateTimeImmutable::class => new DateTimeImmutableNormalizer(), + DateTime::class => new DateTimeNormalizer(), + DateTimeZone::class => new DateTimeZoneNormalizer(), + default => null, + }; + } } From f6aee652cea05f83566498e87d2eca0988352c08 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 17 Apr 2025 16:16:43 +0200 Subject: [PATCH 5/5] fix phpstan --- phpstan-baseline.neon | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4ea58da..8873f69 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -31,17 +31,23 @@ parameters: 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 + message: '#^Method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:guessNormalizerByObjectType\(\) 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 \$objectOrClass of class ReflectionClass constructor expects class\-string\\|T of object, string given\.$#' + message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:findNormalizerOnClass\(\) expects class\-string, string given\.$#' identifier: argument.type count: 2 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: '#^Property Patchlevel\\Hydrator\\Metadata\\ClassMetadata\\:\:\$reflection \(ReflectionClass\\) does not accept ReflectionClass\\.$#' identifier: assign.propertyType