Skip to content

Commit 54db55f

Browse files
committed
use class normalizer
1 parent 163b37b commit 54db55f

14 files changed

Lines changed: 157 additions & 53 deletions

phpstan-baseline.neon

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,16 @@ parameters:
7373
path: src/Metadata/ClassMetadata.php
7474

7575
-
76-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
77-
identifier: assign.propertyType
78-
count: 1
79-
path: src/Normalizer/EnumNormalizer.php
80-
81-
-
82-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Hydrator\:\:hydrate\(\) expects array\<string, mixed\>, array given\.$#'
76+
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Middleware\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
8377
identifier: argument.type
84-
count: 1
85-
path: src/Normalizer/ObjectMapNormalizer.php
78+
count: 3
79+
path: src/MetadataHydrator.php
8680

8781
-
88-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Hydrator\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
89-
identifier: argument.type
82+
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
83+
identifier: assign.propertyType
9084
count: 1
91-
path: src/Normalizer/ObjectNormalizer.php
85+
path: src/Normalizer/EnumNormalizer.php
9286

9387
-
9488
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\ObjectNormalizer\:\:\$className \(class\-string\|null\) does not accept string\.$#'

src/ArrayDataRequired.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator;
6+
7+
use RuntimeException;
8+
9+
use function sprintf;
10+
11+
final class ArrayDataRequired extends RuntimeException implements HydratorException
12+
{
13+
/** @param class-string $class */
14+
public function __construct(string $class)
15+
{
16+
parent::__construct(sprintf(
17+
'The data for the class "%s" must be an array. If you want to use another data type, you need to add a normalizer to the class.',
18+
$class,
19+
));
20+
}
21+
}

src/Hydrator.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ interface Hydrator
88
{
99
/**
1010
* @param class-string<T> $class
11-
* @param array<string, mixed> $data
1211
* @param array<string, mixed> $context
1312
*
1413
* @return T
@@ -17,12 +16,8 @@ interface Hydrator
1716
*
1817
* @template T of object
1918
*/
20-
public function hydrate(string $class, array $data, array $context = []): object;
19+
public function hydrate(string $class, mixed $data, array $context = []): object;
2120

22-
/**
23-
* @param array<string, mixed> $context
24-
*
25-
* @return array<string, mixed>
26-
*/
27-
public function extract(object $object, array $context = []): array;
21+
/** @param array<string, mixed> $context */
22+
public function extract(object $object, array $context = []): mixed;
2823
}

