Skip to content

Commit a098acb

Browse files
committed
add hydrator metadata factory
1 parent 73307cc commit a098acb

8 files changed

Lines changed: 234 additions & 42 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Metadata;
6+
7+
final class ArrayCacheMetadataFactory implements MetadataFactory
8+
{
9+
/** @var array<class-string, ClassMetadata> */
10+
private array $classMetadata = [];
11+
12+
public function __construct(
13+
private readonly MetadataFactory $metadataFactory,
14+
) {
15+
}
16+
17+
/**
18+
* @param class-string<T> $class
19+
*
20+
* @return ClassMetadata<T>
21+
*
22+
* @template T of object
23+
*/
24+
public function metadata(string $class): ClassMetadata
25+
{
26+
if (!isset($this->classMetadata[$class])) {
27+
$this->classMetadata[$class] = $this->metadataFactory->metadata($class);
28+
}
29+
30+
return $this->classMetadata[$class];
31+
}
32+
}

src/Metadata/AttributeMetadataFactory.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,9 @@
3636

3737
final class AttributeMetadataFactory implements MetadataFactory
3838
{
39-
/** @var array<class-string, ClassMetadata> */
40-
private array $classMetadata = [];
41-
4239
private readonly TypeResolver $typeResolver;
4340

44-
private readonly Guesser|null $guesser;
41+
private readonly Guesser $guesser;
4542

4643
public function __construct(
4744
TypeResolver|null $typeResolver = null,
@@ -60,13 +57,6 @@ public function __construct(
6057
*/
6158
public function metadata(string $class): ClassMetadata
6259
{
63-
if (array_key_exists($class, $this->classMetadata)) {
64-
/** @var ClassMetadata<T> $classMetadata */
65-
$classMetadata = $this->classMetadata[$class];
66-
67-
return $classMetadata;
68-
}
69-
7060
try {
7161
$reflectionClass = new ReflectionClass($class);
7262
} catch (ReflectionException) {
@@ -89,15 +79,6 @@ public function metadata(string $class): ClassMetadata
8979
*/
9080
private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetadata
9181
{
92-
$class = $reflectionClass->getName();
93-
94-
if (array_key_exists($class, $this->classMetadata)) {
95-
/** @var ClassMetadata<T> $classMetadata */
96-
$classMetadata = $this->classMetadata[$class];
97-
98-
return $classMetadata;
99-
}
100-
10182
$metadata = new ClassMetadata(
10283
$reflectionClass,
10384
$this->getPropertyMetadataList($reflectionClass),
@@ -115,8 +96,6 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
11596
);
11697
}
11798

118-
$this->classMetadata[$class] = $metadata;
119-
12099
return $metadata;
121100
}
122101

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Metadata;
6+
7+
use Patchlevel\Hydrator\Hydrator;
8+
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
9+
10+
final class HydratorSetterMetadataFactory implements MetadataFactory
11+
{
12+
public function __construct(
13+
private readonly Hydrator $hydrator,
14+
private readonly MetadataFactory $metadataFactory,
15+
) {
16+
}
17+
18+
/**
19+
* @param class-string<T> $class
20+
*
21+
* @return ClassMetadata<T>
22+
*
23+
* @template T of object
24+
*/
25+
public function metadata(string $class): ClassMetadata
26+
{
27+
$metadata = $this->metadataFactory->metadata($class);
28+
29+
foreach ($metadata->properties as $property) {
30+
if (!($property->normalizer instanceof HydratorAwareNormalizer)) {
31+
continue;
32+
}
33+
34+
$property->normalizer->setHydrator($this->hydrator);
35+
}
36+
37+
return $metadata;
38+
}
39+
}

src/MetadataHydrator.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
1212
use Patchlevel\Hydrator\Guesser\ChainGuesser;
1313
use Patchlevel\Hydrator\Guesser\Guesser;
14+
use Patchlevel\Hydrator\Metadata\ArrayCacheMetadataFactory;
1415
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
1516
use Patchlevel\Hydrator\Metadata\ClassMetadata;
1617
use Patchlevel\Hydrator\Metadata\ClassNotFound;
18+
use Patchlevel\Hydrator\Metadata\HydratorSetterMetadataFactory;
1719
use Patchlevel\Hydrator\Metadata\MetadataFactory;
18-
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
1920
use ReflectionClass;
2021
use ReflectionParameter;
2122
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -35,12 +36,21 @@ final class MetadataHydrator implements Hydrator
3536
/** @var array<int, class-string> */
3637
private array $stack = [];
3738

39+
private readonly MetadataFactory $metadataFactory;
40+
3841
public function __construct(
39-
private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
42+
MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
4043
PayloadCryptographer|null $cryptographer = null,
4144
private EventDispatcherInterface|null $eventDispatcher = null,
4245
private readonly bool $defaultLazy = false,
4346
) {
47+
$this->metadataFactory = new ArrayCacheMetadataFactory(
48+
new HydratorSetterMetadataFactory(
49+
$this,
50+
$metadataFactory,
51+
),
52+
);
53+
4454
if (!$cryptographer) {
4555
return;
4656
}
@@ -132,10 +142,6 @@ private function doHydrate(ClassMetadata $metadata, array $data): object
132142
$normalizer = $propertyMetadata->normalizer;
133143

134144
if ($normalizer) {
135-
if ($normalizer instanceof HydratorAwareNormalizer) {
136-
$normalizer->setHydrator($this);
137-
}
138-
139145
try {
140146
/** @psalm-suppress MixedAssignment */
141147
$value = $normalizer->denormalize($value);
@@ -197,10 +203,6 @@ public function extract(object $object): array
197203
$normalizer = $propertyMetadata->normalizer;
198204

199205
if ($normalizer) {
200-
if ($normalizer instanceof HydratorAwareNormalizer) {
201-
$normalizer->setHydrator($this);
202-
}
203-
204206
try {
205207
/** @psalm-suppress MixedAssignment */
206208
$value = $normalizer->normalize($value);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Fixture;
6+
7+
use Attribute;
8+
use Patchlevel\Hydrator\Hydrator;
9+
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
10+
use Patchlevel\Hydrator\Normalizer\Normalizer;
11+
12+
#[Attribute(Attribute::TARGET_PROPERTY)]
13+
class NormalizerWithHydrator implements Normalizer, HydratorAwareNormalizer
14+
{
15+
public Hydrator|null $hydrator = null;
16+
17+
public function setHydrator(Hydrator $hydrator): void
18+
{
19+
$this->hydrator = $hydrator;
20+
}
21+
22+
public function normalize(mixed $value): mixed
23+
{
24+
return null;
25+
}
26+
27+
public function denormalize(mixed $value): mixed
28+
{
29+
return null;
30+
}
31+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Metadata;
6+
7+
use Patchlevel\Hydrator\Metadata\ArrayCacheMetadataFactory;
8+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
9+
use Patchlevel\Hydrator\Metadata\MetadataFactory;
10+
use PHPUnit\Framework\TestCase;
11+
use ReflectionClass;
12+
13+
final class ArrayCacheMetadataFactoryTest extends TestCase
14+
{
15+
public function testGetMetadata(): void
16+
{
17+
$object = new class {
18+
};
19+
20+
$metadata = new ClassMetadata(
21+
new ReflectionClass($object),
22+
);
23+
24+
$parent = $this->createMock(MetadataFactory::class);
25+
$parent->expects($this->once())
26+
->method('metadata')
27+
->with($object::class)
28+
->willReturn($metadata);
29+
30+
$metadataFactory = new ArrayCacheMetadataFactory($parent);
31+
32+
self::assertSame($metadata, $metadataFactory->metadata($object::class));
33+
}
34+
35+
public function testSameMetadata(): void
36+
{
37+
$object = new class {
38+
};
39+
40+
$metadata = new ClassMetadata(
41+
new ReflectionClass($object),
42+
);
43+
44+
$parent = $this->createMock(MetadataFactory::class);
45+
$parent->expects($this->once())
46+
->method('metadata')
47+
->with($object::class)
48+
->willReturn($metadata);
49+
50+
$metadataFactory = new ArrayCacheMetadataFactory($parent);
51+
52+
self::assertSame($metadata, $metadataFactory->metadata($object::class));
53+
self::assertSame($metadata, $metadataFactory->metadata($object::class));
54+
}
55+
}

tests/Unit/Metadata/AttributeMetadataFactoryTest.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,6 @@ public function testEmptyObject(): void
5252
self::assertCount(0, $metadata->postHydrateCallbacks);
5353
}
5454

55-
public function testSameMetadata(): void
56-
{
57-
$object = new class {
58-
};
59-
60-
$metadataFactory = new AttributeMetadataFactory();
61-
62-
self::assertSame($metadataFactory->metadata($object::class), $metadataFactory->metadata($object::class));
63-
}
64-
6555
public function testNotFoundProperty(): void
6656
{
6757
$this->expectException(PropertyMetadataNotFound::class);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Metadata;
6+
7+
use Patchlevel\Hydrator\Hydrator;
8+
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
9+
use Patchlevel\Hydrator\Metadata\HydratorSetterMetadataFactory;
10+
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
11+
use Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerWithHydrator;
12+
use Patchlevel\Hydrator\Tests\Unit\Fixture\Status;
13+
use PHPUnit\Framework\TestCase;
14+
15+
final class HydratorSetterMetadataFactoryTest extends TestCase
16+
{
17+
public function testNoNormalizer(): void
18+
{
19+
$object = new class {
20+
public string $name = 'test';
21+
};
22+
23+
$hydrator = $this->createMock(Hydrator::class);
24+
25+
$metadataFactory = new HydratorSetterMetadataFactory($hydrator, new AttributeMetadataFactory());
26+
27+
$metadata = $metadataFactory->metadata($object::class);
28+
29+
self::assertCount(1, $metadata->properties);
30+
}
31+
32+
public function testNormalizerWithoutHydratorAware(): void
33+
{
34+
$object = new class {
35+
#[EnumNormalizer]
36+
public Status $status = Status::Closed;
37+
};
38+
39+
$hydrator = $this->createMock(Hydrator::class);
40+
41+
$metadataFactory = new HydratorSetterMetadataFactory($hydrator, new AttributeMetadataFactory());
42+
43+
$metadata = $metadataFactory->metadata($object::class);
44+
45+
self::assertCount(1, $metadata->properties);
46+
}
47+
48+
public function testNormalizerWithHydratorAware(): void
49+
{
50+
$object = new class {
51+
#[NormalizerWithHydrator]
52+
public Status $status = Status::Closed;
53+
};
54+
55+
$hydrator = $this->createMock(Hydrator::class);
56+
57+
$metadataFactory = new HydratorSetterMetadataFactory($hydrator, new AttributeMetadataFactory());
58+
59+
$metadata = $metadataFactory->metadata($object::class);
60+
61+
self::assertCount(1, $metadata->properties);
62+
self::assertSame($hydrator, $metadata->propertyForField('status')->normalizer->hydrator);
63+
}
64+
}

0 commit comments

Comments
 (0)