Skip to content

Commit e501c29

Browse files
committed
add generic & template type support
1 parent efbbce8 commit e501c29

File tree

7 files changed

+124
-5
lines changed

7 files changed

+124
-5
lines changed

src/Metadata/AttributeMetadataFactory.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
use Symfony\Component\TypeInfo\Type;
2626
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
2727
use Symfony\Component\TypeInfo\Type\CollectionType;
28+
use Symfony\Component\TypeInfo\Type\GenericType;
2829
use Symfony\Component\TypeInfo\Type\NullableType;
2930
use Symfony\Component\TypeInfo\Type\ObjectType;
31+
use Symfony\Component\TypeInfo\Type\TemplateType;
3032
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
3133

3234
use function array_key_exists;
@@ -391,6 +393,14 @@ private function inferNormalizerByType(Type $type): Normalizer|null
391393
$type = $type->getWrappedType();
392394
}
393395

396+
if ($type instanceof TemplateType) {
397+
$type = $type->getWrappedType();
398+
}
399+
400+
if ($type instanceof GenericType) {
401+
$type = $type->getWrappedType();
402+
}
403+
394404
if ($type instanceof ObjectType) {
395405
$normalizer = $this->findNormalizerOnClass($type->getClassName());
396406

src/Normalizer/EnumNormalizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function denormalize(mixed $value): BackedEnum|null
5858
}
5959
}
6060

