Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ parameters:
count: 1
path: tests/Unit/Extension/Cryptography/Fixture/ChildWithSensitiveDataWithIdentifierDto.php

-
message: '#^Binary operation "\." between mixed and '' '' results in an error\.$#'
identifier: binaryOp.invalid
count: 1
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php

-
message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#'
identifier: binaryOp.invalid
count: 1
path: tests/Unit/Extension/Upcast/UpcastMiddlewareTest.php

-
message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:postHydrate\(\) is unused\.$#'
identifier: method.unused
Expand Down
2 changes: 1 addition & 1 deletion src/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class CoreExtension implements Extension
{
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMiddleware(new TransformMiddleware(), -64);
$builder->addMiddleware(new TransformMiddleware(), Extension::PRIORITY_TRANSFORM);
$builder->addGuesser(new BuiltInGuesser(), -64);
}
}
12 changes: 12 additions & 0 deletions src/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,17 @@
/** @experimental */
interface Extension
{
/** Reshape the raw stored payload before its values are decoded. */
public const PRIORITY_BEFORE_ENCODING = 64;

/** Encode or decode individual field values, the shape stays the same. */
public const PRIORITY_ENCODING = 32;

/** Last structural step before the array becomes an object. */
public const PRIORITY_BEFORE_TRANSFORM = 0;

/** Build the object from the array and deconstruct it again. */
public const PRIORITY_TRANSFORM = -64;

public function configure(StackHydratorBuilder $builder): void;
}
8 changes: 6 additions & 2 deletions src/Extension/Cryptography/CryptographyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMetadataEnricher(new CryptographyMetadataEnricher(), 64);
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64);
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), Extension::PRIORITY_ENCODING);

if ($this->legacyMetadataMapping) {
$builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 63);
Expand All @@ -31,6 +31,10 @@
return;
}

$builder->addMiddleware(new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer), 65);
// the legacy decrypt has to run before the regular decryption
$builder->addMiddleware(
new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer),
Extension::PRIORITY_ENCODING + 1,

Check warning on line 37 in src/Extension/Cryptography/CryptographyExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "IncrementInteger": @@ @@ // the legacy decrypt has to run before the regular decryption $builder->addMiddleware( new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer), - Extension::PRIORITY_ENCODING + 1, + Extension::PRIORITY_ENCODING + 2, ); } }
);
}
}
2 changes: 1 addition & 1 deletion src/Extension/Lifecycle/LifecycleExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMiddleware(new LifecycleMiddleware());
$builder->addMiddleware(new LifecycleMiddleware(), Extension::PRIORITY_BEFORE_TRANSFORM);
$builder->addMetadataEnricher(new LifecycleMetadataEnricher());
}
}
51 changes: 51 additions & 0 deletions src/Extension/Upcast/CallbackUpcaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Upcast;

use Closure;
use Patchlevel\Hydrator\Metadata\ClassMetadata;

/** @experimental */
final class CallbackUpcaster implements Upcaster
{
/** @var Closure(array<string, mixed>, array<string, mixed>): array<string, mixed> */
private readonly Closure $callback;

/**
* @param class-string $className
* @param callable(array<string, mixed>, array<string, mixed>): array<string, mixed> $callback
*/
public function __construct(private readonly string $className, callable $callback)
{
$this->callback = Closure::fromCallable($callback);
}

/**
* @param class-string $className
* @param callable(array<string, mixed>, array<string, mixed>): array<string, mixed> $callback
*/
public static function forClass(string $className, callable $callback): self
{
return new self($className, $callback);
}

/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*
* @template T of object
*/
public function upcast(ClassMetadata $metadata, array $data, array $context): array
{
if ($metadata->className !== $this->className) {
return $data;

Check warning on line 46 in src/Extension/Upcast/CallbackUpcaster.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayOneItem": @@ @@ public function upcast(ClassMetadata $metadata, array $data, array $context): array { if ($metadata->className !== $this->className) { - return $data; + return count($data) > 1 ? array_slice($data, 0, 1, true) : $data; } return ($this->callback)($data, $context);
}

return ($this->callback)($data, $context);
}
}
41 changes: 41 additions & 0 deletions src/Extension/Upcast/UpcastExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Upcast;

