Skip to content

Commit 377ea9e

Browse files
authored
Merge pull request #163 from patchlevel/backport-stack-hydrator
backport stack hydrator
2 parents 58b7180 + e152cfb commit 377ea9e

21 files changed

+1737
-28
lines changed

phpstan-baseline.neon

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ parameters:
6666
count: 3
6767
path: src/Metadata/AttributeMetadataFactory.php
6868

69-
-
70-
message: '#^Property Patchlevel\\Hydrator\\Metadata\\ClassMetadata\<T of object \= object\>\:\:\$reflection \(ReflectionClass\<T of object \= object\>\) does not accept ReflectionClass\<object\>\.$#'
71-
identifier: assign.propertyType
72-
count: 1
73-
path: src/Metadata/ClassMetadata.php
74-
7569
-
7670
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
7771
identifier: assign.propertyType
@@ -185,3 +179,9 @@ parameters:
185179
identifier: cast.string
186180
count: 2
187181
path: tests/Unit/Normalizer/ArrayShapeNormalizerTest.php
182+
183+
-
184+
message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\StackHydrator\:\:hydrate\(\) expects class\-string\<Unknown\>, string given\.$#'
185+
identifier: argument.type
186+
count: 1
187+
path: tests/Unit/StackHydratorTest.php

src/CoreExtension.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator;
6+
7+
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
8+
use Patchlevel\Hydrator\Middleware\TransformMiddleware;
9+
10+
final class CoreExtension implements Extension
11+
{
12+
public function configure(StackHydratorBuilder $builder): void
13+
{
14+
$builder->addMiddleware(new TransformMiddleware(), -64);
15+
$builder->addGuesser(new BuiltInGuesser(), -64);
16+
}
17+
}

src/Extension.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator;
6+
7+
interface Extension
8+
{
9+
public function configure(StackHydratorBuilder $builder): void;
10+
}

src/Metadata/ClassMetadata.php

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,58 @@
55
namespace Patchlevel\Hydrator\Metadata;
66

77
use ReflectionClass;
8+
use ReflectionParameter;
9+
10+
use function array_values;
811

912
/**
1013
* @psalm-type serialized array{
11-
* className: class-string,
12-
* properties: list<PropertyMetadata>,
14+
* className: class-string<T>,
15+
* properties: array<string, PropertyMetadata>,
1316
* dataSubjectIdField: string|null,
1417
* postHydrateCallbacks: list<CallbackMetadata>,
1518
* preExtractCallbacks: list<CallbackMetadata>,
1619
* lazy: bool|null,
20+
* extras: array<string, mixed>
1721
* }
1822
* @template T of object = object
1923
*/
20-
final readonly class ClassMetadata
24+
final class ClassMetadata
2125
{
26+
/** @var class-string<T> */
27+
public readonly string $className;
28+
29+
/** @var array<string, PropertyMetadata> */
30+
public readonly array $properties;
31+
32+
/** @var array<string, ReflectionParameter>|null */
33+
private array|null $promotedConstructorDefaults = null;
34+
2235
/**
2336
* @param ReflectionClass<T> $reflection
2437
* @param list<PropertyMetadata> $properties
2538
* @param list<CallbackMetadata> $postHydrateCallbacks
2639
* @param list<CallbackMetadata> $preExtractCallbacks
40+
* @param array<string, mixed> $extras
2741
*/
2842
public function __construct(
29-
private ReflectionClass $reflection,
30-
private array $properties = [],
31-
private string|null $dataSubjectIdField = null,
32-
private array $postHydrateCallbacks = [],
33-
private array $preExtractCallbacks = [],
34-
private bool|null $lazy = null,
43+
public readonly ReflectionClass $reflection,
44+
array $properties = [],
45+
public string|null $dataSubjectIdField = null,
46+
public array $postHydrateCallbacks = [],
47+
public array $preExtractCallbacks = [],
48+
public bool|null $lazy = null,
49+
public array $extras = [],
3550
) {
51+
$this->className = $reflection->getName();
52+
53+
$map = [];
54+
55+
foreach ($properties as $property) {
56+
$map[$property->propertyName] = $property;
57+
}
58+
59+
$this->properties = $map;
3660
}
3761

3862
/** @return ReflectionClass<T> */
@@ -44,13 +68,13 @@ public function reflection(): ReflectionClass
4468
/** @return class-string<T> */
4569
public function className(): string
4670
{
47-
return $this->reflection->getName();
71+
return $this->className;
4872
}
4973

5074
/** @return list<PropertyMetadata> */
5175
public function properties(): array
5276
{
53-
return $this->properties;
77+
return array_values($this->properties);
5478
}
5579

5680
/** @return list<CallbackMetadata> */
@@ -92,16 +116,43 @@ public function newInstance(): object
92116
return $this->reflection->newInstanceWithoutConstructor();
93117
}
94118

