Skip to content

Commit b33d9f9

Browse files
authored
Merge pull request #192 from patchlevel/upcaster
2 parents dd868eb + be7d588 commit b33d9f9

14 files changed

Lines changed: 448 additions & 5 deletions

phpstan-baseline.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ parameters:
198198
count: 1
199199
path: tests/Unit/Extension/Cryptography/Fixture/ChildWithSensitiveDataWithIdentifierDto.php
200200

201+
-
202+
message: '#^Binary operation "\." between mixed and '' '' results in an error\.$#'
203+
identifier: binaryOp.invalid
204+
count: 1
205+
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php
206+
207+
-
208+
message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#'
209+
identifier: binaryOp.invalid
210+
count: 1
211+
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php
212+
201213
-
202214
message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:postHydrate\(\) is unused\.$#'
203215
identifier: method.unused

src/CoreExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ final class CoreExtension implements Extension
1212
{
1313
public function configure(StackHydratorBuilder $builder): void
1414
{
15-
$builder->addMiddleware(new TransformMiddleware(), -64);
15+
$builder->addMiddleware(new TransformMiddleware(), Extension::PRIORITY_TRANSFORM);
1616
$builder->addGuesser(new BuiltInGuesser(), -64);
1717
}
1818
}

src/Extension.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,17 @@
77
/** @experimental */
88
interface Extension
99
{
10+
/** Reshape the raw stored payload before its values are decoded. */
11+
public const PRIORITY_BEFORE_ENCODING = 64;
12+
13+
/** Encode or decode individual field values, the shape stays the same. */
14+
public const PRIORITY_ENCODING = 32;
15+
16+
/** Last structural step before the array becomes an object. */
17+
public const PRIORITY_BEFORE_TRANSFORM = 0;
18+
19+
/** Build the object from the array and deconstruct it again. */
20+
public const PRIORITY_TRANSFORM = -64;
21+
1022
public function configure(StackHydratorBuilder $builder): void;
1123
}

src/Extension/Cryptography/CryptographyExtension.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __construct(
2121
public function configure(StackHydratorBuilder $builder): void
2222
{
2323
$builder->addMetadataEnricher(new CryptographyMetadataEnricher(), 64);
24-
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64);
24+
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), Extension::PRIORITY_ENCODING);
2525

2626
if ($this->legacyMetadataMapping) {
2727
$builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 63);
@@ -31,6 +31,10 @@ public function configure(StackHydratorBuilder $builder): void
3131
return;
3232
}
3333

34-
$builder->addMiddleware(new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer), 65);
34+
// the legacy decrypt has to run before the regular decryption
35+
$builder->addMiddleware(
36+
new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer),
37+
Extension::PRIORITY_ENCODING + 1,
38+
);
3539
}
3640
}

