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);