From 5a640a10bb3a488c0f65905be046da3b785511d6 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 18 Nov 2025 12:23:34 +0100 Subject: [PATCH 1/3] middleware based hydrator --- composer.json | 1 - composer.lock | 227 +----------------- src/Attribute/PostHydrate.php | 12 - src/Attribute/PreExtract.php | 12 - .../CryptographyMetadataFactory.php | 8 +- src/Cryptography/CryptographyMiddleware.php | 50 ++++ src/Cryptography/CryptographySubscriber.php | 36 --- .../SensitiveDataPayloadCryptographer.php | 8 +- src/Event/PostExtract.php | 17 -- src/Event/PreHydrate.php | 17 -- src/Metadata/AttributeMetadataFactory.php | 64 +---- src/Metadata/CallbackMetadata.php | 51 ---- src/Metadata/ClassMetadata.php | 22 +- src/Metadata/PropertyMetadata.php | 15 +- src/MetadataHydrator.php | 218 ++--------------- src/Middleware/Middleware.php | 30 +++ src/Middleware/NoMoreMiddleware.php | 16 ++ src/Middleware/Stack.php | 29 +++ src/Middleware/TransformMiddleware.php | 164 +++++++++++++ src/NormalizationMissing.php | 24 -- .../HydratorWithCryptographyBench.php | 16 +- .../CryptographySubscriberTest.php | 67 ------ tests/Unit/Fixture/DtoWithHooks.php | 27 --- .../Unit/Fixture/InferNormalizerBrokenDto.php | 13 - .../Metadata/AttributeMetadataFactoryTest.php | 75 +----- tests/Unit/MetadataHydratorTest.php | 168 ++++--------- tests/Unit/Middleware/StackTest.php | 34 +++ 27 files changed, 447 insertions(+), 974 deletions(-) delete mode 100644 src/Attribute/PostHydrate.php delete mode 100644 src/Attribute/PreExtract.php create mode 100644 src/Cryptography/CryptographyMiddleware.php delete mode 100644 src/Cryptography/CryptographySubscriber.php delete mode 100644 src/Event/PostExtract.php delete mode 100644 src/Event/PreHydrate.php delete mode 100644 src/Metadata/CallbackMetadata.php create mode 100644 src/Middleware/Middleware.php create mode 100644 src/Middleware/NoMoreMiddleware.php create mode 100644 src/Middleware/Stack.php create mode 100644 src/Middleware/TransformMiddleware.php delete mode 100644 src/NormalizationMissing.php delete mode 100644 tests/Unit/Cryptography/CryptographySubscriberTest.php delete mode 100644 tests/Unit/Fixture/DtoWithHooks.php delete mode 100644 tests/Unit/Fixture/InferNormalizerBrokenDto.php create mode 100644 tests/Unit/Middleware/StackTest.php diff --git a/composer.json b/composer.json index b33c98f..cbac8d8 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "ext-openssl": "*", "psr/cache": "^2.0.0 || ^3.0.0", "psr/simple-cache": "^2.0.0 || ^3.0.0", - "symfony/event-dispatcher": "^5.4.29 || ^6.4.0 || ^7.0.0 || ^8.0.0", "symfony/type-info": "^7.3.0 || ^8.0.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 64d7e6f..0517e08 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "553b270b52c81e6c81d63d72ad996a9c", + "content-hash": "3dd407f20824a4ede9ccfac938bf2643", "packages": [ { "name": "psr/cache", @@ -108,56 +108,6 @@ }, "time": "2021-11-05T16:47:00+00:00" }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, { "name": "psr/simple-cache", "version": "3.0.0", @@ -209,167 +159,6 @@ }, "time": "2021-10-29T13:26:27+00:00" }, - { - "name": "symfony/event-dispatcher", - "version": "v8.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "573f95783a2ec6e38752979db139f09fec033f03" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", - "reference": "573f95783a2ec6e38752979db139f09fec033f03", - "shasum": "" - }, - "require": { - "php": ">=8.4", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/security-http": "<7.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/error-handler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^7.4|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-10-30T14:17:19+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" - }, { "name": "symfony/type-info", "version": "v8.0.0", @@ -1361,16 +1150,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.1", + "version": "6.6.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/3c25fe750c1599716ef26aa997f7c026cee8c4b7", + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7", "shasum": "" }, "require": { @@ -1430,9 +1219,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.2" }, - "time": "2025-11-07T18:30:29+00:00" + "time": "2025-11-28T15:24:03+00:00" }, { "name": "marc-mabe/php-enum", @@ -5639,5 +5428,5 @@ "ext-openssl": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Attribute/PostHydrate.php b/src/Attribute/PostHydrate.php deleted file mode 100644 index c7a8f62..0000000 --- a/src/Attribute/PostHydrate.php +++ /dev/null @@ -1,12 +0,0 @@ -className(), - $metadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName(), - $property->propertyName(), + $metadata->className, + $metadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName, + $property->propertyName, $subjectIdIdentifier, ); } @@ -53,7 +53,7 @@ public function metadata(string $class): ClassMetadata } if ($isSubjectId) { - throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName()); + throw new SubjectIdAndSensitiveDataConflict($metadata->className, $property->propertyName); } $property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo; diff --git a/src/Cryptography/CryptographyMiddleware.php b/src/Cryptography/CryptographyMiddleware.php new file mode 100644 index 0000000..93f4904 --- /dev/null +++ b/src/Cryptography/CryptographyMiddleware.php @@ -0,0 +1,50 @@ + $metadata + * @param array $data + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + { + return $stack->next()->hydrate( + $metadata, + $this->cryptography->decrypt($metadata, $data), + $stack, + ); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + { + return $this->cryptography->encrypt( + $metadata, + $stack->next()->extract($metadata, $object, $stack), + ); + } +} diff --git a/src/Cryptography/CryptographySubscriber.php b/src/Cryptography/CryptographySubscriber.php deleted file mode 100644 index 23e5f38..0000000 --- a/src/Cryptography/CryptographySubscriber.php +++ /dev/null @@ -1,36 +0,0 @@ -data = $this->cryptography->decrypt($event->metadata, $event->data); - } - - public function postExtract(PostExtract $event): void - { - $event->data = $this->cryptography->encrypt($event->metadata, $event->data); - } - - /** @return array> */ - public static function getSubscribedEvents(): array - { - return [ - PreHydrate::class => 'preHydrate', - PostExtract::class => 'postExtract', - ]; - } -} diff --git a/src/Cryptography/SensitiveDataPayloadCryptographer.php b/src/Cryptography/SensitiveDataPayloadCryptographer.php index 7c56088..6470c2c 100644 --- a/src/Cryptography/SensitiveDataPayloadCryptographer.php +++ b/src/Cryptography/SensitiveDataPayloadCryptographer.php @@ -57,7 +57,7 @@ public function encrypt(ClassMetadata $metadata, array $data): array $subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null; if ($subjectId === null) { - throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName); + throw new MissingSubjectId($metadata->className, $sensitiveDataInfo->subjectIdName); } try { @@ -111,7 +111,7 @@ public function decrypt(ClassMetadata $metadata, array $data): array $subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null; if ($subjectId === null) { - throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName); + throw new MissingSubjectId($metadata->className, $sensitiveDataInfo->subjectIdName); } try { @@ -158,7 +158,7 @@ private function getSubjectIds(ClassMetadata $metadata, SubjectIdFieldMapping $m foreach ($mapping->nameToField as $name => $fieldName) { if (!array_key_exists($fieldName, $data)) { - throw new MissingSubjectId($metadata->className(), $fieldName); + throw new MissingSubjectId($metadata->className, $fieldName); } $subjectId = $data[$fieldName]; @@ -172,7 +172,7 @@ private function getSubjectIds(ClassMetadata $metadata, SubjectIdFieldMapping $m } if (!is_string($subjectId)) { - throw new UnsupportedSubjectId($metadata->className(), $fieldName, $subjectId); + throw new UnsupportedSubjectId($metadata->className, $fieldName, $subjectId); } $result[$name] = $subjectId; diff --git a/src/Event/PostExtract.php b/src/Event/PostExtract.php deleted file mode 100644 index 55a45b9..0000000 --- a/src/Event/PostExtract.php +++ /dev/null @@ -1,17 +0,0 @@ - $data */ - public function __construct( - public array $data, - public readonly ClassMetadata $metadata, - ) { - } -} diff --git a/src/Event/PreHydrate.php b/src/Event/PreHydrate.php deleted file mode 100644 index f6f86b6..0000000 --- a/src/Event/PreHydrate.php +++ /dev/null @@ -1,17 +0,0 @@ - $data */ - public function __construct( - public array $data, - public readonly ClassMetadata $metadata, - ) { - } -} diff --git a/src/Metadata/AttributeMetadataFactory.php b/src/Metadata/AttributeMetadataFactory.php index eb4b1e2..36ba797 100644 --- a/src/Metadata/AttributeMetadataFactory.php +++ b/src/Metadata/AttributeMetadataFactory.php @@ -7,8 +7,6 @@ use Patchlevel\Hydrator\Attribute\Ignore; use Patchlevel\Hydrator\Attribute\Lazy; use Patchlevel\Hydrator\Attribute\NormalizedName; -use Patchlevel\Hydrator\Attribute\PostHydrate; -use Patchlevel\Hydrator\Attribute\PreExtract; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; use Patchlevel\Hydrator\Guesser\Guesser; use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; @@ -76,8 +74,6 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada $metadata = new ClassMetadata( $reflectionClass, $this->getPropertyMetadataList($reflectionClass), - $this->getPostHydrateCallbacks($reflectionClass), - $this->getPreExtractCallbacks($reflectionClass), $this->getLazy($reflectionClass), ); @@ -121,7 +117,7 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra throw DuplicatedFieldNameInMetadata::inClass( $fieldName, $reflectionClass->getName(), - $properties[$fieldName]->propertyName(), + $properties[$fieldName]->propertyName, $reflectionProperty->getName(), ); } @@ -136,58 +132,6 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra return array_values($properties); } - /** - * @param ReflectionClass $reflection - * - * @return list - */ - private function getPostHydrateCallbacks(ReflectionClass $reflection): array - { - $methods = []; - - foreach ($reflection->getMethods() as $reflectionMethod) { - if ($reflectionMethod->isStatic()) { - continue; - } - - $attributeReflectionList = $reflectionMethod->getAttributes(PostHydrate::class); - - if ($attributeReflectionList === []) { - continue; - } - - $methods[] = new CallbackMetadata($reflectionMethod); - } - - return $methods; - } - - /** - * @param ReflectionClass $reflection - * - * @return list - */ - private function getPreExtractCallbacks(ReflectionClass $reflection): array - { - $methods = []; - - foreach ($reflection->getMethods() as $reflectionMethod) { - if ($reflectionMethod->isStatic()) { - continue; - } - - $attributeReflectionList = $reflectionMethod->getAttributes(PreExtract::class); - - if ($attributeReflectionList === []) { - continue; - } - - $methods[] = new CallbackMetadata($reflectionMethod); - } - - return $methods; - } - /** @param ReflectionClass $reflection */ private function getLazy(ReflectionClass $reflection): bool|null { @@ -235,8 +179,8 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla if (array_key_exists($property->fieldName, $properties)) { throw DuplicatedFieldNameInMetadata::byInheritance( $property->fieldName, - $parent->className(), - $child->className(), + $parent->className, + $child->className, ); } @@ -246,8 +190,6 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla return new ClassMetadata( $parent->reflection, array_values($properties), - array_merge($parent->postHydrateCallbacks, $child->postHydrateCallbacks), - array_merge($parent->preExtractCallbacks, $child->preExtractCallbacks), $child->lazy ?? $parent->lazy, array_merge($parent->extras, $child->extras), ); diff --git a/src/Metadata/CallbackMetadata.php b/src/Metadata/CallbackMetadata.php deleted file mode 100644 index cd0e29e..0000000 --- a/src/Metadata/CallbackMetadata.php +++ /dev/null @@ -1,51 +0,0 @@ -reflection; - } - - public function methodName(): string - { - return $this->reflection->getName(); - } - - public function invoke(object $object): void - { - $this->reflection->invoke($object); - } - - /** @return serialized */ - public function __serialize(): array - { - return [ - 'className' => $this->reflection->getDeclaringClass()->getName(), - 'method' => $this->reflection->getName(), - ]; - } - - /** @param serialized $data */ - public function __unserialize(array $data): void - { - $this->reflection = new ReflectionMethod($data['className'], $data['method']); - } -} diff --git a/src/Metadata/ClassMetadata.php b/src/Metadata/ClassMetadata.php index 0d8ee33..cc2c819 100644 --- a/src/Metadata/ClassMetadata.php +++ b/src/Metadata/ClassMetadata.php @@ -10,8 +10,6 @@ * @psalm-type serialized array{ * className: class-string, * properties: list, - * postHydrateCallbacks: list, - * preExtractCallbacks: list, * lazy: bool|null, * extras: array, * } @@ -19,27 +17,21 @@ */ final class ClassMetadata { + /** @var class-string */ + public readonly string $className; + /** * @param ReflectionClass $reflection * @param list $properties - * @param list $postHydrateCallbacks - * @param list $preExtractCallbacks * @param array $extras */ public function __construct( public readonly ReflectionClass $reflection, public readonly array $properties = [], - public readonly array $postHydrateCallbacks = [], - public readonly array $preExtractCallbacks = [], public readonly bool|null $lazy = null, public array $extras = [], ) { - } - - /** @return class-string */ - public function className(): string - { - return $this->reflection->getName(); + $this->className = $reflection->getName(); } public function propertyForField(string $name): PropertyMetadata @@ -63,10 +55,8 @@ public function newInstance(): object public function __serialize(): array { return [ - 'className' => $this->reflection->getName(), + 'className' => $this->className, 'properties' => $this->properties, - 'postHydrateCallbacks' => $this->postHydrateCallbacks, - 'preExtractCallbacks' => $this->preExtractCallbacks, 'lazy' => $this->lazy, 'extras' => $this->extras, ]; @@ -77,8 +67,6 @@ public function __unserialize(array $data): void { $this->reflection = new ReflectionClass($data['className']); $this->properties = $data['properties']; - $this->postHydrateCallbacks = $data['postHydrateCallbacks']; - $this->preExtractCallbacks = $data['preExtractCallbacks']; $this->lazy = $data['lazy']; $this->extras = $data['extras']; } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index dfd611f..2bf6be1 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -10,7 +10,7 @@ /** * @psalm-type serialized = array{ * className: class-string, - * property: string, + * propertyName: string, * fieldName: string, * normalizer: Normalizer|null, * extras: array, @@ -18,6 +18,8 @@ */ final class PropertyMetadata { + public readonly string $propertyName; + /** @param array $extras */ public function __construct( public readonly ReflectionProperty $reflection, @@ -25,11 +27,7 @@ public function __construct( public readonly Normalizer|null $normalizer = null, public array $extras = [], ) { - } - - public function propertyName(): string - { - return $this->reflection->getName(); + $this->propertyName = $reflection->getName(); } public function setValue(object $object, mixed $value): void @@ -47,7 +45,7 @@ public function __serialize(): array { return [ 'className' => $this->reflection->getDeclaringClass()->getName(), - 'property' => $this->reflection->getName(), + 'propertyName' => $this->propertyName, 'fieldName' => $this->fieldName, 'normalizer' => $this->normalizer, 'extras' => $this->extras, @@ -57,7 +55,8 @@ public function __serialize(): array /** @param serialized $data */ public function __unserialize(array $data): void { - $this->reflection = new ReflectionProperty($data['className'], $data['property']); + $this->reflection = new ReflectionProperty($data['className'], $data['propertyName']); + $this->propertyName = $data['propertyName']; $this->fieldName = $data['fieldName']; $this->normalizer = $data['normalizer']; $this->extras = $data['extras']; diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index 0352669..636ee6d 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -4,10 +4,6 @@ namespace Patchlevel\Hydrator; -use Patchlevel\Hydrator\Cryptography\CryptographySubscriber; -use Patchlevel\Hydrator\Cryptography\PayloadCryptographer; -use Patchlevel\Hydrator\Event\PostExtract; -use Patchlevel\Hydrator\Event\PreHydrate; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; use Patchlevel\Hydrator\Guesser\ChainGuesser; use Patchlevel\Hydrator\Guesser\Guesser; @@ -15,46 +11,27 @@ use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Metadata\ClassNotFound; 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 ReflectionParameter; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Throwable; -use TypeError; use function array_key_exists; -use function array_values; -use function is_object; -use function spl_object_id; use const PHP_VERSION_ID; final class MetadataHydrator implements Hydrator { - /** @var array */ - private array $stack = []; - /** @var array */ private array $classMetadata = []; + /** @param list $middlewares */ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), - PayloadCryptographer|null $cryptographer = null, - private EventDispatcherInterface|null $eventDispatcher = null, + private readonly array $middlewares = [], private readonly bool $defaultLazy = false, ) { - if (!$cryptographer) { - return; - } - - if (!$this->eventDispatcher) { - $this->eventDispatcher = new EventDispatcher(); - } - - $this->eventDispatcher->addSubscriber( - new CryptographySubscriber($cryptographer), - ); } /** @@ -74,186 +51,35 @@ public function hydrate(string $class, array $data): object } if (PHP_VERSION_ID < 80400) { - return $this->doHydrate($metadata, $data); + $stack = new Stack($this->middlewares); + + return $stack->next()->hydrate($metadata, $data, $stack); } $lazy = $metadata->lazy ?? $this->defaultLazy; if (!$lazy) { - return $this->doHydrate($metadata, $data); + $stack = new Stack($this->middlewares); + + return $stack->next()->hydrate($metadata, $data, $stack); } return (new ReflectionClass($class))->newLazyProxy( function () use ($metadata, $data): object { - return $this->doHydrate($metadata, $data); + $stack = new Stack($this->middlewares); + + return $stack->next()->hydrate($metadata, $data, $stack); }, ); } - /** - * @param ClassMetadata $metadata - * @param array $data - * - * @return T - * - * @template T of object - */ - private function doHydrate(ClassMetadata $metadata, array $data): object - { - if ($this->eventDispatcher) { - $data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data; - } - - $object = $metadata->newInstance(); - - $constructorParameters = null; - - foreach ($metadata->properties as $propertyMetadata) { - if (!array_key_exists($propertyMetadata->fieldName, $data)) { - if (!$propertyMetadata->reflection->isPromoted()) { - continue; - } - - if ($constructorParameters === null) { - $constructorParameters = $this->promotedConstructorParametersWithDefaultValue($metadata); - } - - if (!array_key_exists($propertyMetadata->propertyName(), $constructorParameters)) { - continue; - } - - /** @psalm-suppress MixedAssignment */ - $defaultValue = $constructorParameters[$propertyMetadata->propertyName()]->getDefaultValue(); - $propertyMetadata->setValue($object, $defaultValue); - - continue; - } - - /** @psalm-suppress MixedAssignment */ - $value = $data[$propertyMetadata->fieldName]; - - $normalizer = $propertyMetadata->normalizer; - - if ($normalizer) { - try { - /** @psalm-suppress MixedAssignment */ - $value = $normalizer->denormalize($value); - } catch (Throwable $e) { - throw new DenormalizationFailure( - $metadata->className(), - $propertyMetadata->propertyName(), - $normalizer::class, - $e, - ); - } - } - - try { - $propertyMetadata->setValue($object, $value); - } catch (TypeError $e) { - throw new TypeMismatch( - $metadata->className(), - $propertyMetadata->propertyName(), - $e, - ); - } - } - - foreach ($metadata->postHydrateCallbacks as $callback) { - $callback->invoke($object); - } - - return $object; - } - /** @return array */ public function extract(object $object): array { - $objectId = spl_object_id($object); - - if (array_key_exists($objectId, $this->stack)) { - $references = array_values($this->stack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->stack[$objectId] = $object::class; - - try { - $metadata = $this->metadata($object::class); - - foreach ($metadata->preExtractCallbacks as $callback) { - $callback->invoke($object); - } - - $data = []; - - foreach ($metadata->properties as $propertyMetadata) { - /** @psalm-suppress MixedAssignment */ - $value = $propertyMetadata->getValue($object); - - $normalizer = $propertyMetadata->normalizer; - - if ($normalizer) { - try { - /** @psalm-suppress MixedAssignment */ - $value = $normalizer->normalize($value); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure( - $object::class, - $propertyMetadata->propertyName(), - $normalizer::class, - $e, - ); - } - } - - if (is_object($value)) { - throw new NormalizationMissing($object::class, $propertyMetadata->propertyName()); - } - - /** @psalm-suppress MixedAssignment */ - $data[$propertyMetadata->fieldName] = $value; - } - - if ($this->eventDispatcher) { - return $this->eventDispatcher->dispatch(new PostExtract($data, $metadata))->data; - } + $metadata = $this->metadata($object::class); + $stack = new Stack($this->middlewares); - return $data; - } finally { - unset($this->stack[$objectId]); - } - } - - /** @return array */ - private function promotedConstructorParametersWithDefaultValue(ClassMetadata $metadata): array - { - $constructor = $metadata->reflection->getConstructor(); - - if (!$constructor) { - return []; - } - - $parameters = $constructor->getParameters(); - $result = []; - - foreach ($parameters as $parameter) { - if (!$parameter->isPromoted()) { - continue; - } - - if (!$parameter->isDefaultValueAvailable()) { - continue; - } - - $result[$parameter->getName()] = $parameter; - } - - return $result; + return $stack->next()->extract($metadata, $object, $stack); } /** @@ -282,10 +108,13 @@ private function metadata(string $class): ClassMetadata return $metadata; } - /** @param iterable $guessers */ + /** + * @param list $additionalMiddleware + * @param iterable $guessers + */ public static function create( + array $additionalMiddleware = [], iterable $guessers = [], - EventDispatcherInterface|null $eventDispatcher = null, bool $defaultLazy = false, ): self { $guesser = new BuiltInGuesser(); @@ -301,8 +130,7 @@ public static function create( new AttributeMetadataFactory( guesser: $guesser, ), - null, - $eventDispatcher, + [...$additionalMiddleware, new TransformMiddleware()], $defaultLazy, ); } diff --git a/src/Middleware/Middleware.php b/src/Middleware/Middleware.php new file mode 100644 index 0000000..33fa3e1 --- /dev/null +++ b/src/Middleware/Middleware.php @@ -0,0 +1,30 @@ + $metadata + * @param array $data + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object; + + /** + * @param ClassMetadata $metadata + * @param T $object + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, Stack $stack): array; +} diff --git a/src/Middleware/NoMoreMiddleware.php b/src/Middleware/NoMoreMiddleware.php new file mode 100644 index 0000000..8203ab3 --- /dev/null +++ b/src/Middleware/NoMoreMiddleware.php @@ -0,0 +1,16 @@ + $middlewares */ + public function __construct( + private readonly array $middlewares, + ) { + } + + public function next(): Middleware + { + $next = $this->middlewares[$this->index] ?? null; + + if ($next === null) { + throw new NoMoreMiddleware(); + } + + $this->index++; + + return $next; + } +} diff --git a/src/Middleware/TransformMiddleware.php b/src/Middleware/TransformMiddleware.php new file mode 100644 index 0000000..c822698 --- /dev/null +++ b/src/Middleware/TransformMiddleware.php @@ -0,0 +1,164 @@ + */ + private array $callStack = []; + + /** + * @param ClassMetadata $metadata + * @param array $data + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + { + $object = $metadata->newInstance(); + + $constructorParameters = null; + + foreach ($metadata->properties as $propertyMetadata) { + if (!array_key_exists($propertyMetadata->fieldName, $data)) { + if (!$propertyMetadata->reflection->isPromoted()) { + continue; + } + + if ($constructorParameters === null) { + $constructorParameters = $this->promotedConstructorParametersWithDefaultValue($metadata); + } + + if (!array_key_exists($propertyMetadata->propertyName, $constructorParameters)) { + continue; + } + + $propertyMetadata->setValue( + $object, + $constructorParameters[$propertyMetadata->propertyName]->getDefaultValue(), + ); + + continue; + } + + $normalizer = $propertyMetadata->normalizer; + + if ($normalizer) { + try { + /** @psalm-suppress MixedAssignment */ + $value = $normalizer->denormalize($data[$propertyMetadata->fieldName]); + } catch (Throwable $e) { + throw new DenormalizationFailure( + $metadata->className, + $propertyMetadata->propertyName, + $normalizer::class, + $e, + ); + } + } else { + $value = $data[$propertyMetadata->fieldName]; + } + + try { + $propertyMetadata->setValue($object, $value); + } catch (TypeError $e) { + throw new TypeMismatch( + $metadata->className, + $propertyMetadata->propertyName, + $e, + ); + } + } + + return $object; + } + + /** @return array */ + public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + { + $objectId = spl_object_id($object); + + if (array_key_exists($objectId, $this->callStack)) { + $references = array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$objectId] = $object::class; + + try { + $data = []; + + foreach ($metadata->properties as $propertyMetadata) { + if ($propertyMetadata->normalizer) { + try { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + ); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure( + $object::class, + $propertyMetadata->propertyName, + $propertyMetadata->normalizer::class, + $e, + ); + } + } else { + $data[$propertyMetadata->fieldName] = $propertyMetadata->getValue($object); + } + } + } finally { + unset($this->callStack[$objectId]); + } + + return $data; + } + + /** @return array */ + private function promotedConstructorParametersWithDefaultValue(ClassMetadata $metadata): array + { + $constructor = $metadata->reflection->getConstructor(); + + if (!$constructor) { + return []; + } + + $parameters = $constructor->getParameters(); + $result = []; + + foreach ($parameters as $parameter) { + if (!$parameter->isPromoted()) { + continue; + } + + if (!$parameter->isDefaultValueAvailable()) { + continue; + } + + $result[$parameter->getName()] = $parameter; + } + + return $result; + } +} diff --git a/src/NormalizationMissing.php b/src/NormalizationMissing.php deleted file mode 100644 index d618418..0000000 --- a/src/NormalizationMissing.php +++ /dev/null @@ -1,24 +0,0 @@ -store = new InMemoryCipherKeyStore(); - $eventDispatcher = new EventDispatcher(); - $eventDispatcher->addSubscriber(new CryptographySubscriber( - SensitiveDataPayloadCryptographer::createWithDefaultSettings($this->store), - )); - $this->hydrator = new MetadataHydrator( - metadataFactory: new CryptographyMetadataFactory(new AttributeMetadataFactory()), - eventDispatcher: $eventDispatcher, + new CryptographyMetadataFactory(new AttributeMetadataFactory()), + [ + new CryptographyMiddleware(SensitiveDataPayloadCryptographer::createWithDefaultSettings($this->store)), + new TransformMiddleware(), + ], ); } diff --git a/tests/Unit/Cryptography/CryptographySubscriberTest.php b/tests/Unit/Cryptography/CryptographySubscriberTest.php deleted file mode 100644 index a8c6e03..0000000 --- a/tests/Unit/Cryptography/CryptographySubscriberTest.php +++ /dev/null @@ -1,67 +0,0 @@ - 'preHydrate', - PostExtract::class => 'postExtract', - ], CryptographySubscriber::getSubscribedEvents()); - } - - public function testPreHydrate(): void - { - $metadata = new ClassMetadata( - new ReflectionClass(stdClass::class), - ); - - $event = new PreHydrate( - ['foo' => 'bar'], - $metadata, - ); - - $cryptographer = $this->createMock(PayloadCryptographer::class); - $cryptographer->expects($this->once())->method('decrypt')->with($metadata, ['foo' => 'bar'])->willReturn(['foo' => 'baz']); - - $subscriber = new CryptographySubscriber($cryptographer); - $subscriber->preHydrate($event); - - self::assertEquals(['foo' => 'baz'], $event->data); - } - - public function testPostExtract(): void - { - $metadata = new ClassMetadata( - new ReflectionClass(stdClass::class), - ); - - $event = new PostExtract( - ['foo' => 'bar'], - $metadata, - ); - - $cryptographer = $this->createMock(PayloadCryptographer::class); - $cryptographer->expects($this->once())->method('encrypt')->with($metadata, ['foo' => 'bar'])->willReturn(['foo' => 'baz']); - - $subscriber = new CryptographySubscriber($cryptographer); - $subscriber->postExtract($event); - - self::assertEquals(['foo' => 'baz'], $event->data); - } -} diff --git a/tests/Unit/Fixture/DtoWithHooks.php b/tests/Unit/Fixture/DtoWithHooks.php deleted file mode 100644 index f5ec909..0000000 --- a/tests/Unit/Fixture/DtoWithHooks.php +++ /dev/null @@ -1,27 +0,0 @@ -postHydrateCalled = true; - } - - #[PreExtract] - private function preExtract(): void - { - $this->preExtractCalled = true; - } -} diff --git a/tests/Unit/Fixture/InferNormalizerBrokenDto.php b/tests/Unit/Fixture/InferNormalizerBrokenDto.php deleted file mode 100644 index baf63a0..0000000 --- a/tests/Unit/Fixture/InferNormalizerBrokenDto.php +++ /dev/null @@ -1,13 +0,0 @@ -metadata($object::class); self::assertCount(0, $metadata->properties); - self::assertCount(0, $metadata->preExtractCallbacks); - self::assertCount(0, $metadata->postHydrateCallbacks); } public function testNotFoundProperty(): void @@ -72,7 +68,7 @@ public function testWithProperties(): void $propertyMetadata = $metadata->propertyForField('name'); - self::assertSame('name', $propertyMetadata->propertyName()); + self::assertSame('name', $propertyMetadata->propertyName); self::assertSame('name', $propertyMetadata->fieldName); self::assertNull($propertyMetadata->normalizer); } @@ -117,7 +113,7 @@ public function __construct( $propertyMetadata = $metadata->propertyForField('name'); - self::assertSame('name', $propertyMetadata->propertyName()); + self::assertSame('name', $propertyMetadata->propertyName); self::assertSame('name', $propertyMetadata->fieldName); self::assertNull($propertyMetadata->normalizer); } @@ -141,7 +137,7 @@ public function __construct( $propertyMetadata = $metadata->propertyForField('username'); - self::assertSame('name', $propertyMetadata->propertyName()); + self::assertSame('name', $propertyMetadata->propertyName); self::assertSame('username', $propertyMetadata->fieldName); self::assertNull($propertyMetadata->normalizer); } @@ -165,7 +161,7 @@ public function __construct( $propertyMetadata = $metadata->propertyForField('email'); - self::assertSame('email', $propertyMetadata->propertyName()); + self::assertSame('email', $propertyMetadata->propertyName); self::assertSame('email', $propertyMetadata->fieldName); self::assertInstanceOf(EmailNormalizer::class, $propertyMetadata->normalizer); } @@ -189,7 +185,7 @@ public function __construct( $propertyMetadata = $metadata->propertyForField('status'); - self::assertSame('status', $propertyMetadata->propertyName()); + self::assertSame('status', $propertyMetadata->propertyName); self::assertSame('status', $propertyMetadata->fieldName); $normalizer = $propertyMetadata->normalizer; @@ -256,13 +252,13 @@ public function testExtends(): void $emailPropertyMetadata = $metadata->propertyForField('profileId'); - self::assertSame('profileId', $emailPropertyMetadata->propertyName()); + self::assertSame('profileId', $emailPropertyMetadata->propertyName); self::assertSame('profileId', $emailPropertyMetadata->fieldName); self::assertInstanceOf(IdNormalizer::class, $emailPropertyMetadata->normalizer); $emailPropertyMetadata = $metadata->propertyForField('email'); - self::assertSame('email', $emailPropertyMetadata->propertyName()); + self::assertSame('email', $emailPropertyMetadata->propertyName); self::assertSame('email', $emailPropertyMetadata->fieldName); self::assertInstanceOf(EmailNormalizer::class, $emailPropertyMetadata->normalizer); } @@ -284,7 +280,7 @@ public function testBug70(): void $property = $metadata->propertyForField('recordedDate'); - self::assertSame('recordedDate', $property->propertyName()); + self::assertSame('recordedDate', $property->propertyName); } public function testSameClassDuplicatedFieldName(): void @@ -304,13 +300,13 @@ public function testExtendsWithIgnore(): void $emailPropertyMetadata = $metadata->propertyForField('profileId'); - self::assertSame('profileId', $emailPropertyMetadata->propertyName()); + self::assertSame('profileId', $emailPropertyMetadata->propertyName); self::assertSame('profileId', $emailPropertyMetadata->fieldName); self::assertInstanceOf(IdNormalizer::class, $emailPropertyMetadata->normalizer); $emailPropertyMetadata = $metadata->propertyForField('email'); - self::assertSame('email', $emailPropertyMetadata->propertyName()); + self::assertSame('email', $emailPropertyMetadata->propertyName); self::assertSame('email', $emailPropertyMetadata->fieldName); self::assertInstanceOf(EmailNormalizer::class, $emailPropertyMetadata->normalizer); } @@ -324,7 +320,7 @@ public function testIgnore(): void $emailPropertyMetadata = $metadata->propertyForField('profileId'); - self::assertSame('profileId', $emailPropertyMetadata->propertyName()); + self::assertSame('profileId', $emailPropertyMetadata->propertyName); self::assertSame('profileId', $emailPropertyMetadata->fieldName); self::assertInstanceOf(IdNormalizer::class, $emailPropertyMetadata->normalizer); } @@ -339,55 +335,6 @@ public function testIgnoreNotFoundProperty(): void $metadata->propertyForField('email'); } - public function testHooks(): void - { - $object = new class { - #[PreExtract] - private function preExtract(): void - { - } - - #[PostHydrate] - private function postHydrate(): void - { - } - }; - - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata($object::class); - - $preExtract = $metadata->preExtractCallbacks; - - self::assertCount(1, $preExtract); - self::assertSame('preExtract', $preExtract[0]->methodName()); - - $postHydrate = $metadata->postHydrateCallbacks; - - self::assertCount(1, $postHydrate); - self::assertSame('postHydrate', $postHydrate[0]->methodName()); - } - - public function testSkipStaticHook(): void - { - $object = new class { - #[PreExtract] - private static function preExtract(): void - { - } - - #[PostHydrate] - private static function postHydrate(): void - { - } - }; - - $metadataFactory = new AttributeMetadataFactory(); - $metadata = $metadataFactory->metadata($object::class); - - self::assertCount(0, $metadata->preExtractCallbacks); - self::assertCount(0, $metadata->postHydrateCallbacks); - } - public function testNoLazy(): void { $object = new class { diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index 46ce134..51e4acc 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -9,23 +9,23 @@ use DateTimeZone; use Patchlevel\Hydrator\CircularReference; use Patchlevel\Hydrator\ClassNotSupported; +use Patchlevel\Hydrator\Cryptography\CryptographyMiddleware; use Patchlevel\Hydrator\Cryptography\PayloadCryptographer; use Patchlevel\Hydrator\DenormalizationFailure; -use Patchlevel\Hydrator\Event\PostExtract; -use Patchlevel\Hydrator\Event\PreHydrate; use Patchlevel\Hydrator\Guesser\Guesser; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; +use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\MetadataHydrator; +use Patchlevel\Hydrator\Middleware\Middleware; +use Patchlevel\Hydrator\Middleware\Stack; +use Patchlevel\Hydrator\Middleware\TransformMiddleware; use Patchlevel\Hydrator\NormalizationFailure; -use Patchlevel\Hydrator\NormalizationMissing; use Patchlevel\Hydrator\Normalizer\Normalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto; -use Patchlevel\Hydrator\Tests\Unit\Fixture\DtoWithHooks; use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; -use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerBrokenDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto; @@ -54,7 +54,7 @@ final class MetadataHydratorTest extends TestCase public function setUp(): void { - $this->hydrator = new MetadataHydrator(new AttributeMetadataFactory()); + $this->hydrator = MetadataHydrator::create(); } public function testExtract(): void @@ -136,26 +136,6 @@ public function testExtractWithInferNormalizer2(): void ); } - public function testExtractWithInferNormalizerFailed(): void - { - $this->expectException(NormalizationMissing::class); - $this->hydrator->extract( - new InferNormalizerBrokenDto( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('info@patchlevel.de'), - ), - ), - ); - } - - public function testExtractWithHooks(): void - { - $data = $this->hydrator->extract(new DtoWithHooks()); - - self::assertEquals(['postHydrateCalled' => false, 'preExtractCalled' => true], $data); - } - public function testHydrate(): void { $expected = new ProfileCreated( @@ -276,7 +256,13 @@ public function testDecrypt(): void ->with($metadataFactory->metadata(ProfileCreated::class), $encryptedPayload) ->willReturn($payload); - $hydrator = new MetadataHydrator($metadataFactory, $cryptographer); + $hydrator = new MetadataHydrator( + $metadataFactory, + [ + new CryptographyMiddleware($cryptographer), + new TransformMiddleware(), + ], + ); $return = $hydrator->hydrate(ProfileCreated::class, $encryptedPayload); @@ -302,77 +288,14 @@ public function testEncrypt(): void ->with($metadataFactory->metadata(ProfileCreated::class), $payload) ->willReturn($encryptedPayload); - $hydrator = new MetadataHydrator($metadataFactory, $cryptographer); - - $return = $hydrator->extract($object); - - self::assertSame($encryptedPayload, $return); - } - - public function testPreHydrate(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('info@patchlevel.de'), - ); - - $payload = ['profileId' => '1', 'email' => 'info@patchlevel.de']; - $encryptedPayload = ['profileId' => '1', 'email' => 'encrypted']; - - $metadataFactory = new AttributeMetadataFactory(); - - $event = new PreHydrate( - $encryptedPayload, - $metadataFactory->metadata(ProfileCreated::class), - ); - - $eventReturn = new PreHydrate( - $payload, - $metadataFactory->metadata(ProfileCreated::class), - ); - - $eventDispatcher = $this->createMock(EventDispatcherInterface::class); - $eventDispatcher - ->expects($this->once()) - ->method('dispatch') - ->with($event) - ->willReturn($eventReturn); - - $hydrator = new MetadataHydrator($metadataFactory, eventDispatcher: $eventDispatcher); - - $return = $hydrator->hydrate(ProfileCreated::class, $encryptedPayload); - - self::assertEquals($object, $return); - } - - public function testPostExtract(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('info@patchlevel.de'), - ); - - $payload = ['profileId' => '1', 'email' => 'info@patchlevel.de']; - $encryptedPayload = ['profileId' => '1', 'email' => 'encrypted']; - - $metadataFactory = new AttributeMetadataFactory(); - - $event = new PostExtract( - $payload, - $metadataFactory->metadata(ProfileCreated::class), - ); - - $eventReturn = new PostExtract( - $encryptedPayload, - $metadataFactory->metadata(ProfileCreated::class), + $hydrator = new MetadataHydrator( + $metadataFactory, + [ + new CryptographyMiddleware($cryptographer), + new TransformMiddleware(), + ], ); - $eventDispatcher = $this->createMock(EventDispatcherInterface::class); - $eventDispatcher->expects($this->once())->method('dispatch')->with($event) - ->willReturn($eventReturn); - - $hydrator = new MetadataHydrator($metadataFactory, eventDispatcher: $eventDispatcher); - $return = $hydrator->extract($object); self::assertSame($encryptedPayload, $return); @@ -503,25 +426,6 @@ public function testHydrateWithInferNormalizerWitIterables(): void self::assertEquals($expected, $event); } - public function testHydrateWithInferNormalizerFailed(): void - { - $this->expectException(TypeMismatch::class); - $this->hydrator->hydrate( - InferNormalizerBrokenDto::class, - [ - 'profileCreated' => ['profileId' => '1', 'email' => 'info@patchlevel.de'], - ], - ); - } - - public function testHydrateWithHooks(): void - { - $object = $this->hydrator->hydrate(DtoWithHooks::class, ['postHydrateCalled' => false, 'preExtractCalled' => false]); - - self::assertEquals(true, $object->postHydrateCalled); - self::assertEquals(false, $object->preExtractCalled); - } - #[RequiresPhp('>=8.4')] public function testLazyHydrate(): void { @@ -585,7 +489,39 @@ public function guess(ObjectType $type): Normalizer|null } }; - $hydrator = MetadataHydrator::create([$guesser]); + $hydrator = MetadataHydrator::create( + [ + new class implements Middleware + { + /** + * @param ClassMetadata $metadata + * @param array $data + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + { + return $stack->next()->hydrate($metadata, $data, $stack); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + { + return $stack->next()->extract($metadata, $object, $stack); + } + }, + ], + [$guesser], + ); $hydrator->extract(new InferNormalizerDto( Status::Draft, diff --git a/tests/Unit/Middleware/StackTest.php b/tests/Unit/Middleware/StackTest.php new file mode 100644 index 0000000..31f30b2 --- /dev/null +++ b/tests/Unit/Middleware/StackTest.php @@ -0,0 +1,34 @@ +expectException(NoMoreMiddleware::class); + + $stack = new Stack([]); + $stack->next(); + } + + public function testStack(): void + { + $middleware1 = $this->createStub(Middleware::class); + $middleware2 = $this->createStub(Middleware::class); + + $stack = new Stack([$middleware1, $middleware2]); + + self::assertSame($middleware1, $stack->next()); + self::assertSame($middleware2, $stack->next()); + } +} From e98e4d391a1064c8180829ff54c82f5a398f8eb9 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 28 Nov 2025 10:00:11 +0100 Subject: [PATCH 2/3] add tests --- .../CryptographyMiddlewareTest.php | 63 ++++++++++++++++++ .../Middleware/TransformerMiddlewareTest.php | 66 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 tests/Unit/Cryptography/CryptographyMiddlewareTest.php create mode 100644 tests/Unit/Middleware/TransformerMiddlewareTest.php diff --git a/tests/Unit/Cryptography/CryptographyMiddlewareTest.php b/tests/Unit/Cryptography/CryptographyMiddlewareTest.php new file mode 100644 index 0000000..c7b160c --- /dev/null +++ b/tests/Unit/Cryptography/CryptographyMiddlewareTest.php @@ -0,0 +1,63 @@ +createMock(PayloadCryptographer::class); + $payloadCryptographer->expects($this->once())->method('decrypt')->with($metadata, ['name' => 'foo'])->willReturn(['name' => 'bar']); + + $object = new stdClass(); + + $cryptographyMiddleware = new CryptographyMiddleware($payloadCryptographer); + + $otherMiddleware = $this->createMock(Middleware::class); + + $stack = new Stack([$otherMiddleware]); + + $otherMiddleware->expects($this->once())->method('hydrate')->with($metadata, ['name' => 'bar'], $stack)->willReturn($object); + + $result = $cryptographyMiddleware->hydrate($metadata, ['name' => 'foo'], $stack); + + self::assertSame($object, $result); + } + + public function testExtract(): void + { + $metadata = new ClassMetadata(new ReflectionClass(stdClass::class)); + + $payloadCryptographer = $this->createMock(PayloadCryptographer::class); + $payloadCryptographer->expects($this->once())->method('encrypt')->with($metadata, ['name' => 'foo'])->willReturn(['name' => 'bar']); + + $object = new stdClass(); + + $cryptographyMiddleware = new CryptographyMiddleware($payloadCryptographer); + + $otherMiddleware = $this->createMock(Middleware::class); + + $stack = new Stack([$otherMiddleware]); + + $otherMiddleware->expects($this->once())->method('extract')->with($metadata, $object, $stack)->willReturn(['name' => 'foo']); + + $result = $cryptographyMiddleware->extract($metadata, $object, $stack); + + self::assertSame(['name' => 'bar'], $result); + } +} diff --git a/tests/Unit/Middleware/TransformerMiddlewareTest.php b/tests/Unit/Middleware/TransformerMiddlewareTest.php new file mode 100644 index 0000000..288d5fa --- /dev/null +++ b/tests/Unit/Middleware/TransformerMiddlewareTest.php @@ -0,0 +1,66 @@ +hydrate( + $this->classMetadata(ProfileCreated::class), + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + new Stack([]), + ); + + self::assertEquals($expected, $event); + } + + public function testExtract(): void + { + $middleware = new TransformMiddleware(); + + $expected = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + $data = $middleware->extract( + $this->classMetadata(ProfileCreated::class), + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + new Stack([]), + ); + + self::assertEquals($expected, $data); + } + + /** + * @param class-string $class + * + * @return ClassMetadata + * + * @template T of object + */ + private function classMetadata(string $class): ClassMetadata + { + return (new AttributeMetadataFactory()) + ->metadata($class); + } +} From ba350e18464701c2823b14acaff67495beca8d1f Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 28 Nov 2025 18:22:58 +0100 Subject: [PATCH 3/3] add cover class in tests --- phpstan-baseline.neon | 42 ------------------- phpstan.neon.dist | 1 + .../Cipher/CreateCipherKeyFailedTest.php | 3 +- .../Cipher/DecryptionFailedTest.php | 3 +- .../Cipher/EncryptionFailedTest.php | 3 +- .../Cipher/OpensslCipherKeyFactoryTest.php | 3 +- .../Cryptography/Cipher/OpensslCipherTest.php | 3 +- .../Cryptography/MissingSubjectIdTest.php | 3 +- .../SensitiveDataPayloadCryptographerTest.php | 3 +- .../Store/CipherKeyNotExistsTest.php | 3 +- .../Store/InMemoryCipherKeyStoreTest.php | 3 +- .../Cryptography/UnsupportedSubjectIdTest.php | 3 +- tests/Unit/Guesser/BuiltInGuesserTest.php | 2 + tests/Unit/Guesser/ChainGuesserTest.php | 2 + .../Metadata/AttributeMetadataFactoryTest.php | 2 + tests/Unit/MetadataHydratorTest.php | 4 +- .../Middleware/TransformerMiddlewareTest.php | 2 + tests/Unit/Normalizer/ArrayNormalizerTest.php | 4 +- .../Normalizer/ArrayShapeNormalizerTest.php | 4 +- .../DateTimeImmutableNormalizerTest.php | 4 +- .../Normalizer/DateTimeNormalizerTest.php | 4 +- .../Normalizer/DateTimeZoneNormalizerTest.php | 4 +- tests/Unit/Normalizer/EnumNormalizerTest.php | 4 +- .../Unit/Normalizer/ObjectNormalizerTest.php | 4 +- 24 files changed, 46 insertions(+), 67 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5529549..216d2ad 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,12 +66,6 @@ parameters: count: 1 path: src/Metadata/ClassMetadata.php - - - message: '#^Dead catch \- Patchlevel\\Hydrator\\CircularReference is never thrown in the try block\.$#' - identifier: catch.neverThrown - count: 1 - path: src/MetadataHydrator.php - - message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\\|null\) does not accept string\.$#' identifier: assign.propertyType @@ -96,54 +90,18 @@ parameters: count: 1 path: tests/Unit/Fixture/ChildWithSensitiveDataWithIdentifierDto.php - - - message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:postHydrate\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Fixture/DtoWithHooks.php - - - - message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:preExtract\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Fixture/DtoWithHooks.php - - message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\IdNormalizer\:\:\$idClass \(class\-string\\|null\) does not accept string\.$#' identifier: assign.propertyType count: 1 path: tests/Unit/Fixture/IdNormalizer.php - - - message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:344\:\:postHydrate\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php - - - - message: '#^Method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:344\:\:preExtract\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php - - message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:metadata\(\) expects class\-string\, string given\.$#' identifier: argument.type count: 1 path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php - - - message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:372\:\:postHydrate\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php - - - - message: '#^Static method class@anonymous/tests/Unit/Metadata/AttributeMetadataFactoryTest\.php\:372\:\:preExtract\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Unit/Metadata/AttributeMetadataFactoryTest.php - - message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\MetadataHydrator\:\:hydrate\(\) expects class\-string\, string given\.$#' identifier: argument.type diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 12e99fa..b2f33b2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,7 @@ parameters: paths: - src - tests + reportUnmatchedIgnoredErrors: false services: - diff --git a/tests/Unit/Cryptography/Cipher/CreateCipherKeyFailedTest.php b/tests/Unit/Cryptography/Cipher/CreateCipherKeyFailedTest.php index a60910d..77db12a 100644 --- a/tests/Unit/Cryptography/Cipher/CreateCipherKeyFailedTest.php +++ b/tests/Unit/Cryptography/Cipher/CreateCipherKeyFailedTest.php @@ -5,9 +5,10 @@ namespace Patchlevel\Hydrator\Tests\Unit\Cryptography\Cipher; use Patchlevel\Hydrator\Cryptography\Cipher\CreateCipherKeyFailed; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Cipher\CreateCipherKeyFailed */ +#[CoversClass(CreateCipherKeyFailed::class)] final class CreateCipherKeyFailedTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Cryptography/Cipher/DecryptionFailedTest.php b/tests/Unit/Cryptography/Cipher/DecryptionFailedTest.php index 426dced..687f157 100644 --- a/tests/Unit/Cryptography/Cipher/DecryptionFailedTest.php +++ b/tests/Unit/Cryptography/Cipher/DecryptionFailedTest.php @@ -5,9 +5,10 @@ namespace Patchlevel\Hydrator\Tests\Unit\Cryptography\Cipher; use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed */ +#[CoversClass(DecryptionFailed::class)] final class DecryptionFailedTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Cryptography/Cipher/EncryptionFailedTest.php b/tests/Unit/Cryptography/Cipher/EncryptionFailedTest.php index d8553e5..05123a5 100644 --- a/tests/Unit/Cryptography/Cipher/EncryptionFailedTest.php +++ b/tests/Unit/Cryptography/Cipher/EncryptionFailedTest.php @@ -5,9 +5,10 @@ namespace Patchlevel\Hydrator\Tests\Unit\Cryptography\Cipher; use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed */ +#[CoversClass(EncryptionFailed::class)] final class EncryptionFailedTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Cryptography/Cipher/OpensslCipherKeyFactoryTest.php b/tests/Unit/Cryptography/Cipher/OpensslCipherKeyFactoryTest.php index ce86005..a20b422 100644 --- a/tests/Unit/Cryptography/Cipher/OpensslCipherKeyFactoryTest.php +++ b/tests/Unit/Cryptography/Cipher/OpensslCipherKeyFactoryTest.php @@ -6,11 +6,12 @@ use Patchlevel\Hydrator\Cryptography\Cipher\MethodNotSupported; use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use function strlen; -/** @covers \Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory */ +#[CoversClass(OpensslCipherKeyFactory::class)] final class OpensslCipherKeyFactoryTest extends TestCase { public function testCreateKey(): void diff --git a/tests/Unit/Cryptography/Cipher/OpensslCipherTest.php b/tests/Unit/Cryptography/Cipher/OpensslCipherTest.php index cbd629f..9d3c0a7 100644 --- a/tests/Unit/Cryptography/Cipher/OpensslCipherTest.php +++ b/tests/Unit/Cryptography/Cipher/OpensslCipherTest.php @@ -9,10 +9,11 @@ use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed; use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed; use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher */ +#[CoversClass(OpensslCipher::class)] final class OpensslCipherTest extends TestCase { #[DataProvider('dataProvider')] diff --git a/tests/Unit/Cryptography/MissingSubjectIdTest.php b/tests/Unit/Cryptography/MissingSubjectIdTest.php index 479cac6..6d23236 100644 --- a/tests/Unit/Cryptography/MissingSubjectIdTest.php +++ b/tests/Unit/Cryptography/MissingSubjectIdTest.php @@ -6,9 +6,10 @@ use Patchlevel\Hydrator\Cryptography\MissingSubjectId; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\MissingSubjectId */ +#[CoversClass(MissingSubjectId::class)] final class MissingSubjectIdTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php index ab82dcc..b34c6f0 100644 --- a/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php +++ b/tests/Unit/Cryptography/SensitiveDataPayloadCryptographerTest.php @@ -22,9 +22,10 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\SensitiveDataProfileCreatedFallbackCallback; use Patchlevel\Hydrator\Tests\Unit\Fixture\SensitiveDataWithStringableSubjectId; use Patchlevel\Hydrator\Tests\Unit\Fixture\StringableSubjectId; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\SensitiveDataPayloadCryptographer */ +#[CoversClass(SensitiveDataPayloadCryptographer::class)] final class SensitiveDataPayloadCryptographerTest extends TestCase { public function testSkipEncrypt(): void diff --git a/tests/Unit/Cryptography/Store/CipherKeyNotExistsTest.php b/tests/Unit/Cryptography/Store/CipherKeyNotExistsTest.php index 7e3ae57..05019c1 100644 --- a/tests/Unit/Cryptography/Store/CipherKeyNotExistsTest.php +++ b/tests/Unit/Cryptography/Store/CipherKeyNotExistsTest.php @@ -5,9 +5,10 @@ namespace Patchlevel\Hydrator\Tests\Unit\Cryptography\Store; use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists */ +#[CoversClass(CipherKeyNotExists::class)] final class CipherKeyNotExistsTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Cryptography/Store/InMemoryCipherKeyStoreTest.php b/tests/Unit/Cryptography/Store/InMemoryCipherKeyStoreTest.php index 553312c..d34f00f 100644 --- a/tests/Unit/Cryptography/Store/InMemoryCipherKeyStoreTest.php +++ b/tests/Unit/Cryptography/Store/InMemoryCipherKeyStoreTest.php @@ -7,9 +7,10 @@ use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey; use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists; use Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore */ +#[CoversClass(InMemoryCipherKeyStore::class)] final class InMemoryCipherKeyStoreTest extends TestCase { public function testStoreAndLoad(): void diff --git a/tests/Unit/Cryptography/UnsupportedSubjectIdTest.php b/tests/Unit/Cryptography/UnsupportedSubjectIdTest.php index 7c7b7fd..8aa016d 100644 --- a/tests/Unit/Cryptography/UnsupportedSubjectIdTest.php +++ b/tests/Unit/Cryptography/UnsupportedSubjectIdTest.php @@ -5,9 +5,10 @@ namespace Patchlevel\Hydrator\Tests\Unit\Cryptography; use Patchlevel\Hydrator\Cryptography\UnsupportedSubjectId; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -/** @covers \Patchlevel\Hydrator\Cryptography\UnsupportedSubjectId */ +#[CoversClass(UnsupportedSubjectId::class)] final class UnsupportedSubjectIdTest extends TestCase { public function testCreation(): void diff --git a/tests/Unit/Guesser/BuiltInGuesserTest.php b/tests/Unit/Guesser/BuiltInGuesserTest.php index 48b4c9e..9a0019c 100644 --- a/tests/Unit/Guesser/BuiltInGuesserTest.php +++ b/tests/Unit/Guesser/BuiltInGuesserTest.php @@ -15,9 +15,11 @@ use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Type; +#[CoversClass(BuiltInGuesser::class)] final class BuiltInGuesserTest extends TestCase { public function testNoMatch(): void diff --git a/tests/Unit/Guesser/ChainGuesserTest.php b/tests/Unit/Guesser/ChainGuesserTest.php index cb85686..1a0db8b 100644 --- a/tests/Unit/Guesser/ChainGuesserTest.php +++ b/tests/Unit/Guesser/ChainGuesserTest.php @@ -8,9 +8,11 @@ use Patchlevel\Hydrator\Guesser\ChainGuesser; use Patchlevel\Hydrator\Guesser\Guesser; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Type; +#[CoversClass(ChainGuesser::class)] final class ChainGuesserTest extends TestCase { public function testGuessReturnsFirstNonNullResult(): void diff --git a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php index 3446864..e47650c 100644 --- a/tests/Unit/Metadata/AttributeMetadataFactoryTest.php +++ b/tests/Unit/Metadata/AttributeMetadataFactoryTest.php @@ -25,8 +25,10 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; use Patchlevel\Hydrator\Tests\Unit\Fixture\Wrapper; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +#[CoversClass(AttributeMetadataFactory::class)] final class AttributeMetadataFactoryTest extends TestCase { public function testEmptyObject(): void diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index 51e4acc..7dbd41f 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -41,13 +41,15 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\StatusWithNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer; use Patchlevel\Hydrator\TypeMismatch; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\TypeInfo\Type\ObjectType; +#[CoversClass(MetadataHydrator::class)] +#[CoversClass(TransformMiddleware::class)] final class MetadataHydratorTest extends TestCase { private MetadataHydrator $hydrator; diff --git a/tests/Unit/Middleware/TransformerMiddlewareTest.php b/tests/Unit/Middleware/TransformerMiddlewareTest.php index 288d5fa..4f07755 100644 --- a/tests/Unit/Middleware/TransformerMiddlewareTest.php +++ b/tests/Unit/Middleware/TransformerMiddlewareTest.php @@ -11,8 +11,10 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +#[CoversClass(TransformMiddleware::class)] class TransformerMiddlewareTest extends TestCase { public function testHydrate(): void diff --git a/tests/Unit/Normalizer/ArrayNormalizerTest.php b/tests/Unit/Normalizer/ArrayNormalizerTest.php index fdc8c71..702a8de 100644 --- a/tests/Unit/Normalizer/ArrayNormalizerTest.php +++ b/tests/Unit/Normalizer/ArrayNormalizerTest.php @@ -4,15 +4,15 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; use Patchlevel\Hydrator\Normalizer\Normalizer; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(ArrayNormalizer::class)] final class ArrayNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php b/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php index 45fdfcc..35baed3 100644 --- a/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php +++ b/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php @@ -4,15 +4,15 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Normalizer\ArrayShapeNormalizer; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; use Patchlevel\Hydrator\Normalizer\Normalizer; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(ArrayShapeNormalizer::class)] final class ArrayShapeNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/DateTimeImmutableNormalizerTest.php b/tests/Unit/Normalizer/DateTimeImmutableNormalizerTest.php index 7a4c22b..28510f1 100644 --- a/tests/Unit/Normalizer/DateTimeImmutableNormalizerTest.php +++ b/tests/Unit/Normalizer/DateTimeImmutableNormalizerTest.php @@ -4,14 +4,14 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use DateTime; use DateTimeImmutable; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(DateTimeImmutableNormalizer::class)] final class DateTimeImmutableNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/DateTimeNormalizerTest.php b/tests/Unit/Normalizer/DateTimeNormalizerTest.php index d585727..851382b 100644 --- a/tests/Unit/Normalizer/DateTimeNormalizerTest.php +++ b/tests/Unit/Normalizer/DateTimeNormalizerTest.php @@ -4,13 +4,13 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use DateTime; use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(DateTimeNormalizer::class)] final class DateTimeNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/DateTimeZoneNormalizerTest.php b/tests/Unit/Normalizer/DateTimeZoneNormalizerTest.php index f0300b3..2622863 100644 --- a/tests/Unit/Normalizer/DateTimeZoneNormalizerTest.php +++ b/tests/Unit/Normalizer/DateTimeZoneNormalizerTest.php @@ -4,13 +4,13 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use DateTimeZone; use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(DateTimeZoneNormalizer::class)] final class DateTimeZoneNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/EnumNormalizerTest.php b/tests/Unit/Normalizer/EnumNormalizerTest.php index 2b3e946..8f81b9a 100644 --- a/tests/Unit/Normalizer/EnumNormalizerTest.php +++ b/tests/Unit/Normalizer/EnumNormalizerTest.php @@ -4,16 +4,16 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use Patchlevel\Hydrator\Normalizer\EnumNormalizer; use Patchlevel\Hydrator\Normalizer\InvalidArgument; use Patchlevel\Hydrator\Normalizer\InvalidType; use Patchlevel\Hydrator\Tests\Unit\Fixture\AnotherEnum; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Type; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(EnumNormalizer::class)] final class EnumNormalizerTest extends TestCase { public function testNormalizeWithNull(): void diff --git a/tests/Unit/Normalizer/ObjectNormalizerTest.php b/tests/Unit/Normalizer/ObjectNormalizerTest.php index a99b8c9..3ff1327 100644 --- a/tests/Unit/Normalizer/ObjectNormalizerTest.php +++ b/tests/Unit/Normalizer/ObjectNormalizerTest.php @@ -4,7 +4,6 @@ namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; -use Attribute; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Normalizer\InvalidArgument; use Patchlevel\Hydrator\Normalizer\InvalidType; @@ -14,13 +13,14 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\TypeInfo\Type; use function serialize; use function unserialize; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[CoversClass(ObjectNormalizer::class)] final class ObjectNormalizerTest extends TestCase { public function testNormalizeMissingHydrator(): void