119+
/** @return array<string, ReflectionParameter> */
120+
public function promotedConstructorDefaults(): array
121+
{
122+
if ($this->promotedConstructorDefaults !== null) {
123+
return $this->promotedConstructorDefaults;
124+
}
125+
126+
$constructor = $this->reflection->getConstructor();
127+
128+
if (!$constructor) {
129+
return $this->promotedConstructorDefaults = [];
130+
}
131+
132+
$result = [];
133+
134+
foreach ($constructor->getParameters() as $parameter) {
135+
if (!$parameter->isPromoted() || !$parameter->isDefaultValueAvailable()) {
136+
continue;
137+
}
138+
139+
$result[$parameter->getName()] = $parameter;
140+
}
141+
142+
return $this->promotedConstructorDefaults = $result;
143+
}
144+
95145
/** @return serialized */
96146
public function __serialize(): array
97147
{
98148
return [
99-
'className' => $this->reflection->getName(),
149+
'className' => $this->className,
100150
'properties' => $this->properties,
101151
'dataSubjectIdField' => $this->dataSubjectIdField,
102152
'postHydrateCallbacks' => $this->postHydrateCallbacks,
103153
'preExtractCallbacks' => $this->preExtractCallbacks,
104154
'lazy' => $this->lazy,
155+
'extras' => $this->extras,
105156
];
106157
}
107158

@@ -114,5 +165,6 @@ public function __unserialize(array $data): void
114165
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
115166
$this->preExtractCallbacks = $data['preExtractCallbacks'];
116167
$this->lazy = $data['lazy'];
168+
$this->extras = $data['extras'];
117169
}
118170
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Metadata;
6+
7+
final readonly class EnrichingMetadataFactory implements MetadataFactory
8+
{
9+
/** @param iterable<MetadataEnricher> $enrichers */
10+
public function __construct(
11+
private MetadataFactory $factory,
12+
private iterable $enrichers,
13+
) {
14+
}
15+
16+
/**
17+
* @param class-string<T> $class
18+
*
19+
* @return ClassMetadata<T>
20+
*
21+
* @throws ClassNotFound if the class does not exist.
22+
*
23+
* @template T of object
24+
*/
25+
public function metadata(string $class): ClassMetadata
26+
{
27+
$metadata = $this->factory->metadata($class);
28+
29+
foreach ($this->enrichers as $enricher) {
30+
$enricher->enrich($metadata);
31+
}
32+
33+
return $metadata;
34+
}
35+
}

src/Metadata/MetadataEnricher.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Metadata;
6+
7+
interface MetadataEnricher
8+
{
9+
public function enrich(ClassMetadata $classMetadata): void;
10+
}

