From 864e4e677d031d91469f5816e9db3f0aa35eaf86 Mon Sep 17 00:00:00 2001 From: Sergio Brighenti Date: Sat, 17 May 2025 00:09:14 +0200 Subject: [PATCH 1/5] Add support for additional data in hydration methods Extended the Hydrator to accept and process additional data during object hydration. Updated relevant methods, tests, and resolvers to accommodate this change. This enhancement improves flexibility in handling complex data structures. --- composer.json | 2 +- src/Annotation/ConcreteResolver.php | 6 ++-- src/Hydrator.php | 42 +++++++++++++---------- tests/Fixtures/ObjectWithAbstract.php | 1 + tests/Fixtures/Resolver/AppleResolver.php | 2 +- tests/HydratorTest.php | 17 +++++++++ 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index af45c17..457703a 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/container": "^1.1 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "~9.5.0", + "phpunit/phpunit": "^10", "illuminate/container": "^10.0" }, "autoload": { diff --git a/src/Annotation/ConcreteResolver.php b/src/Annotation/ConcreteResolver.php index 4d20031..f4aa037 100644 --- a/src/Annotation/ConcreteResolver.php +++ b/src/Annotation/ConcreteResolver.php @@ -14,11 +14,11 @@ abstract class ConcreteResolver protected array $concretes = []; /** - * @param array $data - * + * @param array $data + * @param array $all * @return string|null */ - abstract public function concreteFor(array $data): ?string; + abstract public function concreteFor(array $data, array $all): ?string; /** * @return array diff --git a/src/Hydrator.php b/src/Hydrator.php index 5c78d4d..0447ebd 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -23,7 +23,6 @@ use SergiX44\Hydrator\Annotation\SkipConstructor; use SergiX44\Hydrator\Annotation\UnionResolver; use SergiX44\Hydrator\Exception\InvalidObjectException; - use function array_key_exists; use function class_exists; use function ctype_digit; @@ -39,7 +38,6 @@ use function is_subclass_of; use function sprintf; use function strtotime; - use const FILTER_NULL_ON_FAILURE; use const FILTER_VALIDATE_BOOLEAN; use const FILTER_VALIDATE_FLOAT; @@ -59,6 +57,7 @@ public function __construct(?ContainerInterface $container = null) * * @param class-string|T $object * @param array|object $data + * @param array $additional * * @throws Exception\UnsupportedPropertyTypeException * If one of the object properties contains an unsupported type. @@ -77,13 +76,13 @@ public function __construct(?ContainerInterface $container = null) * * @template T of object */ - public function hydrate(string|object $object, array|object $data): object + public function hydrate(string|object $object, array|object $data, array $additional = []): object { if (is_object($data)) { $data = get_object_vars($data); } - $object = $this->initializeObject($object, $data); + $object = $this->initializeObject($object, $data, $additional); $class = new ReflectionClass($object); foreach ($class->getProperties() as $property) { @@ -147,7 +146,7 @@ public function hydrate(string|object $object, array|object $data): object $data[$key] = $mutator->apply($data[$key]); } - $this->hydrateProperty($object, $class, $property, $propertyType, $data[$key]); + $this->hydrateProperty($object, $class, $property, $propertyType, $data[$key], $data); unset($data[$key]); } @@ -215,6 +214,8 @@ public function getConcreteResolverFor(string|object $object): ?ConcreteResolver * Initializes the given object. * * @param class-string|T $object + * @param array|object $data + * @param array $additional * * @throws ContainerExceptionInterface * If the object cannot be initialized. @@ -224,7 +225,7 @@ public function getConcreteResolverFor(string|object $object): ?ConcreteResolver * * @template T */ - private function initializeObject(string|object $object, array|object $data): object + private function initializeObject(string|object $object, array|object $data, array $additional = []): object { if (is_object($object)) { return $object; @@ -257,7 +258,7 @@ private function initializeObject(string|object $object, array|object $data): ob $data = get_object_vars($data); } - return $this->initializeObject($attribute->concreteFor($data), $data); + return $this->initializeObject($attribute->concreteFor($data, $additional), $data); } // if we have a container, get the instance through it @@ -336,7 +337,8 @@ private function hydrateProperty( ReflectionClass $class, ReflectionProperty $property, ReflectionNamedType $type, - mixed $value + mixed $value, + array $additional = [] ): void { $propertyType = $type->getName(); @@ -357,7 +359,7 @@ private function hydrateProperty( 'string' === $propertyType => $this->propertyString($object, $class, $property, $type, $value), - 'array' === $propertyType => $this->propertyArray($object, $class, $property, $type, $value), + 'array' === $propertyType => $this->propertyArray($object, $class, $property, $type, $value, $additional), 'object' === $propertyType => $this->propertyObject($object, $class, $property, $type, $value), @@ -382,7 +384,7 @@ private function hydrateProperty( BackedEnum::class ) => $this->propertyBackedEnum($object, $class, $property, $type, $value), - class_exists($propertyType) => $this->propertyFromInstance($object, $class, $property, $type, $value), + class_exists($propertyType) => $this->propertyFromInstance($object, $class, $property, $type, $value, $additional), default => throw new Exception\UnsupportedPropertyTypeException(sprintf( 'The %s.%s property contains an unsupported type %s.', @@ -593,7 +595,8 @@ private function propertyArray( ReflectionClass $class, ReflectionProperty $property, ReflectionNamedType $type, - mixed $value + mixed $value, + array $additional = [] ): void { if (is_object($value)) { $value = get_object_vars($value); @@ -609,7 +612,7 @@ private function propertyArray( $arrayType = $this->getAttributeInstance($property, ArrayType::class); if ($arrayType !== null) { - $value = $this->hydrateObjectsInArray($value, $arrayType->class, $arrayType->depth); + $value = $this->hydrateObjectsInArray($value, $arrayType->class, $arrayType->depth, $additional); } $property->setValue($object, $value); @@ -624,20 +627,20 @@ private function propertyArray( * * @return array */ - private function hydrateObjectsInArray(array $array, string $class, int $depth): array + private function hydrateObjectsInArray(array $array, string $class, int $depth, array $additional = []): array { if ($depth > 1) { - return array_map(function ($child) use ($class, $depth) { - return $this->hydrateObjectsInArray($child, $class, --$depth); + return array_map(function ($child) use ($class, $depth, $additional) { + return $this->hydrateObjectsInArray($child, $class, --$depth, $additional); }, $array); } - return array_map(function ($object) use ($class) { + return array_map(function ($object) use ($class, $additional) { if (is_subclass_of($class, BackedEnum::class)) { return $class::tryFrom($object) ?? $object; } - return $this->hydrate($class, $object); + return $this->hydrate($class, $object, $additional); }, $array); } @@ -838,7 +841,8 @@ private function propertyFromInstance( ReflectionClass $class, ReflectionProperty $property, ReflectionNamedType $type, - mixed $value + mixed $value, + array $additional = [] ): void { if (!is_array($value) && !is_object($value)) { throw new Exception\InvalidValueException($property, sprintf( @@ -848,6 +852,6 @@ private function propertyFromInstance( )); } - $property->setValue($object, $this->hydrate($type->getName(), $value)); + $property->setValue($object, $this->hydrate($type->getName(), $value, $additional)); } } diff --git a/tests/Fixtures/ObjectWithAbstract.php b/tests/Fixtures/ObjectWithAbstract.php index 34e80a0..e24f1e5 100644 --- a/tests/Fixtures/ObjectWithAbstract.php +++ b/tests/Fixtures/ObjectWithAbstract.php @@ -7,4 +7,5 @@ final class ObjectWithAbstract { public Apple $value; + public string $name = 'Apple'; } diff --git a/tests/Fixtures/Resolver/AppleResolver.php b/tests/Fixtures/Resolver/AppleResolver.php index 76a1b3a..1a6418d 100644 --- a/tests/Fixtures/Resolver/AppleResolver.php +++ b/tests/Fixtures/Resolver/AppleResolver.php @@ -15,7 +15,7 @@ class AppleResolver extends ConcreteResolver 'sauce' => AppleSauce::class, ]; - public function concreteFor(array $data): ?string + public function concreteFor(array $data, array $all): ?string { return $this->concretes[$data['type']] ?? null; } diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index d01e493..a663cd7 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -831,6 +831,23 @@ public function testHydrateAbstractProperty(): void $this->assertSame('brandy', $o->value->category); } + public function testHydrateAbstractPropertyWithAdditional(): void + { + $o = (new Hydrator())->hydrate(new ObjectWithAbstract(), [ + 'name' => 'notApple', + 'value' => [ + 'type' => 'jack', + 'sweetness' => null, + 'category' => 'brandy', + ], + ]); + + $this->assertInstanceOf(ObjectWithAbstract::class, $o); + $this->assertInstanceOf(AppleJack::class, $o->value); + $this->assertSame('jack', $o->value->type); + $this->assertSame('brandy', $o->value->category); + } + public function testHydrateArrayAbstractProperty(): void { $o = (new Hydrator())->hydrate(new ObjectWithArrayOfAbstracts(), [ From 64d7238f2e55bd31aaddaf2e9723fde10fb35fe8 Mon Sep 17 00:00:00 2001 From: Sergio Brighenti Date: Fri, 16 May 2025 22:09:28 +0000 Subject: [PATCH 2/5] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Annotation/ConcreteResolver.php | 5 +++-- src/Hydrator.php | 6 ++++-- tests/HydratorTest.php | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Annotation/ConcreteResolver.php b/src/Annotation/ConcreteResolver.php index f4aa037..0838842 100644 --- a/src/Annotation/ConcreteResolver.php +++ b/src/Annotation/ConcreteResolver.php @@ -14,8 +14,9 @@ abstract class ConcreteResolver protected array $concretes = []; /** - * @param array $data - * @param array $all + * @param array $data + * @param array $all + * * @return string|null */ abstract public function concreteFor(array $data, array $all): ?string; diff --git a/src/Hydrator.php b/src/Hydrator.php index 0447ebd..8789dae 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -23,6 +23,7 @@ use SergiX44\Hydrator\Annotation\SkipConstructor; use SergiX44\Hydrator\Annotation\UnionResolver; use SergiX44\Hydrator\Exception\InvalidObjectException; + use function array_key_exists; use function class_exists; use function ctype_digit; @@ -38,6 +39,7 @@ use function is_subclass_of; use function sprintf; use function strtotime; + use const FILTER_NULL_ON_FAILURE; use const FILTER_VALIDATE_BOOLEAN; use const FILTER_VALIDATE_FLOAT; @@ -57,7 +59,7 @@ public function __construct(?ContainerInterface $container = null) * * @param class-string|T $object * @param array|object $data - * @param array $additional + * @param array $additional * * @throws Exception\UnsupportedPropertyTypeException * If one of the object properties contains an unsupported type. @@ -215,7 +217,7 @@ public function getConcreteResolverFor(string|object $object): ?ConcreteResolver * * @param class-string|T $object * @param array|object $data - * @param array $additional + * @param array $additional * * @throws ContainerExceptionInterface * If the object cannot be initialized. diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index a663cd7..815a784 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -834,7 +834,7 @@ public function testHydrateAbstractProperty(): void public function testHydrateAbstractPropertyWithAdditional(): void { $o = (new Hydrator())->hydrate(new ObjectWithAbstract(), [ - 'name' => 'notApple', + 'name' => 'notApple', 'value' => [ 'type' => 'jack', 'sweetness' => null, From 04e5a7156b3ac77e786051c28d39d732fb0db748 Mon Sep 17 00:00:00 2001 From: Sergio Brighenti Date: Sat, 17 May 2025 00:12:56 +0200 Subject: [PATCH 3/5] Add assertion for 'name' property in HydratorTest Ensure the 'name' property is correctly validated in tests. This improves test coverage and helps prevent potential regressions for this property. --- tests/HydratorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index 815a784..93bae7c 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -846,6 +846,7 @@ public function testHydrateAbstractPropertyWithAdditional(): void $this->assertInstanceOf(AppleJack::class, $o->value); $this->assertSame('jack', $o->value->type); $this->assertSame('brandy', $o->value->category); + $this->assertSame('notApple', $o->name); } public function testHydrateArrayAbstractProperty(): void From 144da3fe405ff9674e51548f9a69f4d419ee2499 Mon Sep 17 00:00:00 2001 From: Sergio Brighenti Date: Sat, 17 May 2025 11:13:11 +0200 Subject: [PATCH 4/5] Refactor hydration to optimize unused data handling Introduce a keys tracking mechanism to manage processed data keys, replacing the use of `unset`. This improves clarity and ensures unprocessed data is cleanly managed before invoking `__set`. --- src/Hydrator.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Hydrator.php b/src/Hydrator.php index 8789dae..8776246 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -23,7 +23,6 @@ use SergiX44\Hydrator\Annotation\SkipConstructor; use SergiX44\Hydrator\Annotation\UnionResolver; use SergiX44\Hydrator\Exception\InvalidObjectException; - use function array_key_exists; use function class_exists; use function ctype_digit; @@ -39,7 +38,6 @@ use function is_subclass_of; use function sprintf; use function strtotime; - use const FILTER_NULL_ON_FAILURE; use const FILTER_VALIDATE_BOOLEAN; use const FILTER_VALIDATE_FLOAT; @@ -85,8 +83,8 @@ public function hydrate(string|object $object, array|object $data, array $additi } $object = $this->initializeObject($object, $data, $additional); - $class = new ReflectionClass($object); + $keys = []; foreach ($class->getProperties() as $property) { // statical properties cannot be hydrated... if ($property->isStatic()) { @@ -149,13 +147,15 @@ public function hydrate(string|object $object, array|object $data, array $additi } $this->hydrateProperty($object, $class, $property, $propertyType, $data[$key], $data); - unset($data[$key]); + $keys[] = $key; } + $data = array_diff_key($data, array_flip($keys)); + // if the object has a __set method, we will use it to hydrate the remaining data if (!empty($data) && $class->hasMethod('__set')) { foreach ($data as $key => $value) { - $object->$key = $value; + $object->__set($key, $value); } } From 9cdc2d2a7fa68dd071691ad537afe40086665409 Mon Sep 17 00:00:00 2001 From: Sergio Brighenti Date: Sat, 17 May 2025 09:13:26 +0000 Subject: [PATCH 5/5] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Hydrator.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Hydrator.php b/src/Hydrator.php index 8776246..df68a41 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -23,6 +23,7 @@ use SergiX44\Hydrator\Annotation\SkipConstructor; use SergiX44\Hydrator\Annotation\UnionResolver; use SergiX44\Hydrator\Exception\InvalidObjectException; + use function array_key_exists; use function class_exists; use function ctype_digit; @@ -38,6 +39,7 @@ use function is_subclass_of; use function sprintf; use function strtotime; + use const FILTER_NULL_ON_FAILURE; use const FILTER_VALIDATE_BOOLEAN; use const FILTER_VALIDATE_FLOAT;