Skip to content

Commit 4af297b

Browse files
committed
add upcaster extension
1 parent 2c2a383 commit 4af297b

13 files changed

Lines changed: 464 additions & 13 deletions

phpstan-baseline.neon

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,6 @@ parameters:
108108
count: 2
109109
path: src/Middleware/TransformMiddleware.php
110110

111-
-
112-
message: '#^Attribute class Patchlevel\\Hydrator\\Normalizer\\Deprecated does not exist\.$#'
113-
identifier: attribute.notFound
114-
count: 1
115-
path: src/Normalizer/EnumNormalizer.php
116-
117111
-
118112
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
119113
identifier: assign.propertyType
@@ -127,15 +121,15 @@ parameters:
127121
path: src/Normalizer/ObjectNormalizer.php
128122

129123
-
130-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Middleware\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
124+
message: '#^Parameter \#1 \$middlewares of class Patchlevel\\Hydrator\\Middleware\\Stack constructor expects non\-empty\-list\<Patchlevel\\Hydrator\\Middleware\\Middleware\>, list\<Patchlevel\\Hydrator\\Middleware\\Middleware\> given\.$#'
131125
identifier: argument.type
132-
count: 3
126+
count: 4
133127
path: src/StackHydrator.php
134128

135129
-
136-
message: '#^Parameter \#1 \$middlewares of class Patchlevel\\Hydrator\\Middleware\\Stack constructor expects non\-empty\-list\<Patchlevel\\Hydrator\\Middleware\\Middleware\>, list\<Patchlevel\\Hydrator\\Middleware\\Middleware\> given\.$#'
130+
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Middleware\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
137131
identifier: argument.type
138-
count: 4
132+
count: 3
139133
path: src/StackHydrator.php
140134

141135
-
@@ -144,6 +138,36 @@ parameters:
144138
count: 1
145139
path: tests/Unit/Extension/Cryptography/Fixture/ChildWithSensitiveDataWithIdentifierDto.php
146140

141+
-
142+
message: '#^Class Patchlevel\\Hydrator\\Cryptography\\PayloadCryptographer not found\.$#'
143+
identifier: class.notFound
144+
count: 1
145+
path: tests/Unit/Extension/Upcast/UpcastExtensionTest.php
146+
147+
-
148+
message: '#^Class Patchlevel\\Hydrator\\Extension\\Cryptography\\CryptographyExtension constructor invoked with 2 parameters, 1 required\.$#'
149+
identifier: arguments.count
150+
count: 1
151+
path: tests/Unit/Extension/Upcast/UpcastExtensionTest.php
152+
153+
-
154+
message: '#^Class Patchlevel\\Hydrator\\Extension\\Cryptography\\LegacyCryptographyDecryptMiddleware not found\.$#'
155+
identifier: class.notFound
156+
count: 1
157+
path: tests/Unit/Extension/Upcast/UpcastExtensionTest.php
158+
159+
-
160+
message: '#^Binary operation "\." between mixed and '' '' results in an error\.$#'
161+
identifier: binaryOp.invalid
162+
count: 1
163+
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php
164+
165+
-
166+
message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#'
167+
identifier: binaryOp.invalid
168+
count: 1
169+
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php
170+
147171
-
148172
message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\IdNormalizer\:\:\$idClass \(class\-string\<Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\Id\>\|null\) does not accept string\.$#'
149173
identifier: assign.propertyType

src/CoreExtension.php

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

src/Extension.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,17 @@
66

77
interface Extension
88
{
9+
/** Reshape the raw stored payload before its values are decoded. */
10+
public const PRIORITY_BEFORE_ENCODING = 64;
11+
12+
/** Encode or decode individual field values, the shape stays the same. */
13+
public const PRIORITY_ENCODING = 32;
14+
15+
/** Last structural step before the array becomes an object. */
16+
public const PRIORITY_BEFORE_TRANSFORM = 0;
17+
18+
/** Build the object from the array and deconstruct it again. */
19+
public const PRIORITY_TRANSFORM = -64;
20+
921
public function configure(StackHydratorBuilder $builder): void;
1022
}

src/Extension/Lifecycle/LifecycleExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
{
1212
public function configure(StackHydratorBuilder $builder): void
1313
{
14-
$builder->addMiddleware(new LifecycleMiddleware());
14+
$builder->addMiddleware(new LifecycleMiddleware(), Extension::PRIORITY_BEFORE_TRANSFORM);
1515
$builder->addMetadataEnricher(new LifecycleMetadataEnricher());
1616
}
1717
}
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
@@ -34,7 +34,7 @@ final class StackHydratorBuilder
3434
private CacheItemPoolInterface|CacheInterface|null $cache = null;
3535

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Extension\Upcast;
6+
7+
use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
8+
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
9+
use Patchlevel\Hydrator\Tests\Unit\Extension\Lifecycle\Fixture\LifecycleFixture;
10+
use Patchlevel\Hydrator\Tests\Unit\Extension\Upcast\Fixture\UpcastFixture;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\TestCase;
13+
14+
use function assert;
15+
use function is_string;
16+
17+
#[CoversClass(CallbackUpcaster::class)]
18+
final class CallbackUpcasterTest extends TestCase
19+
{
20+
public function testUpcast(): void
21+
{
22+
$metadata = (new AttributeMetadataFactory())->metadata(UpcastFixture::class);
23+
$upcaster = CallbackUpcaster::forClass(
24+
UpcastFixture::class,
25+
static function (array $data, array $context): array {
26+
$prefix = $context['prefix'] ?? '';
27+
$name = $data['name'] ?? '';
28+
assert(is_string($prefix));
29+
assert(is_string($name));
30+
31+
$data['name'] = $prefix . $name;
32+
33+
return $data;
34+
},
35+
);
36+
37+
self::assertSame(
38+
['name' => 'Upcast: foo'],
39+
$upcaster->upcast($metadata, ['name' => 'foo'], ['prefix' => 'Upcast: ']),
40+
);
41+
}
42+
43+
public function testSkipDifferentClass(): void
44+
{
45+
$metadata = (new AttributeMetadataFactory())->metadata(LifecycleFixture::class);
46+
$upcaster = CallbackUpcaster::forClass(
47+
UpcastFixture::class,
48+
static fn (array $data): array => ['name' => 'changed'],
49+
);
50+
51+
self::assertSame(['name' => 'foo'], $upcaster->upcast($metadata, ['name' => 'foo'], []));
52+
}
53+
}

0 commit comments

Comments
 (0)