use Patchlevel\Hydrator\Extension;
use Patchlevel\Hydrator\StackHydratorBuilder;

/** @experimental */
final readonly class UpcastExtension implements Extension
{
/**
* @param list<Upcaster> $beforeEncoding upcasters that reshape the raw stored payload before its values are decoded
* @param list<Upcaster> $beforeTransform upcasters that run after value decoding, right before the object is built
*/
public function __construct(
private array $beforeEncoding = [],
private array $beforeTransform = [],
) {
}

public function configure(StackHydratorBuilder $builder): void
{
if ($this->beforeEncoding !== []) {
$builder->addMiddleware(
new UpcastMiddleware($this->beforeEncoding),
Extension::PRIORITY_BEFORE_ENCODING,
);
}

if ($this->beforeTransform === []) {
return;
}

$builder->addMiddleware(
new UpcastMiddleware($this->beforeTransform),
Extension::PRIORITY_BEFORE_TRANSFORM,
);
}
}
51 changes: 51 additions & 0 deletions src/Extension/Upcast/UpcastMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Upcast;

use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;

/** @experimental */
final readonly class UpcastMiddleware implements Middleware
{
/** @param list<Upcaster> $upcasters */
public function __construct(
private array $upcasters,
) {
}

/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
foreach ($this->upcasters as $upcaster) {
$data = $upcaster->upcast($metadata, $data, $context);
}

return $stack->next()->hydrate($metadata, $data, $context, $stack);
}

/**
* @param ClassMetadata<T> $metadata
* @param T $object
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*
* @template T of object
*/
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
return $stack->next()->extract($metadata, $object, $context, $stack);
}
}
22 changes: 22 additions & 0 deletions src/Extension/Upcast/Upcaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Upcast;

use Patchlevel\Hydrator\Metadata\ClassMetadata;

/** @experimental */
interface Upcaster
{
/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*
* @template T of object
*/
public function upcast(ClassMetadata $metadata, array $data, array $context): array;
}
2 changes: 1 addition & 1 deletion src/StackHydratorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class StackHydratorBuilder
private CacheItemPoolInterface|CacheInterface|null $cache = null;

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

Expand Down
53 changes: 53 additions & 0 deletions tests/Unit/Extension/Upcast/CallbackUpcasterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Extension\Upcast;

use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Tests\Unit\Extension\Lifecycle\Fixture\LifecycleFixture;
use Patchlevel\Hydrator\Tests\Unit\Extension\Upcast\Fixture\UpcastFixture;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function assert;
use function is_string;

#[CoversClass(CallbackUpcaster::class)]
final class CallbackUpcasterTest extends TestCase
{
public function testUpcast(): void
{
$metadata = (new AttributeMetadataFactory())->metadata(UpcastFixture::class);
$upcaster = CallbackUpcaster::forClass(
UpcastFixture::class,
static function (array $data, array $context): array {
$prefix = $context['prefix'] ?? '';
$name = $data['name'] ?? '';
assert(is_string($prefix));
assert(is_string($name));

$data['name'] = $prefix . $name;

return $data;
},
);

self::assertSame(
['name' => 'Upcast: foo'],
$upcaster->upcast($metadata, ['name' => 'foo'], ['prefix' => 'Upcast: ']),
);
}

public function testSkipDifferentClass(): void
{
$metadata = (new AttributeMetadataFactory())->metadata(LifecycleFixture::class);
$upcaster = CallbackUpcaster::forClass(
UpcastFixture::class,
static fn (array $data): array => ['name' => 'changed'],
);

self::assertSame(['name' => 'foo'], $upcaster->upcast($metadata, ['name' => 'foo'], []));
}
}
13 changes: 13 additions & 0 deletions tests/Unit/Extension/Upcast/Fixture/UpcastFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Extension\Upcast\Fixture;

final class UpcastFixture
{
public function __construct(
public string $name,
) {
}
}
Loading
Loading