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..0838842 100644 --- a/src/Annotation/ConcreteResolver.php +++ b/src/Annotation/ConcreteResolver.php @@ -15,10 +15,11 @@ abstract class ConcreteResolver /** * @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..df68a41 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -59,6 +59,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,15 +78,15 @@ 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); + $keys = []; foreach ($class->getProperties() as $property) { // statical properties cannot be hydrated... if ($property->isStatic()) { @@ -147,14 +148,16 @@ public function hydrate(string|object $object, array|object $data): object $data[$key] = $mutator->apply($data[$key]); } - $this->hydrateProperty($object, $class, $property, $propertyType, $data[$key]); - unset($data[$key]); + $this->hydrateProperty($object, $class, $property, $propertyType, $data[$key], $data); + $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); } } @@ -215,6 +218,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 +229,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 +262,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 +341,8 @@ private function hydrateProperty( ReflectionClass $class, ReflectionProperty $property, ReflectionNamedType $type, - mixed $value + mixed $value, + array $additional = [] ): void { $propertyType = $type->getName(); @@ -357,7 +363,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 +388,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 +599,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 +616,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 +631,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 +845,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 +856,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..93bae7c 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -831,6 +831,24 @@ 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); + $this->assertSame('notApple', $o->name); + } + public function testHydrateArrayAbstractProperty(): void { $o = (new Hydrator())->hydrate(new ObjectWithArrayOfAbstracts(), [