src/Metadata/AttributeMetadataFactory.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
1313
use Patchlevel\Hydrator\Normalizer\ArrayShapeNormalizer;
1414
use Patchlevel\Hydrator\Normalizer\Normalizer;
15+
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;
1516
use Patchlevel\Hydrator\Normalizer\TypeAwareNormalizer;
1617
use ReflectionAttribute;
1718
use ReflectionClass;
@@ -73,6 +74,7 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
7374
{
7475
$metadata = new ClassMetadata(
7576
$reflectionClass,
77+
$this->getNormalizerOnClass($reflectionClass),
7678
$this->getPropertyMetadataList($reflectionClass),
7779
$this->getLazy($reflectionClass),
7880
);
@@ -189,6 +191,7 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
189191

190192
return new ClassMetadata(
191193
$parent->reflection,
194+
$child->normalizer ?? $parent->normalizer,
192195
array_values($properties),
193196
$child->lazy ?? $parent->lazy,
194197
array_merge($parent->extras, $child->extras),
@@ -212,6 +215,23 @@ private function getNormalizer(ReflectionProperty $reflectionProperty): Normaliz
212215
return $normalizer;
213216
}
214217

218+
/** @param ReflectionClass<object> $reflectionClass */
219+
private function getNormalizerOnClass(ReflectionClass $reflectionClass): Normalizer|null
220+
{
221+
$type = Type::object($reflectionClass->getName());
222+
$normalizer = $this->inferNormalizerByType($type);
223+
224+
if ($normalizer instanceof ObjectNormalizer) {
225+
return null;
226+
}
227+
228+
if ($normalizer instanceof TypeAwareNormalizer) {
229+
$normalizer->handleType($type);
230+
}
231+
232+
return $normalizer;
233+
}
234+
215235
private function findNormalizerOnProperty(ReflectionProperty $reflectionProperty): Normalizer|null
216236
{
217237
/** @var list<ReflectionAttribute<Normalizer>> $attributeReflectionList */

src/Metadata/ClassMetadata.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace Patchlevel\Hydrator\Metadata;
66

7+
use Patchlevel\Hydrator\Normalizer\Normalizer;
78
use ReflectionClass;
89

910
/**
1011
* @phpstan-type serialized array{
1112
* className: class-string,
13+
* normalizer: Normalizer|null,
1214
* properties: array<string, PropertyMetadata>,
1315
* lazy: bool|null,
1416
* extras: array<string, mixed>,
@@ -30,6 +32,7 @@ final class ClassMetadata
3032
*/
3133
public function __construct(
3234
public readonly ReflectionClass $reflection,
35+
public Normalizer|null $normalizer = null,
3336
array $properties = [],
3437
public bool|null $lazy = null,
3538
public array $extras = [],
@@ -67,6 +70,7 @@ public function __serialize(): array
6770
{
6871
return [
6972
'className' => $this->className,
73+
'normalizer' => $this->normalizer,
7074
'properties' => $this->properties,
7175
'lazy' => $this->lazy,
7276
'extras' => $this->extras,
@@ -77,6 +81,7 @@ public function __serialize(): array
7781
public function __unserialize(array $data): void
7882
{
7983
$this->reflection = new ReflectionClass($data['className']);
84+
$this->normalizer = $data['normalizer'];
8085
$this->properties = $data['properties'];
8186
$this->lazy = $data['lazy'];
8287
$this->extras = $data['extras'];

src/MetadataHydrator.php

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use ReflectionClass;
1616

1717
use function array_key_exists;
18+
use function is_array;
1819

1920
use const PHP_VERSION_ID;
2021

@@ -33,21 +34,34 @@ public function __construct(
3334

3435
/**
3536
* @param class-string<T> $class
36-
* @param array<string, mixed> $data
3737
* @param array<string, mixed> $context
3838
*
3939
* @return T
4040
*
4141
* @template T of object
4242
*/
43-
public function hydrate(string $class, array $data, array $context = []): object
43+
public function hydrate(string $class, mixed $data, array $context = []): object
4444
{
4545
try {
4646
$metadata = $this->metadata($class);
4747
} catch (ClassNotFound $e) {
4848
throw new ClassNotSupported($class, $e);
4949
}
5050

51+
if ($metadata->normalizer) {
52+
$return = $metadata->normalizer->denormalize($data, $context);
53+
54+
if (!$return instanceof $class) {
55+
throw new ObjectRequired($class, $metadata->normalizer::class);
56+
}
57+
58+
return $return;
59+
}
60+
61+
if (!is_array($data)) {
62+
throw new ArrayDataRequired($class);
63+
}
64+
5165
if (PHP_VERSION_ID < 80400) {
5266
$stack = new Stack($this->middlewares);
5367

@@ -71,14 +85,15 @@ function () use ($metadata, $data, $context): object {
7185
);
7286
}
7387

74-
/**
75-
* @param array<string, mixed> $context
76-
*
77-
* @return array<string, mixed>
78-
*/
79-
public function extract(object $object, array $context = []): array
88+
/** @param array<string, mixed> $context */
89+
public function extract(object $object, array $context = []): mixed
8090
{
8191
$metadata = $this->metadata($object::class);
92+
93+
if ($metadata->normalizer) {
94+
return $metadata->normalizer->normalize($object, $context);
95+
}
96+
8297
$stack = new Stack($this->middlewares);
8398

8499
return $stack->next()->extract($metadata, $object, $context, $stack);

src/Normalizer/ObjectMapNormalizer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ public function normalize(mixed $value, array $context): mixed
6767
}
6868

6969
$data = $this->hydrator->extract($value);
70+
71+
if (!is_array($data)) {
72+
throw InvalidArgument::withWrongType('array<string, mixed>', $data);
73+
}
74+
7075
$data[$this->typeFieldName] = $this->classToTypeMap[$value::class];
7176

7277
return $data;

src/Normalizer/ObjectNormalizer.php

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
use Symfony\Component\TypeInfo\Type\ObjectType;
1313
use Symfony\Component\TypeInfo\Type\TemplateType;
1414

15-
use function is_array;
16-
1715
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
1816
final class ObjectNormalizer implements Normalizer, TypeAwareNormalizer, HydratorAwareNormalizer
1917
{
@@ -25,12 +23,8 @@ public function __construct(
2523
) {
2624
}
2725

28-
/**
29-
* @param array<string, mixed> $context
30-
*
31-
* @return array<string, mixed>|null
32-
*/
33-
public function normalize(mixed $value, array $context): array|null
26+
/** @param array<string, mixed> $context */
27+
public function normalize(mixed $value, array $context): mixed
3428
{
3529
if (!$this->hydrator) {
3630
throw new MissingHydrator();
@@ -60,10 +54,6 @@ public function denormalize(mixed $value, array $context): object|null
6054
return null;
6155
}
6256

63-
if (!is_array($value)) {
64-
throw InvalidArgument::withWrongType('array<string, mixed>|null', $value);
65-
}
66-
6757
$className = $this->getClassName();
6858

6959
return $this->hydrator->hydrate($className, $value, $context);

src/ObjectRequired.php

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;
6+
7+
use Patchlevel\Hydrator\Normalizer\Normalizer;
8+
use RuntimeException;
9+
10+
use function sprintf;
11+
12+
final class ObjectRequired extends RuntimeException implements HydratorException
13+
{
14+
/**
15+
* @param class-string $class
16+
* @param class-string<Normalizer> $normalizerClass
17+
*/
18+
public function __construct(
19+
string $class,
20+
string $normalizerClass,
21+
) {
22+
parent::__construct(
23+
sprintf(
24+
'The result of the normalizer "%s" for the class "%s" must be an instance of "%s".',
25+
$normalizerClass,
26+
$class,
27+
$class,
28+
),
29+
);
30+
}
31+
}

tests/Unit/Extension/Lifecycle/LifecycleExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function testIntegration(): void
2828

2929
$extractedData = $hydrator->extract($object);
3030

31+
self::assertIsArray($extractedData);
3132
self::assertSame('foo [preHydrate] [postHydrate] [preExtract] [postExtract]', $extractedData['name']);
3233
}
3334
}

0 commit comments

Comments
 (0)