Skip to content

Commit 503390d

Browse files
committed
test middleware hydrator
1 parent 9de60ae commit 503390d

File tree

9 files changed

+423
-13
lines changed

9 files changed

+423
-13
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
8+
use Patchlevel\Hydrator\Middleware\Middleware;
9+
use Patchlevel\Hydrator\Middleware\Stack;
10+
11+
final class CryptographyMiddleware implements Middleware
12+
{
13+
public function __construct(
14+
private readonly PayloadCryptographer $cryptography,
15+
) {
16+
}
17+
18+
public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object
19+
{
20+
return $stack->next()->hydrate(
21+
$metadata,
22+
$this->cryptography->decrypt($metadata, $data),
23+
$stack
24+
);
25+
}
26+
27+
public function extract(ClassMetadata $metadata, object $object, Stack $stack): array
28+
{
29+
$data = $stack->next()->extract($metadata, $object, $stack);
30+
31+
return $this->cryptography->decrypt($metadata, $data);
32+
}
33+
}

src/Middleware/Middleware.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Middleware;
4+
5+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
6+
7+
interface Middleware
8+
{
9+
/**
10+
* @param ClassMetadata<T> $metadata
11+
* @param array<string, mixed> $data
12+
*
13+
* @return T
14+
*
15+
* @template T of object
16+
*/
17+
public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object;
18+
19+
/**
20+
* @param ClassMetadata<T> $metadata
21+
* @param T $object
22+
*
23+
* @return array<string, mixed>
24+
*
25+
* @template T of object
26+
*/
27+
public function extract(ClassMetadata $metadata, object $object, Stack $stack): array;
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Middleware;
4+
5+
use Patchlevel\Hydrator\HydratorException;
6+
use RuntimeException;
7+
8+
final class NoMoreMiddleware extends RuntimeException implements HydratorException
9+
{
10+
public function __construct()
11+
{
12+
parent::__construct('no more middleware');
13+
}
14+
}

src/Middleware/Stack.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Middleware;
4+
5+
use Patchlevel\Hydrator\Hydrator;
6+
7+
final class Stack
8+
{
9+
private int $index = 0;
10+
11+
/**
12+
* @param iterable<Middleware> $middlewares
13+
*/
14+
public function __construct(
15+
private readonly iterable $middlewares,
16+
public readonly Hydrator $hydrator,
17+
) {
18+
}
19+
20+
public function next(): Middleware
21+
{
22+
$next = $this->middlewares[$this->index] ?? null;
23+
24+
if ($next === null) {
25+
throw new NoMoreMiddleware();
26+
}
27+
28+
$this->index++;
29+
30+
return $next;
31+
}
32+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Middleware;
6+
7+
use Patchlevel\Hydrator\CircularReference;
8+
use Patchlevel\Hydrator\DenormalizationFailure;
9+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
10+
use Patchlevel\Hydrator\NormalizationFailure;
11+
use Patchlevel\Hydrator\NormalizationMissing;
12+
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
13+
use Patchlevel\Hydrator\TypeMismatch;
14+
use ReflectionParameter;
15+
use Throwable;
16+
use TypeError;
17+
use function array_key_exists;
18+
use function is_object;
19+
20+
final class TransformMiddleware implements Middleware
21+
{
22+
/**
23+
* @param ClassMetadata<T> $metadata
24+
* @param array<string, mixed> $data
25+
*
26+
* @return T
27+
*
28+
* @template T of object
29+
*/
30+
public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object
31+
{
32+
$object = $metadata->newInstance();
33+
34+
$constructorParameters = null;
35+
36+
foreach ($metadata->properties() as $propertyMetadata) {
37+
if (!array_key_exists($propertyMetadata->fieldName(), $data)) {
38+
if (!$propertyMetadata->reflection()->isPromoted()) {
39+
continue;
40+
}
41+
42+
if ($constructorParameters === null) {
43+
$constructorParameters = $this->promotedConstructorParametersWithDefaultValue($metadata);
44+
}
45+
46+
if (!array_key_exists($propertyMetadata->propertyName(), $constructorParameters)) {
47+
continue;
48+
}
49+
50+
/** @psalm-suppress MixedAssignment */
51+
$defaultValue = $constructorParameters[$propertyMetadata->propertyName()]->getDefaultValue();
52+
$propertyMetadata->setValue($object, $defaultValue);
53+
54+
continue;
55+
}
56+
57+
/** @psalm-suppress MixedAssignment */
58+
$value = $data[$propertyMetadata->fieldName()];
59+
60+
$normalizer = $propertyMetadata->normalizer();
61+
62+
if ($normalizer) {
63+
if ($normalizer instanceof HydratorAwareNormalizer) {
64+
$normalizer->setHydrator($stack->hydrator);
65+
}
66+
67+
try {
68+
/** @psalm-suppress MixedAssignment */
69+
$value = $normalizer->denormalize($value);
70+
} catch (Throwable $e) {
71+
throw new DenormalizationFailure(
72+
$metadata->className(),
73+
$propertyMetadata->propertyName(),
74+
$normalizer::class,
75+
$e,
76+
);
77+
}
78+
}
79+
80+
try {
81+
$propertyMetadata->setValue($object, $value);
82+
} catch (TypeError $e) {
83+
throw new TypeMismatch(
84+
$metadata->className(),
85+
$propertyMetadata->propertyName(),
86+
$e,
87+
);
88+
}
89+
}
90+
91+
foreach ($metadata->postHydrateCallbacks() as $callback) {
92+
$callback->invoke($object);
93+
}
94+
95+
return $object;
96+
}
97+
98+
/** @return array<string, mixed> */
99+
public function extract(ClassMetadata $metadata, object $object, Stack $stack): array
100+
{
101+
foreach ($metadata->preExtractCallbacks() as $callback) {
102+
$callback->invoke($object);
103+
}
104+
105+
$data = [];
106+
107+
foreach ($metadata->properties() as $propertyMetadata) {
108+
/** @psalm-suppress MixedAssignment */
109+
$value = $propertyMetadata->getValue($object);
110+
111+
$normalizer = $propertyMetadata->normalizer();
112+
113+
if ($normalizer) {
114+
if ($normalizer instanceof HydratorAwareNormalizer) {
115+
$normalizer->setHydrator($stack->hydrator);
116+
}
117+
118+
try {
119+
/** @psalm-suppress MixedAssignment */
120+
$value = $normalizer->normalize($value);
121+
} catch (CircularReference $e) {
122+
throw $e;
123+
} catch (Throwable $e) {
124+
throw new NormalizationFailure(
125+
$object::class,
126+
$propertyMetadata->propertyName(),
127+
$normalizer::class,
128+
$e,
129+
);
130+
}
131+
}
132+
133+
if (is_object($value)) {
134+
throw new NormalizationMissing($object::class, $propertyMetadata->propertyName());
135+
}
136+
137+
/** @psalm-suppress MixedAssignment */
138+
$data[$propertyMetadata->fieldName()] = $value;
139+
}
140+
141+
return $data;
142+
}
143+
144+
/** @return array<string, ReflectionParameter> */
145+
private function promotedConstructorParametersWithDefaultValue(ClassMetadata $metadata): array
146+
{
147+
$constructor = $metadata->reflection()->getConstructor();
148+
149+
if (!$constructor) {
150+
return [];
151+
}
152+
153+
$parameters = $constructor->getParameters();
154+
$result = [];
155+
156+
foreach ($parameters as $parameter) {
157+
if (!$parameter->isPromoted()) {
158+
continue;
159+
}
160+
161+
if (!$parameter->isDefaultValueAvailable()) {
162+
continue;
163+
}
164+
165+
$result[$parameter->getName()] = $parameter;
166+
}
167+
168+
return $result;
169+
}
170+
}

0 commit comments

Comments
 (0)