Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/CoreExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Guesser\Guesser;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\TransformMiddleware;

final class CoreExtension implements MiddlewareProvider, GuesserProvider
{
/** @return iterable<Middleware|array{0: Middleware, 1?: int}> */
public function middlewares(): iterable
{
yield new TransformMiddleware();
}

/** @return iterable<Guesser|array{0: Guesser, 1?: int}> */
public function guesser(): iterable
{
yield new BuiltInGuesser();
}
}
30 changes: 30 additions & 0 deletions src/Cryptography/CryptographyExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography;

use Patchlevel\Hydrator\Metadata\MetadataEnricher;
use Patchlevel\Hydrator\MetadataEnricherProvider;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\MiddlewareProvider;

final class CryptographyExtension implements MiddlewareProvider, MetadataEnricherProvider
Copy link
Member

@DanielBadura DanielBadura Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other idea which i have in mind is: instead of this provider use attributes to define middleware/metadataenricher?

#[Middleware(priority: 64)]
final readonly class CryptographyMiddleware implements Middleware {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I do not want to work with attributes here. I think attributes are good if you want to enrich your own code (from your domain) with information from external sources. But not as an extension point for the library. I would always prefer interfaces for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont want to remove the interface (I missed it in the small code example) it's more about the provider / configuration of the priority

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension is there to register the middleware, etc. I find it unnecessary to read the attributes from it. In my opinion, that adds no value and only complicates things.

I am also considering whether interfaces like MiddlewareProvider are overkill, and whether it would be better to simply provide an abstract class Extension that predefines methods with empty arrays. That would also make things simpler.

{
public function __construct(
private readonly PayloadCryptographer $cryptography,
) {
}

/** @return iterable<Middleware|array{0: Middleware, 1?: int}> */
public function middlewares(): iterable
{
yield [new CryptographyMiddleware($this->cryptography), 64];
}

/** @return iterable<MetadataEnricher|array{0: MetadataEnricher, 1?: int}> */
public function metadataEnrichers(): iterable
{
yield [new CryptographyMetadataEnricher(), 64];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,18 @@
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\SensitiveData;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Metadata\MetadataEnricher;
use ReflectionProperty;

use function array_key_exists;

final class CryptographyMetadataFactory implements MetadataFactory
final class CryptographyMetadataEnricher implements MetadataEnricher
{
public function __construct(
private readonly MetadataFactory $metadataFactory,
) {
}

public function metadata(string $class): ClassMetadata
public function enrich(ClassMetadata $classMetadata): void
{
$metadata = $this->metadataFactory->metadata($class);

$subjectIdMapping = [];

foreach ($metadata->properties as $property) {
foreach ($classMetadata->properties as $property) {
$isSubjectId = false;
$attributeReflectionList = $property->reflection->getAttributes(DataSubjectId::class);

Expand All @@ -34,8 +27,8 @@ public function metadata(string $class): ClassMetadata

if (array_key_exists($subjectIdIdentifier, $subjectIdMapping)) {
throw new DuplicateSubjectIdIdentifier(
$metadata->className,
$metadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName,
$classMetadata->className,
$classMetadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName,
$property->propertyName,
$subjectIdIdentifier,
);
Expand All @@ -53,17 +46,17 @@ public function metadata(string $class): ClassMetadata
}

if ($isSubjectId) {
throw new SubjectIdAndSensitiveDataConflict($metadata->className, $property->propertyName);
throw new SubjectIdAndSensitiveDataConflict($classMetadata->className, $property->propertyName);
}

$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
}

if ($subjectIdMapping !== []) {
$metadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping);
if ($subjectIdMapping === []) {
return;
}

return $metadata;
$classMetadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping);
}

private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null
Expand Down
13 changes: 13 additions & 0 deletions src/GuesserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Guesser\Guesser;

interface GuesserProvider
{
/** @return iterable<Guesser|array{0: Guesser, 1?: int}> */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** @return iterable<Guesser|array{0: Guesser, 1?: int}> */
/** @return iterable<array{0: Guesser, 1?: int}> */

I think reducing it to this is a better api, otherwise we have 2 version's to omit the priority as the also allow here the priority to be missing. wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think either "type" is harmful, and I like the DX of the first one. Besides, it is practically the same principle as in Symfony.

public function guesser(): iterable;
}
2 changes: 1 addition & 1 deletion src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function __construct(
Guesser|null $guesser = null,
) {
$this->typeResolver = $typeResolver ?: TypeResolver::create();
$this->guesser = $guesser ?: new BuiltInGuesser(false);
$this->guesser = $guesser ?: new BuiltInGuesser();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Metadata/MetadataEnricher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Metadata;

interface MetadataEnricher
{
public function enrich(ClassMetadata $classMetadata): void;
}
13 changes: 13 additions & 0 deletions src/MetadataEnricherProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Metadata\MetadataEnricher;

interface MetadataEnricherProvider
{
/** @return iterable<MetadataEnricher|array{0: MetadataEnricher, 1?: int}> */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

public function metadataEnrichers(): iterable;
}
75 changes: 58 additions & 17 deletions src/MetadataHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Guesser\ChainGuesser;
use Patchlevel\Hydrator\Guesser\Guesser;
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\ClassNotFound;
use Patchlevel\Hydrator\Metadata\MetadataEnricher;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;
use Patchlevel\Hydrator\Middleware\TransformMiddleware;
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
use ReflectionClass;

use function array_key_exists;
use function array_merge;
use function krsort;

use const PHP_VERSION_ID;

Expand All @@ -26,10 +27,14 @@
/** @var array<class-string, ClassMetadata> */
private array $classMetadata = [];

/** @param list<Middleware> $middlewares */
/**
* @param list<Middleware> $middlewares
* @param list<MetadataEnricher> $metadataEnrichers
*/
public function __construct(
private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(),
private readonly array $middlewares = [],
private readonly array $metadataEnrichers = [],
private readonly bool $defaultLazy = false,
) {
}
Expand Down Expand Up @@ -110,32 +115,68 @@
$property->normalizer->setHydrator($this);
}

foreach ($this->metadataEnrichers as $enricher) {
$enricher->enrich($metadata);

Check warning on line 119 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ $property->normalizer->setHydrator($this); } foreach ($this->metadataEnrichers as $enricher) { - $enricher->enrich($metadata); + } return $metadata; }
}

return $metadata;
}

/**
* @param list<Middleware> $additionalMiddleware
* @param iterable<Guesser> $guessers
*/
/** @param iterable<MiddlewareProvider|MetadataEnricherProvider|GuesserProvider> $extensions */
public static function create(
array $additionalMiddleware = [],
iterable $guessers = [],
iterable $extensions = [],
bool $defaultLazy = false,
): self {
$guesser = new BuiltInGuesser();
$extensions = [...$extensions, new CoreExtension()];

$middlewares = [];
$enrichers = [];
$guessers = [];

foreach ($extensions as $extension) {
if ($extension instanceof MiddlewareProvider) {
foreach ($extension->middlewares() as $entry) {
if ($entry instanceof Middleware) {
$middlewares[0][] = $entry;

Check warning on line 140 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ if ($extension instanceof MiddlewareProvider) { foreach ($extension->middlewares() as $entry) { if ($entry instanceof Middleware) { - $middlewares[0][] = $entry; + $middlewares[1][] = $entry; } else { $middlewares[$entry[1] ?? 0][] = $entry[0]; }
} else {
$middlewares[$entry[1] ?? 0][] = $entry[0];

Check warning on line 142 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ if ($entry instanceof Middleware) { $middlewares[0][] = $entry; } else { - $middlewares[$entry[1] ?? 0][] = $entry[0]; + $middlewares[$entry[1] ?? 1][] = $entry[0]; } } }

Check warning on line 142 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "DecrementInteger": @@ @@ if ($entry instanceof Middleware) { $middlewares[0][] = $entry; } else { - $middlewares[$entry[1] ?? 0][] = $entry[0]; + $middlewares[$entry[1] ?? -1][] = $entry[0]; } } }
}
}
}

if ($guessers !== []) {
$guesser = new ChainGuesser([
...$guessers,
$guesser,
]);
if ($extension instanceof MetadataEnricherProvider) {
foreach ($extension->metadataEnrichers() as $entry) {
if ($entry instanceof MetadataEnricher) {
$enrichers[0][] = $entry;
} else {
$enrichers[$entry[1] ?? 0][] = $entry[0];

Check warning on line 152 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ if ($entry instanceof MetadataEnricher) { $enrichers[0][] = $entry; } else { - $enrichers[$entry[1] ?? 0][] = $entry[0]; + $enrichers[$entry[1] ?? 1][] = $entry[0]; } } }

Check warning on line 152 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "DecrementInteger": @@ @@ if ($entry instanceof MetadataEnricher) { $enrichers[0][] = $entry; } else { - $enrichers[$entry[1] ?? 0][] = $entry[0]; + $enrichers[$entry[1] ?? -1][] = $entry[0]; } } }
}
}
}

