diff --git a/src/Cryptography/CryptographyMiddleware.php b/src/Cryptography/CryptographyMiddleware.php index 93f4904..cd78542 100644 --- a/src/Cryptography/CryptographyMiddleware.php +++ b/src/Cryptography/CryptographyMiddleware.php @@ -18,33 +18,36 @@ public function __construct( /** * @param ClassMetadata $metadata * @param array $data + * @param array $context * * @return T * * @template T of object */ - public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object { return $stack->next()->hydrate( $metadata, $this->cryptography->decrypt($metadata, $data), + $context, $stack, ); } /** - * @param ClassMetadata $metadata - * @param T $object + * @param ClassMetadata $metadata + * @param T $object + * @param array $context * * @return array * * @template T of object */ - public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array { return $this->cryptography->encrypt( $metadata, - $stack->next()->extract($metadata, $object, $stack), + $stack->next()->extract($metadata, $object, $context, $stack), ); } } diff --git a/src/Hydrator.php b/src/Hydrator.php index 60fb29e..2a59a61 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -9,6 +9,7 @@ interface Hydrator /** * @param class-string $class * @param array $data + * @param array $context * * @return T * @@ -16,8 +17,12 @@ interface Hydrator * * @template T of object */ - public function hydrate(string $class, array $data): object; + public function hydrate(string $class, array $data, array $context = []): object; - /** @return array */ - public function extract(object $object): array; + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context = []): array; } diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index 636ee6d..4c0ca44 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -37,12 +37,13 @@ public function __construct( /** * @param class-string $class * @param array $data + * @param array $context * * @return T * * @template T of object */ - public function hydrate(string $class, array $data): object + public function hydrate(string $class, array $data, array $context = []): object { try { $metadata = $this->metadata($class); @@ -53,7 +54,7 @@ public function hydrate(string $class, array $data): object if (PHP_VERSION_ID < 80400) { $stack = new Stack($this->middlewares); - return $stack->next()->hydrate($metadata, $data, $stack); + return $stack->next()->hydrate($metadata, $data, $context, $stack); } $lazy = $metadata->lazy ?? $this->defaultLazy; @@ -61,25 +62,29 @@ public function hydrate(string $class, array $data): object if (!$lazy) { $stack = new Stack($this->middlewares); - return $stack->next()->hydrate($metadata, $data, $stack); + return $stack->next()->hydrate($metadata, $data, $context, $stack); } return (new ReflectionClass($class))->newLazyProxy( - function () use ($metadata, $data): object { + function () use ($metadata, $data, $context): object { $stack = new Stack($this->middlewares); - return $stack->next()->hydrate($metadata, $data, $stack); + return $stack->next()->hydrate($metadata, $data, $context, $stack); }, ); } - /** @return array */ - public function extract(object $object): array + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context = []): array { $metadata = $this->metadata($object::class); $stack = new Stack($this->middlewares); - return $stack->next()->extract($metadata, $object, $stack); + return $stack->next()->extract($metadata, $object, $context, $stack); } /** diff --git a/src/Middleware/Middleware.php b/src/Middleware/Middleware.php index 33fa3e1..106dab6 100644 --- a/src/Middleware/Middleware.php +++ b/src/Middleware/Middleware.php @@ -11,20 +11,22 @@ interface Middleware /** * @param ClassMetadata $metadata * @param array $data + * @param array $context * * @return T * * @template T of object */ - public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object; + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object; /** - * @param ClassMetadata $metadata - * @param T $object + * @param ClassMetadata $metadata + * @param T $object + * @param array $context * * @return array * * @template T of object */ - public function extract(ClassMetadata $metadata, object $object, Stack $stack): array; + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array; } diff --git a/src/Middleware/TransformMiddleware.php b/src/Middleware/TransformMiddleware.php index c822698..9c42041 100644 --- a/src/Middleware/TransformMiddleware.php +++ b/src/Middleware/TransformMiddleware.php @@ -8,6 +8,7 @@ use Patchlevel\Hydrator\DenormalizationFailure; use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\NormalizationFailure; +use Patchlevel\Hydrator\Normalizer\ContextAwareNormalizer; use Patchlevel\Hydrator\TypeMismatch; use ReflectionParameter; use Throwable; @@ -25,12 +26,13 @@ final class TransformMiddleware implements Middleware /** * @param ClassMetadata $metadata * @param array $data + * @param array $context * * @return T * * @template T of object */ - public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object { $object = $metadata->newInstance(); @@ -58,17 +60,20 @@ public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): obj continue; } - $normalizer = $propertyMetadata->normalizer; - - if ($normalizer) { + if ($propertyMetadata->normalizer) { try { - /** @psalm-suppress MixedAssignment */ - $value = $normalizer->denormalize($data[$propertyMetadata->fieldName]); + if ($propertyMetadata->normalizer instanceof ContextAwareNormalizer) { + /** @psalm-suppress MixedAssignment */ + $value = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); + } else { + /** @psalm-suppress MixedAssignment */ + $value = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName]); + } } catch (Throwable $e) { throw new DenormalizationFailure( $metadata->className, $propertyMetadata->propertyName, - $normalizer::class, + $propertyMetadata->normalizer::class, $e, ); } @@ -90,8 +95,12 @@ public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): obj return $object; } - /** @return array */ - public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + /** + * @param array $context + * + * @return array + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array { $objectId = spl_object_id($object); @@ -110,10 +119,18 @@ public function extract(ClassMetadata $metadata, object $object, Stack $stack): foreach ($metadata->properties as $propertyMetadata) { if ($propertyMetadata->normalizer) { try { - /** @psalm-suppress MixedAssignment */ - $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( - $propertyMetadata->getValue($object), - ); + if ($propertyMetadata->normalizer instanceof ContextAwareNormalizer) { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + $context, + ); + } else { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + ); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { diff --git a/src/Normalizer/ContextAwareNormalizer.php b/src/Normalizer/ContextAwareNormalizer.php new file mode 100644 index 0000000..757eb05 --- /dev/null +++ b/src/Normalizer/ContextAwareNormalizer.php @@ -0,0 +1,22 @@ + $context + * + * @throws InvalidArgument + */ + public function normalize(mixed $value, array $context = []): mixed; + + /** + * @param array $context + * + * @throws InvalidArgument + */ + public function denormalize(mixed $value, array $context = []): mixed; +} diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index a3f64ee..e5558c4 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -15,7 +15,7 @@ use function is_array; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)] -final class ObjectNormalizer implements Normalizer, TypeAwareNormalizer, HydratorAwareNormalizer +final class ObjectNormalizer implements ContextAwareNormalizer, TypeAwareNormalizer, HydratorAwareNormalizer { private Hydrator|null $hydrator = null; @@ -25,8 +25,12 @@ public function __construct( ) { } - /** @return array|null */ - public function normalize(mixed $value): array|null + /** + * @param array $context + * + * @return array|null + */ + public function normalize(mixed $value, array $context = []): array|null { if (!$this->hydrator) { throw new MissingHydrator(); @@ -42,10 +46,11 @@ public function normalize(mixed $value): array|null throw InvalidArgument::withWrongType($className . '|null', $value); } - return $this->hydrator->extract($value); + return $this->hydrator->extract($value, $context); } - public function denormalize(mixed $value): object|null + /** @param array $context */ + public function denormalize(mixed $value, array $context = []): object|null { if (!$this->hydrator) { throw new MissingHydrator(); @@ -61,7 +66,7 @@ public function denormalize(mixed $value): object|null $className = $this->getClassName(); - return $this->hydrator->hydrate($className, $value); + return $this->hydrator->hydrate($className, $value, $context); } public function setHydrator(Hydrator $hydrator): void diff --git a/tests/Unit/Cryptography/CryptographyMiddlewareTest.php b/tests/Unit/Cryptography/CryptographyMiddlewareTest.php index c7b160c..5e675af 100644 --- a/tests/Unit/Cryptography/CryptographyMiddlewareTest.php +++ b/tests/Unit/Cryptography/CryptographyMiddlewareTest.php @@ -32,9 +32,9 @@ public function testHydrate(): void $stack = new Stack([$otherMiddleware]); - $otherMiddleware->expects($this->once())->method('hydrate')->with($metadata, ['name' => 'bar'], $stack)->willReturn($object); + $otherMiddleware->expects($this->once())->method('hydrate')->with($metadata, ['name' => 'bar'], [], $stack)->willReturn($object); - $result = $cryptographyMiddleware->hydrate($metadata, ['name' => 'foo'], $stack); + $result = $cryptographyMiddleware->hydrate($metadata, ['name' => 'foo'], [], $stack); self::assertSame($object, $result); } @@ -54,9 +54,9 @@ public function testExtract(): void $stack = new Stack([$otherMiddleware]); - $otherMiddleware->expects($this->once())->method('extract')->with($metadata, $object, $stack)->willReturn(['name' => 'foo']); + $otherMiddleware->expects($this->once())->method('extract')->with($metadata, $object, [], $stack)->willReturn(['name' => 'foo']); - $result = $cryptographyMiddleware->extract($metadata, $object, $stack); + $result = $cryptographyMiddleware->extract($metadata, $object, [], $stack); self::assertSame(['name' => 'bar'], $result); } diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index 7dbd41f..e0355f5 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -138,6 +138,41 @@ public function testExtractWithInferNormalizer2(): void ); } + public function testExtractWithContext(): void + { + $object = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $expect = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('extract') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $object, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = MetadataHydrator::create([$middleware]); + + $data = $hydrator->extract($object, ['context' => '123']); + + self::assertEquals($expect, $data); + } + public function testHydrate(): void { $expected = new ProfileCreated( @@ -220,6 +255,41 @@ public function testHydrateWithTypeMismatch(): void ); } + public function testHydrateWithContext(): void + { + $expect = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $data = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('hydrate') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $data, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = MetadataHydrator::create([$middleware]); + + $object = $hydrator->hydrate(InferNormalizerDto::class, $data, ['context' => '123']); + + self::assertEquals($expect, $object); + } + public function testDenormalizationFailure(): void { $this->expectException(DenormalizationFailure::class); @@ -498,27 +568,29 @@ public function guess(ObjectType $type): Normalizer|null /** * @param ClassMetadata $metadata * @param array $data + * @param array $context * * @return T * * @template T of object */ - public function hydrate(ClassMetadata $metadata, array $data, Stack $stack): object + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object { - return $stack->next()->hydrate($metadata, $data, $stack); + return $stack->next()->hydrate($metadata, $data, $context, $stack); } /** - * @param ClassMetadata $metadata - * @param T $object + * @param ClassMetadata $metadata + * @param T $object + * @param array $context * * @return array * * @template T of object */ - public function extract(ClassMetadata $metadata, object $object, Stack $stack): array + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array { - return $stack->next()->extract($metadata, $object, $stack); + return $stack->next()->extract($metadata, $object, $context, $stack); } }, ], diff --git a/tests/Unit/Middleware/TransformerMiddlewareTest.php b/tests/Unit/Middleware/TransformerMiddlewareTest.php index 4f07755..b8c4c4a 100644 --- a/tests/Unit/Middleware/TransformerMiddlewareTest.php +++ b/tests/Unit/Middleware/TransformerMiddlewareTest.php @@ -29,6 +29,7 @@ public function testHydrate(): void $event = $middleware->hydrate( $this->classMetadata(ProfileCreated::class), ['profileId' => '1', 'email' => 'info@patchlevel.de'], + [], new Stack([]), ); @@ -47,6 +48,7 @@ public function testExtract(): void ProfileId::fromString('1'), Email::fromString('info@patchlevel.de'), ), + [], new Stack([]), );