src/Extension/Lifecycle/LifecycleExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
{
1313
public function configure(StackHydratorBuilder $builder): void
1414
{
15-
$builder->addMiddleware(new LifecycleMiddleware());
15+
$builder->addMiddleware(new LifecycleMiddleware(), Extension::PRIORITY_BEFORE_TRANSFORM);
1616
$builder->addMetadataEnricher(new LifecycleMetadataEnricher());
1717
}
1818
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Upcast;
6+
7+
use Closure;
8+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
9+
10+
/** @experimental */
11+
final class CallbackUpcaster implements Upcaster
12+
{
13+
/** @var Closure(array<string, mixed>, array<string, mixed>): array<string, mixed> */
14+
private readonly Closure $callback;
15+
16+
/**
17+
* @param class-string $className
18+
* @param callable(array<string, mixed>, array<string, mixed>): array<string, mixed> $callback
19+
*/
20+
public function __construct(private readonly string $className, callable $callback)
21+
{
22+
$this->callback = Closure::fromCallable($callback);
23+
}
24+
25+
/**
26+
* @param class-string $className
27+
* @param callable(array<string, mixed>, array<string, mixed>): array<string, mixed> $callback
28+
*/
29+
public static function forClass(string $className, callable $callback): self
30+
{
31+
return new self($className, $callback);
32+
}
33+
34+
/**
35+
* @param ClassMetadata<T> $metadata
36+
* @param array<string, mixed> $data
37+
* @param array<string, mixed> $context
38+
*
39+
* @return array<string, mixed>
40+
*
41+
* @template T of object
42+
*/
43+
public function upcast(ClassMetadata $metadata, array $data, array $context): array
44+
{
45+
if ($metadata->className !== $this->className) {
46+
return $data;
47+
}
48+
49+
return ($this->callback)($data, $context);
50+
}
51+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Upcast;
6+
7+
use Patchlevel\Hydrator\Extension;
8+
use Patchlevel\Hydrator\StackHydratorBuilder;
9+
10+
/** @experimental */
11+
final readonly class UpcastExtension implements Extension
12+
{
13+
/**
14+
* @param list<Upcaster> $beforeEncoding upcasters that reshape the raw stored payload before its values are decoded
15+
* @param list<Upcaster> $beforeTransform upcasters that run after value decoding, right before the object is built
16+
*/
17+
public function __construct(
18+
private array $beforeEncoding = [],
19+
private array $beforeTransform = [],
20+
) {
21+
}
22+
23+
public function configure(StackHydratorBuilder $builder): void
24+
{
25+
if ($this->beforeEncoding !== []) {
26+
$builder->addMiddleware(
27+
new UpcastMiddleware($this->beforeEncoding),
28+
Extension::PRIORITY_BEFORE_ENCODING,
29+
);
30+
}
31+
32+
if ($this->beforeTransform === []) {
33+
return;
34+
}
35+
36+
$builder->addMiddleware(
37+
new UpcastMiddleware($this->beforeTransform),
38+
Extension::PRIORITY_BEFORE_TRANSFORM,
39+
);
40+
}
41+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Upcast;
6+
7+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
8+
use Patchlevel\Hydrator\Middleware\Middleware;
9+
use Patchlevel\Hydrator\Middleware\Stack;
10+
11+
/** @experimental */
12+
final readonly class UpcastMiddleware implements Middleware
13+
{
14+
/** @param list<Upcaster> $upcasters */
15+
public function __construct(
16+
private array $upcasters,
17+
) {
18+
}
19+
20+
/**
21+
* @param ClassMetadata<T> $metadata
22+
* @param array<string, mixed> $data
23+
* @param array<string, mixed> $context
24+
*
25+
* @return T
26+
*
27+
* @template T of object
28+
*/
29+
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
30+
{
31+
foreach ($this->upcasters as $upcaster) {
32+
$data = $upcaster->upcast($metadata, $data, $context);
33+
}
34+
35+
return $stack->next()->hydrate($metadata, $data, $context, $stack);
36+
}
37+
38+
/**
39+
* @param ClassMetadata<T> $metadata
40+
* @param T $object
41+
* @param array<string, mixed> $context
42+
*
43+
* @return array<string, mixed>
44+
*
45+
* @template T of object
46+
*/
47+
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
48+
{
49+
return $stack->next()->extract($metadata, $object, $context, $stack);
50+
}
51+
}

src/Extension/Upcast/Upcaster.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Upcast;
6+
7+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
8+
9+
/** @experimental */
10+
interface Upcaster
11+
{
12+
/**
13+
* @param ClassMetadata<T> $metadata
14+
* @param array<string, mixed> $data
15+
* @param array<string, mixed> $context
16+
*
17+
* @return array<string, mixed>
18+
*
19+
* @template T of object
20+
*/
21+
public function upcast(ClassMetadata $metadata, array $data, array $context): array;
22+
}

src/StackHydratorBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final class StackHydratorBuilder
3535
private CacheItemPoolInterface|CacheInterface|null $cache = null;
3636

3737
/** @return $this */
38-
public function addMiddleware(Middleware $middleware, int $priority = 0): static
38+
public function addMiddleware(Middleware $middleware, int $priority = Extension::PRIORITY_BEFORE_TRANSFORM): static
3939
{
4040
$this->middlewares[$priority][] = $middleware;
4141

0 commit comments

Comments
 (0)