diff --git a/src/CoreExtension.php b/src/CoreExtension.php new file mode 100644 index 0000000..b66c551 --- /dev/null +++ b/src/CoreExtension.php @@ -0,0 +1,25 @@ + */ + public function middlewares(): iterable + { + yield new TransformMiddleware(); + } + + /** @return iterable */ + public function guesser(): iterable + { + yield new BuiltInGuesser(); + } +} diff --git a/src/Cryptography/CryptographyExtension.php b/src/Cryptography/CryptographyExtension.php new file mode 100644 index 0000000..23a5cc6 --- /dev/null +++ b/src/Cryptography/CryptographyExtension.php @@ -0,0 +1,30 @@ + */ + public function middlewares(): iterable + { + yield [new CryptographyMiddleware($this->cryptography), 64]; + } + + /** @return iterable */ + public function metadataEnrichers(): iterable + { + yield [new CryptographyMetadataEnricher(), 64]; + } +} diff --git a/src/Cryptography/CryptographyMetadataFactory.php b/src/Cryptography/CryptographyMetadataEnricher.php similarity index 69% rename from src/Cryptography/CryptographyMetadataFactory.php rename to src/Cryptography/CryptographyMetadataEnricher.php index d74c3ee..3569a8f 100644 --- a/src/Cryptography/CryptographyMetadataFactory.php +++ b/src/Cryptography/CryptographyMetadataEnricher.php @@ -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); @@ -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, ); @@ -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 diff --git a/src/GuesserProvider.php b/src/GuesserProvider.php new file mode 100644 index 0000000..6d3d837 --- /dev/null +++ b/src/GuesserProvider.php @@ -0,0 +1,13 @@ + */ + public function guesser(): iterable; +} diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index 36ba797..2355697 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -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(); } /** diff --git a/src/Metadata/MetadataEnricher.php b/src/Metadata/MetadataEnricher.php new file mode 100644 index 0000000..5516db1 --- /dev/null +++ b/src/Metadata/MetadataEnricher.php @@ -0,0 +1,10 @@ + */ + public function metadataEnrichers(): iterable; +} diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index 4c0ca44..483984e 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -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; @@ -26,10 +27,14 @@ final class MetadataHydrator implements Hydrator /** @var array */ private array $classMetadata = []; - /** @param list $middlewares */ + /** + * @param list $middlewares + * @param list $metadataEnrichers + */ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), private readonly array $middlewares = [], + private readonly array $metadataEnrichers = [], private readonly bool $defaultLazy = false, ) { } @@ -110,32 +115,68 @@ private function metadata(string $class): ClassMetadata $property->normalizer->setHydrator($this); } + foreach ($this->metadataEnrichers as $enricher) { + $enricher->enrich($metadata); + } + return $metadata; } - /** - * @param list $additionalMiddleware - * @param iterable $guessers - */ + /** @param iterable $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; + } else { + $middlewares[$entry[1] ?? 0][] = $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]; + } + } + } + + if (!($extension instanceof GuesserProvider)) { + continue; + } + + foreach ($extension->guesser() as $entry) { + if ($entry instanceof Guesser) { + $guessers[0][] = $entry; + } else { + $guessers[$entry[1] ?? 0][] = $entry[0]; + } + } } + krsort($middlewares); + krsort($enrichers); + krsort($guessers); + return new self( new AttributeMetadataFactory( - guesser: $guesser, + guesser: new ChainGuesser([...array_merge(...$guessers)]), ), - [...$additionalMiddleware, new TransformMiddleware()], + [...array_merge(...$middlewares)], + [...array_merge(...$enrichers)], $defaultLazy, ); } diff --git a/src/MiddlewareProvider.php b/src/MiddlewareProvider.php new file mode 100644 index 0000000..38b23b3 --- /dev/null +++ b/src/MiddlewareProvider.php @@ -0,0 +1,13 @@ + */ + public function middlewares(): iterable; +} diff --git a/tests/Benchmark/HydratorWithCryptographyBench.php b/tests/Benchmark/HydratorWithCryptographyBench.php index 4e4af49..082a561 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/HydratorWithCryptographyBench.php @@ -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; @@ -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 diff --git a/tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php b/tests/Unit/Cryptography/CryptographyMetadataEnricherTest.php similarity index 85% rename from tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php rename to tests/Unit/Cryptography/CryptographyMetadataEnricherTest.php index 7a07c30..b030daf 100644 --- a/tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php +++ b/tests/Unit/Cryptography/CryptographyMetadataEnricherTest.php @@ -7,19 +7,20 @@ use Patchlevel\Hydrator\Attribute\DataSubjectId; use Patchlevel\Hydrator\Attribute\NormalizedName; use Patchlevel\Hydrator\Attribute\SensitiveData; -use Patchlevel\Hydrator\Cryptography\CryptographyMetadataFactory; +use Patchlevel\Hydrator\Cryptography\CryptographyMetadataEnricher; use Patchlevel\Hydrator\Cryptography\DuplicateSubjectIdIdentifier; use Patchlevel\Hydrator\Cryptography\SensitiveDataInfo; use Patchlevel\Hydrator\Cryptography\SubjectIdAndSensitiveDataConflict; use Patchlevel\Hydrator\Cryptography\SubjectIdFieldMapping; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; +use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithSensitiveDataDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentWithSensitiveDataWithIdentifierDto; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[CoversClass(CryptographyMetadataFactory::class)] -final class CryptographyMetadataFactoryTest extends TestCase +#[CoversClass(CryptographyMetadataEnricher::class)] +final class CryptographyMetadataEnricherTest extends TestCase { public function testSensitiveData(): void { @@ -35,8 +36,7 @@ public function __construct( } }; - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadata = $metadataFactory->metadata($event::class); + $metadata = $this->metadata($event::class); self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; @@ -66,8 +66,7 @@ public function __construct( $this->expectException(SubjectIdAndSensitiveDataConflict::class); - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadataFactory->metadata($event::class); + $this->metadata($event::class); } public function testMultipleDataSubjectIdWithSameIdentifier(): void @@ -84,8 +83,7 @@ public function __construct( $this->expectException(DuplicateSubjectIdIdentifier::class); - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadataFactory->metadata($event::class); + $this->metadata($event::class); } public function testSensitiveDataWithMultipleDataSubjectIdWithDifferentNames(): void @@ -108,8 +106,7 @@ public function __construct( } }; - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadata = $metadataFactory->metadata($event::class); + $metadata = $this->metadata($event::class); self::assertArrayHasKey(SubjectIdFieldMapping::class, $metadata->extras); $subjectIdFieldMapping = $metadata->extras[SubjectIdFieldMapping::class]; @@ -155,17 +152,15 @@ public function __construct( } }; - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $this->expectException(DuplicateSubjectIdIdentifier::class); $this->expectExceptionMessageMatches('/Duplicate subject id identifier found\. Used foo for .*::fooId and .*::barId\./'); - $metadataFactory->metadata($event::class); + + $this->metadata($event::class); } public function testExtendsWithSensitiveData(): void { - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadata = $metadataFactory->metadata(ParentWithSensitiveDataDto::class); + $metadata = $this->metadata(ParentWithSensitiveDataDto::class); self::assertCount(2, $metadata->properties); @@ -186,8 +181,7 @@ public function testExtendsWithSensitiveData(): void public function testExtendsWithSensitiveDataWithName(): void { - $metadataFactory = new CryptographyMetadataFactory(new AttributeMetadataFactory()); - $metadata = $metadataFactory->metadata(ParentWithSensitiveDataWithIdentifierDto::class); + $metadata = $this->metadata(ParentWithSensitiveDataWithIdentifierDto::class); self::assertCount(2, $metadata->properties); @@ -205,4 +199,13 @@ public function testExtendsWithSensitiveDataWithName(): void self::assertSame('profile', $sensitiveDataInfo->subjectIdName); self::assertSame(null, $sensitiveDataInfo->fallback); } + + /** @param class-string $class */ + private function metadata(string $class): ClassMetadata + { + $metadata = (new AttributeMetadataFactory())->metadata($class); + (new CryptographyMetadataEnricher())->enrich($metadata); + + return $metadata; + } } diff --git a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php index b34c6f0..17d2f0c 100644 --- a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php +++ b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php @@ -9,7 +9,7 @@ use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey; use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory; use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed; -use Patchlevel\Hydrator\Cryptography\CryptographyMetadataFactory; +use Patchlevel\Hydrator\Cryptography\CryptographyMetadataEnricher; use Patchlevel\Hydrator\Cryptography\MissingSubjectId; use Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer; use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists; @@ -474,10 +474,9 @@ public function testCreateWithOpenssl(): void /** @param class-string $class */ private function metadata(string $class): ClassMetadata { - $factory = new CryptographyMetadataFactory( - new AttributeMetadataFactory(), - ); + $metadata = (new AttributeMetadataFactory())->metadata($class); + (new CryptographyMetadataEnricher())->enrich($metadata); - return $factory->metadata($class); + return $metadata; } } diff --git a/tests/Unit/Fixture/Extension.php b/tests/Unit/Fixture/Extension.php new file mode 100644 index 0000000..acbc526 --- /dev/null +++ b/tests/Unit/Fixture/Extension.php @@ -0,0 +1,45 @@ + $middlewares + * @param iterable $metadataEnrichers + * @param iterable $guessers + */ + public function __construct( + private iterable $middlewares = [], + private iterable $metadataEnrichers = [], + private iterable $guessers = [], + ) { + } + + /** @return iterable */ + public function guesser(): iterable + { + return $this->guessers; + } + + /** @return iterable */ + public function metadataEnrichers(): iterable + { + return $this->metadataEnrichers; + } + + /** @return iterable */ + public function middlewares(): iterable + { + return $this->middlewares; + } +} diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index e0355f5..560a0c7 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -9,7 +9,7 @@ use DateTimeZone; use Patchlevel\Hydrator\CircularReference; use Patchlevel\Hydrator\ClassNotSupported; -use Patchlevel\Hydrator\Cryptography\CryptographyMiddleware; +use Patchlevel\Hydrator\Cryptography\CryptographyExtension; use Patchlevel\Hydrator\Cryptography\PayloadCryptographer; use Patchlevel\Hydrator\DenormalizationFailure; use Patchlevel\Hydrator\Guesser\Guesser; @@ -26,6 +26,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; +use Patchlevel\Hydrator\Tests\Unit\Fixture\Extension; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto; @@ -153,6 +154,7 @@ public function testExtractWithContext(): void 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', 'dateTime' => '2015-02-13T22:34:32+01:00', 'dateTimeZone' => 'EDT', + 'array' => ['foo'], ]; $middleware = $this->createMock(Middleware::class); @@ -166,7 +168,7 @@ public function testExtractWithContext(): void $this->isInstanceOf(Stack::class), )->willReturn($expect); - $hydrator = MetadataHydrator::create([$middleware]); + $hydrator = MetadataHydrator::create([new Extension([$middleware])]); $data = $hydrator->extract($object, ['context' => '123']); @@ -270,6 +272,7 @@ public function testHydrateWithContext(): void 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', 'dateTime' => '2015-02-13T22:34:32+01:00', 'dateTimeZone' => 'EDT', + 'array' => ['foo'], ]; $middleware = $this->createMock(Middleware::class); @@ -283,7 +286,7 @@ public function testHydrateWithContext(): void $this->isInstanceOf(Stack::class), )->willReturn($expect); - $hydrator = MetadataHydrator::create([$middleware]); + $hydrator = MetadataHydrator::create([new Extension([$middleware])]); $object = $hydrator->hydrate(InferNormalizerDto::class, $data, ['context' => '123']); @@ -328,13 +331,7 @@ public function testDecrypt(): void ->with($metadataFactory->metadata(ProfileCreated::class), $encryptedPayload) ->willReturn($payload); - $hydrator = new MetadataHydrator( - $metadataFactory, - [ - new CryptographyMiddleware($cryptographer), - new TransformMiddleware(), - ], - ); + $hydrator = MetadataHydrator::create([new CryptographyExtension($cryptographer)]); $return = $hydrator->hydrate(ProfileCreated::class, $encryptedPayload); @@ -360,13 +357,9 @@ public function testEncrypt(): void ->with($metadataFactory->metadata(ProfileCreated::class), $payload) ->willReturn($encryptedPayload); - $hydrator = new MetadataHydrator( - $metadataFactory, - [ - new CryptographyMiddleware($cryptographer), - new TransformMiddleware(), - ], - ); + $hydrator = MetadataHydrator::create([ + new CryptographyExtension($cryptographer), + ]); $return = $hydrator->extract($object); @@ -563,38 +556,50 @@ public function guess(ObjectType $type): Normalizer|null $hydrator = MetadataHydrator::create( [ - new class implements Middleware - { - /** - * @param ClassMetadata $metadata - * @param array $data - * @param array $context - * - * @return T - * - * @template T of object - */ - public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object - { - return $stack->next()->hydrate($metadata, $data, $context, $stack); - } - - /** - * @param ClassMetadata $metadata - * @param T $object - * @param array $context - * - * @return array - * - * @template T of object - */ - public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array - { - return $stack->next()->extract($metadata, $object, $context, $stack); - } - }, + new Extension( + [ + new class implements Middleware { + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate( + ClassMetadata $metadata, + array $data, + array $context, + Stack $stack, + ): object { + return $stack->next()->hydrate($metadata, $data, $context, $stack); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * @param array $context + * + * @return array + * + * @template T of object + */ + public function extract( + ClassMetadata $metadata, + object $object, + array $context, + Stack $stack, + ): array { + return $stack->next()->extract($metadata, $object, $context, $stack); + } + }, + ], + [], + [$guesser], + ), ], - [$guesser], ); $hydrator->extract(new InferNormalizerDto(