61+
/** @deprecated use `handleType()` instead */
6162
public function handleReflectionType(ReflectionType|null $reflectionType): void
6263
{
6364
if ($this->enum !== null || $reflectionType === null) {

src/Normalizer/ObjectNormalizer.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use Patchlevel\Hydrator\Hydrator;
99
use ReflectionType;
1010
use Symfony\Component\TypeInfo\Type;
11+
use Symfony\Component\TypeInfo\Type\GenericType;
1112
use Symfony\Component\TypeInfo\Type\NullableType;
1213
use Symfony\Component\TypeInfo\Type\ObjectType;
14+
use Symfony\Component\TypeInfo\Type\TemplateType;
1315

1416
use function is_array;
1517

@@ -68,6 +70,7 @@ public function setHydrator(Hydrator $hydrator): void
6870
$this->hydrator = $hydrator;
6971
}
7072

73+
/** @deprecated use handleType instead */
7174
public function handleReflectionType(ReflectionType|null $reflectionType): void
7275
{
7376
if ($this->className !== null || $reflectionType === null) {
@@ -87,6 +90,14 @@ public function handleType(Type|null $type): void
8790
$type = $type->getWrappedType();
8891
}
8992

93+
if ($type instanceof GenericType) {
94+
$type = $type->getWrappedType();
95+
}
96+
97+
if ($type instanceof TemplateType) {
98+
$type = $type->getWrappedType();
99+
}
100+
90101
if (!$type instanceof ObjectType) {
91102
return;
92103
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Fixture;
6+
7+
final class ProfileCreatedWithGeneric
8+
{
9+
/** @param Wrapper<Email> $email */
10+
public function __construct(
11+
#[IdNormalizer]
12+
public ProfileId $profileId,
13+
public Wrapper $email,
14+
) {
15+
}
16+
}

tests/Unit/Fixture/Wrapper.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Fixture;
6+
7+
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;
8+
9+
/** @template T */
10+
#[ObjectNormalizer]
11+
class Wrapper
12+
{
13+
/**
14+
* @param T $value
15+
* @param Wrapper<Email> $object
16+
* @param Wrapper<string>|null $scalar
17+
*/
18+
public function __construct(
19+
public mixed $value,
20+
public Email $object,
21+
public Wrapper|null $scalar = null,
22+
) {
23+
}
24+
}

tests/Unit/Metadata/AttributeMetadataFactoryTest.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Patchlevel\Hydrator\Metadata\PropertyMetadataNotFound;
1919
use Patchlevel\Hydrator\Metadata\SubjectIdAndPersonalDataConflict;
2020
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
21+
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;
2122
use Patchlevel\Hydrator\Tests\Unit\Fixture\BrokenParentDto;
2223
use Patchlevel\Hydrator\Tests\Unit\Fixture\DistributionCreated;
2324
use Patchlevel\Hydrator\Tests\Unit\Fixture\DuplicateFieldNameDto;
@@ -29,8 +30,10 @@
2930
use Patchlevel\Hydrator\Tests\Unit\Fixture\MissingSubjectIdDto;
3031
use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto;
3132
use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithPersonalDataDto;
33+
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithGeneric;
3234
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId;
3335
use Patchlevel\Hydrator\Tests\Unit\Fixture\Status;
36+
use Patchlevel\Hydrator\Tests\Unit\Fixture\Wrapper;
3437
use PHPUnit\Framework\TestCase;
3538

3639
final class AttributeMetadataFactoryTest extends TestCase
@@ -136,7 +139,7 @@ public function __construct(
136139
self::assertNull($propertyMetadata->normalizer());
137140
}
138141

139-
public function testEventWithFieldName(): void
142+
public function testNormalizedName(): void
140143
{
141144
$object = new class ('Foo') {
142145
public function __construct(
@@ -160,7 +163,7 @@ public function __construct(
160163
self::assertNull($propertyMetadata->normalizer());
161164
}
162165

163-
public function testEventWithNormalizer(): void
166+
public function testDefineNormalizer(): void
164167
{
165168
$object = new class (Email::fromString('info@patchlevel.de')) {
166169
public function __construct(
@@ -184,7 +187,7 @@ public function __construct(
184187
self::assertInstanceOf(EmailNormalizer::class, $propertyMetadata->normalizer());
185188
}
186189

187-
public function testEventWithTypeAwareNormalizer(): void
190+
public function testTypeAwareNormalizer(): void
188191
{
189192
$object = new class (Status::Draft) {
190193
public function __construct(
@@ -212,7 +215,7 @@ public function __construct(
212215
self::assertSame(Status::class, $normalizer->getEnum());
213216
}
214217

215-
public function testEventWithInferNormalizer(): void
218+
public function testInferNormalizer(): void
216219
{
217220
$object = new class {
218221
public function __construct(
@@ -233,6 +236,34 @@ public function __construct(
233236
self::assertEquals(new IdNormalizer(ProfileId::class), $propertyMetadata->normalizer());
234237
}
235238

239+
public function testInferNormalizerWithGeneric(): void
240+
{
241+
$metadataFactory = new AttributeMetadataFactory();
242+
$metadata = $metadataFactory->metadata(ProfileCreatedWithGeneric::class);
243+
244+
self::assertCount(2, $metadata->properties());
245+
246+
$propertyMetadata = $metadata->propertyForField('email');
247+
self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer());
248+
}
249+
250+
public function testInferNormalizerWithTemplate(): void
251+
{
252+
$metadataFactory = new AttributeMetadataFactory();
253+
$metadata = $metadataFactory->metadata(Wrapper::class);
254+
255+
self::assertCount(3, $metadata->properties());
256+
257+
$propertyMetadata = $metadata->propertyForField('value');
258+
self::assertNull($propertyMetadata->normalizer());
259+
260+
$propertyMetadata = $metadata->propertyForField('object');
261+
self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer());
262+
263+
$propertyMetadata = $metadata->propertyForField('scalar');
264+
self::assertEquals(new ObjectNormalizer(Wrapper::class), $propertyMetadata->normalizer());
265+
}
266+
236267
public function testExtends(): void
237268
{
238269
$metadataFactory = new AttributeMetadataFactory();

tests/Unit/Normalizer/ObjectNormalizerTest.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ReflectionClass;
1919
use ReflectionType;
2020
use RuntimeException;
21+
use Symfony\Component\TypeInfo\Type;
2122

2223
use function serialize;
2324
use function unserialize;
@@ -115,7 +116,10 @@ public function testDenormalizeWithValue(): void
115116
Email::fromString('info@patchlevel.de'),
116117
);
117118

118-
$hydrator->expects($this->once())->method('hydrate')->with(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de'])
119+
$hydrator->expects($this->once())->method('hydrate')->with(
120+
ProfileCreated::class,
121+
['profileId' => '1', 'email' => 'info@patchlevel.de'],
122+
)
119123
->willReturn($expected);
120124

121125
$normalizer = new ObjectNormalizer(ProfileCreated::class);
@@ -174,6 +178,28 @@ public function testAutoDetectMissingTypeBecauseNull(): void
174178
$normalizer->getClassName();
175179
}
176180

181+
public function testGeneric(): void
182+
{
183+
$hydrator = $this->createMock(Hydrator::class);
184+
185+
$normalizer = new ObjectNormalizer();
186+
$normalizer->setHydrator($hydrator);
187+
$normalizer->handleType(Type::generic(Type::object(ProfileCreated::class)));
188+
189+
self::assertEquals(ProfileCreated::class, $normalizer->getClassName());
190+
}
191+
192+
public function testTemplate(): void
193+
{
194+
$hydrator = $this->createMock(Hydrator::class);
195+
196+
$normalizer = new ObjectNormalizer();
197+
$normalizer->setHydrator($hydrator);
198+
$normalizer->handleType(Type::template('T', Type::object(ProfileCreated::class)));
199+
200+
self::assertEquals(ProfileCreated::class, $normalizer->getClassName());
201+
}
202+
177203
public function testSerialize(): void
178204
{
179205
$hydrator = $this->createMock(Hydrator::class);

0 commit comments

Comments
 (0)