diff --git a/baseline.xml b/baseline.xml index f0ef8fa2..10226442 100644 --- a/baseline.xml +++ b/baseline.xml @@ -54,6 +54,13 @@ + + newLazyProxy( + function () use ($metadata, $data): object { + return $this->doHydrate($metadata, $data); + }, + )]]> + - - - - - - - - - - - $this->extractProfileCreated($object), - $object instanceof Skill => $this->extractSkill($object), - default => throw new InvalidArgumentException('Unknown object type'), - }]]> - - @@ -163,6 +153,20 @@ $normalizer]]]> + + + + + + + + + + + + + + reflectionType($object, 'notAObject'))]]> diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index 74901477..6fb79801 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -25,8 +25,10 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\ArrayShapeType; use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\TemplateType; use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; use function array_key_exists; @@ -391,6 +393,14 @@ private function inferNormalizerByType(Type $type): Normalizer|null $type = $type->getWrappedType(); } + if ($type instanceof TemplateType) { + $type = $type->getWrappedType(); + } + + if ($type instanceof GenericType) { + $type = $type->getWrappedType(); + } + if ($type instanceof ObjectType) { $normalizer = $this->findNormalizerOnClass($type->getClassName()); diff --git a/src/Normalizer/EnumNormalizer.php b/src/Normalizer/EnumNormalizer.php index 2f06cd14..95bf1b63 100644 --- a/src/Normalizer/EnumNormalizer.php +++ b/src/Normalizer/EnumNormalizer.php @@ -58,6 +58,7 @@ public function denormalize(mixed $value): BackedEnum|null } } + /** @deprecated use `handleType()` instead */ public function handleReflectionType(ReflectionType|null $reflectionType): void { if ($this->enum !== null || $reflectionType === null) { diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index 591dfbae..4977ec93 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -8,8 +8,10 @@ use Patchlevel\Hydrator\Hydrator; use ReflectionType; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\TemplateType; use function is_array; @@ -68,6 +70,7 @@ public function setHydrator(Hydrator $hydrator): void $this->hydrator = $hydrator; } + /** @deprecated use handleType instead */ public function handleReflectionType(ReflectionType|null $reflectionType): void { if ($this->className !== null || $reflectionType === null) { @@ -87,6 +90,14 @@ public function handleType(Type|null $type): void $type = $type->getWrappedType(); } + if ($type instanceof GenericType) { + $type = $type->getWrappedType(); + } + + if ($type instanceof TemplateType) { + $type = $type->getWrappedType(); + } + if (!$type instanceof ObjectType) { return; } diff --git a/tests/Unit/Fixture/ProfileCreatedWithGeneric.php b/tests/Unit/Fixture/ProfileCreatedWithGeneric.php new file mode 100644 index 00000000..8a3dedcc --- /dev/null +++ b/tests/Unit/Fixture/ProfileCreatedWithGeneric.php @@ -0,0 +1,16 @@ + $email */ + public function __construct( + #[IdNormalizer] + public ProfileId $profileId, + public Wrapper $email, + ) { + } +} diff --git a/tests/Unit/Fixture/Wrapper.php b/tests/Unit/Fixture/Wrapper.php new file mode 100644 index 00000000..6214028e --- /dev/null +++ b/tests/Unit/Fixture/Wrapper.php @@ -0,0 +1,24 @@ + $object + * @param Wrapper|null $scalar + */ + public function __construct( + public mixed $value, + public Wrapper $object, + public Wrapper|null $scalar = null, + ) { + } +} diff --git a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php index 60445d6e..9ba032db 100644 --- a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php +++ b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php @@ -18,6 +18,7 @@ use Patchlevel\Hydrator\Metadata\PropertyMetadataNotFound; use Patchlevel\Hydrator\Metadata\SubjectIdAndPersonalDataConflict; use Patchlevel\Hydrator\Normalizer\EnumNormalizer; +use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\BrokenParentDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\DistributionCreated; use Patchlevel\Hydrator\Tests\Unit\Fixture\DuplicateFieldNameDto; @@ -29,8 +30,10 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\MissingSubjectIdDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithPersonalDataDto; +use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithGeneric; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; +use Patchlevel\Hydrator\Tests\Unit\Fixture\Wrapper; use PHPUnit\Framework\TestCase; final class AttributeMetadataFactoryTest extends TestCase @@ -136,7 +139,7 @@ public function __construct( self::assertNull($propertyMetadata->normalizer()); } - public function testEventWithFieldName(): void + public function testNormalizedName(): void { $object = new class ('Foo') { public function __construct( @@ -160,7 +163,7 @@ public function __construct( self::assertNull($propertyMetadata->normalizer()); } - public function testEventWithNormalizer(): void + public function testDefineNormalizer(): void { $object = new class (Email::fromString('info@patchlevel.de')) { public function __construct( @@ -184,7 +187,7 @@ public function __construct( self::assertInstanceOf(EmailNormalizer::class, $propertyMetadata->normalizer()); } - public function testEventWithTypeAwareNormalizer(): void + public function testTypeAwareNormalizer(): void { $object = new class (Status::Draft) { public function __construct( @@ -212,7 +215,7 @@ public function __construct( self::assertSame(Status::class, $normalizer->getEnum()); } - public function testEventWithInferNormalizer(): void + public function testInferNormalizer(): void { $object = new class { public function __construct( @@ -233,6 +236,34 @@ public function __construct( self::assertEquals(new IdNormalizer(ProfileId::class), $propertyMetadata->normalizer()); } + public function testInferNormalizerWithGeneric(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $metadata = $metadataFactory->metadata(ProfileCreatedWithGeneric::class); + + self::assertCount(2, $metadata->properties()); + + $propertyMetadata = $metadata->propertyForField('email'); + self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer()); + } + + public function testInferNormalizerWithTemplate(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $metadata = $metadataFactory->metadata(Wrapper::class); + + self::assertCount(3, $metadata->properties()); + + $propertyMetadata = $metadata->propertyForField('value'); + self::assertNull($propertyMetadata->normalizer()); + + $propertyMetadata = $metadata->propertyForField('object'); + self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer()); + + $propertyMetadata = $metadata->propertyForField('scalar'); + self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer()); + } + public function testExtends(): void { $metadataFactory = new AttributeMetadataFactory(); diff --git a/tests/Unit/Normalizer/ObjectNormalizerTest.php b/tests/Unit/Normalizer/ObjectNormalizerTest.php index 9a9a6448..485aaa1f 100644 --- a/tests/Unit/Normalizer/ObjectNormalizerTest.php +++ b/tests/Unit/Normalizer/ObjectNormalizerTest.php @@ -18,6 +18,7 @@ use ReflectionClass; use ReflectionType; use RuntimeException; +use Symfony\Component\TypeInfo\Type; use function serialize; use function unserialize; @@ -115,7 +116,10 @@ public function testDenormalizeWithValue(): void Email::fromString('info@patchlevel.de'), ); - $hydrator->expects($this->once())->method('hydrate')->with(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']) + $hydrator->expects($this->once())->method('hydrate')->with( + ProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ) ->willReturn($expected); $normalizer = new ObjectNormalizer(ProfileCreated::class); @@ -174,6 +178,28 @@ public function testAutoDetectMissingTypeBecauseNull(): void $normalizer->getClassName(); } + public function testGeneric(): void + { + $hydrator = $this->createMock(Hydrator::class); + + $normalizer = new ObjectNormalizer(); + $normalizer->setHydrator($hydrator); + $normalizer->handleType(Type::generic(Type::object(ProfileCreated::class))); + + self::assertEquals(ProfileCreated::class, $normalizer->getClassName()); + } + + public function testTemplate(): void + { + $hydrator = $this->createMock(Hydrator::class); + + $normalizer = new ObjectNormalizer(); + $normalizer->setHydrator($hydrator); + $normalizer->handleType(Type::template('T', Type::object(ProfileCreated::class))); + + self::assertEquals(ProfileCreated::class, $normalizer->getClassName()); + } + public function testSerialize(): void { $hydrator = $this->createMock(Hydrator::class);