src/Metadata/PropertyMetadata.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,31 @@
1818
* fieldName: string,
1919
* normalizer: Normalizer|null,
2020
* isPersonalData: bool,
21-
* personalDataFallback: mixed
21+
* personalDataFallback: mixed,
22+
* extras: array<string, mixed>
2223
* }
2324
*/
2425
final class PropertyMetadata
2526
{
2627
private const ENCRYPTED_PREFIX = '!';
2728

28-
/** @param (callable(string, mixed):mixed)|null $personalDataFallbackCallable */
29+
public readonly string $propertyName;
30+
31+
/**
32+
* @param (callable(string, mixed):mixed)|null $personalDataFallbackCallable
33+
* @param array<string, mixed> $extras
34+
*/
2935
public function __construct(
30-
private readonly ReflectionProperty $reflection,
31-
private readonly string $fieldName,
32-
private readonly Normalizer|null $normalizer = null,
33-
private readonly bool $isPersonalData = false,
34-
private readonly mixed $personalDataFallback = null,
35-
private readonly mixed $personalDataFallbackCallable = null,
36+
public readonly ReflectionProperty $reflection,
37+
public string $fieldName,
38+
public Normalizer|null $normalizer = null,
39+
public readonly bool $isPersonalData = false,
40+
public readonly mixed $personalDataFallback = null,
41+
public readonly mixed $personalDataFallbackCallable = null,
42+
public array $extras = [],
3643
) {
44+
$this->propertyName = $reflection->getName();
45+
3746
if (str_starts_with($fieldName, self::ENCRYPTED_PREFIX)) {
3847
throw new InvalidArgumentException('fieldName must not start with !');
3948
}
@@ -46,7 +55,7 @@ public function reflection(): ReflectionProperty
4655

4756
public function propertyName(): string
4857
{
49-
return $this->reflection->getName();
58+
return $this->propertyName;
5059
}
5160

5261
public function fieldName(): string
@@ -99,11 +108,12 @@ public function __serialize(): array
99108
{
100109
return [
101110
'className' => $this->reflection->getDeclaringClass()->getName(),
102-
'property' => $this->reflection->getName(),
111+
'property' => $this->propertyName,
103112
'fieldName' => $this->fieldName,
104113
'normalizer' => $this->normalizer,
105114
'isPersonalData' => $this->isPersonalData,
106115
'personalDataFallback' => $this->personalDataFallback,
116+
'extras' => $this->extras,
107117
];
108118
}
109119

@@ -115,5 +125,6 @@ public function __unserialize(array $data): void
115125
$this->normalizer = $data['normalizer'];
116126
$this->isPersonalData = $data['isPersonalData'];
117127
$this->personalDataFallback = $data['personalDataFallback'];
128+
$this->extras = $data['extras'];
118129
}
119130
}

src/Middleware/Middleware.php

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\Middleware;
6+
7+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
8+
9+
interface Middleware
10+
{
11+
/**
12+
* @param ClassMetadata<T> $metadata
13+
* @param array<string, mixed> $data
14+
* @param array<string, mixed> $context
15+
*
16+
* @return T
17+
*
18+
* @template T of object
19+
*/
20+
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object;
21+
22+
/**
23+
* @param ClassMetadata<T> $metadata
24+
* @param T $object
25+
* @param array<string, mixed> $context
26+
*
27+
* @return array<string, mixed>
28+
*
29+
* @template T of object
30+
*/
31+
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array;
32+
}
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\Middleware;
6+
7+
use Patchlevel\Hydrator\HydratorException;
8+
use RuntimeException;
9+
10+
final class NoMoreMiddleware extends RuntimeException implements HydratorException
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct('no more middlewares');
15+
}
16+
}

src/Middleware/Stack.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Middleware;
6+
7+
final class Stack
8+
{
9+
private int $index = 0;
10+
11+
/** @param list<Middleware> $middlewares */
12+
public function __construct(
13+
private readonly array $middlewares,
14+
) {
15+
}
16+
17+
public function next(): Middleware
18+
{
19+
$next = $this->middlewares[$this->index] ?? null;
20+
21+
if ($next === null) {
22+
throw new NoMoreMiddleware();
23+
}
24+
25+
$this->index++;
26+
27+
return $next;
28+
}
29+
}

0 commit comments

Comments
 (0)