if (!($extension instanceof GuesserProvider)) {
continue;
}

foreach ($extension->guesser() as $entry) {
if ($entry instanceof Guesser) {
$guessers[0][] = $entry;

Check warning on line 163 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ } foreach ($extension->guesser() as $entry) { if ($entry instanceof Guesser) { - $guessers[0][] = $entry; + $guessers[1][] = $entry; } else { $guessers[$entry[1] ?? 0][] = $entry[0]; }
} else {
$guessers[$entry[1] ?? 0][] = $entry[0];
}
}
}

krsort($middlewares);

Check warning on line 170 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ } } } - krsort($middlewares); + krsort($enrichers); krsort($guessers); return new self(new AttributeMetadataFactory(guesser: new ChainGuesser([...array_merge(...$guessers)])), [...array_merge(...$middlewares)], [...array_merge(...$enrichers)], $defaultLazy); } }
krsort($enrichers);

Check warning on line 171 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ } } krsort($middlewares); - krsort($enrichers); + krsort($guessers); return new self(new AttributeMetadataFactory(guesser: new ChainGuesser([...array_merge(...$guessers)])), [...array_merge(...$middlewares)], [...array_merge(...$enrichers)], $defaultLazy); } }
krsort($guessers);

Check warning on line 172 in src/MetadataHydrator.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ } krsort($middlewares); krsort($enrichers); - krsort($guessers); + return new self(new AttributeMetadataFactory(guesser: new ChainGuesser([...array_merge(...$guessers)])), [...array_merge(...$middlewares)], [...array_merge(...$enrichers)], $defaultLazy); } }

return new self(
new AttributeMetadataFactory(
guesser: $guesser,
guesser: new ChainGuesser([...array_merge(...$guessers)]),
),
[...$additionalMiddleware, new TransformMiddleware()],
[...array_merge(...$middlewares)],
[...array_merge(...$enrichers)],
$defaultLazy,
);
}
Expand Down
13 changes: 13 additions & 0 deletions src/MiddlewareProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Middleware\Middleware;

interface MiddlewareProvider
{
/** @return iterable<Middleware|array{0: Middleware, 1?: int}> */
public function middlewares(): iterable;
}
17 changes: 6 additions & 11 deletions tests/Benchmark/HydratorWithCryptographyBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@

namespace Patchlevel\Hydrator\Tests\Benchmark;

use Patchlevel\Hydrator\Cryptography\CryptographyMetadataFactory;
use Patchlevel\Hydrator\Cryptography\CryptographyMiddleware;
use Patchlevel\Hydrator\Cryptography\CryptographyExtension;
use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore;
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;
use Patchlevel\Hydrator\Middleware\TransformMiddleware;
use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated;
use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileId;
use Patchlevel\Hydrator\Tests\Benchmark\Fixture\Skill;
Expand All @@ -28,13 +25,11 @@ public function __construct()
{
$this->store = new InMemoryCipherKeyStore();

$this->hydrator = new MetadataHydrator(
new CryptographyMetadataFactory(new AttributeMetadataFactory()),
[
new CryptographyMiddleware(SensitiveDataPayloadCryptographer::createWithDefaultSettings($this->store)),
new TransformMiddleware(),
],
);
$this->hydrator = MetadataHydrator::create([
new CryptographyExtension(
SensitiveDataPayloadCryptographer::createWithDefaultSettings($this->store),
),
]);
}

public function setUp(): void
Expand Down
Loading
Loading