diff --git a/src/ConstructorParametersHydratorDecorator.php b/src/ConstructorParametersHydratorDecorator.php new file mode 100644 index 00000000..bc878c54 --- /dev/null +++ b/src/ConstructorParametersHydratorDecorator.php @@ -0,0 +1,99 @@ + */ + private static $parametersCache = []; + + /** @var AbstractHydrator */ + private $decoratedHydrator; + + public function __construct(AbstractHydrator $decoratedHydrator) + { + $this->decoratedHydrator = $decoratedHydrator; + } + + /** + * {@inheritDoc} + */ + public function extract(object $object): array + { + return $this->decoratedHydrator->extract($object); + } + + /** + * {@inheritDoc} + */ + public function hydrate(array $data, object $object) + { + if (! $object instanceof ProxyObject) { + return $this->decoratedHydrator->hydrate($data, $object); + } + + $constructorParameters = $this->getConstructorParameters($object); + $parameters = []; + foreach ($constructorParameters as $constructorParameter) { + $parameterName = $this->decoratedHydrator->extractName($constructorParameter->getName()); + try { + /** @var mixed $value */ + $value = $data[$parameterName] ?? $constructorParameter->getDefaultValue(); + } catch (ReflectionException $e) { + $value = null; + } + + $value = $this->castScalarValue($value, $constructorParameter); + $parameters[] = $this->decoratedHydrator->hydrateValue($parameterName, $value, $data); + } + + return $this->decoratedHydrator->hydrate($data, $object->createProxiedObject($parameters)); + } + + /** @return ReflectionParameter[] */ + private function getConstructorParameters(ProxyObject $object): array + { + if (! isset(self::$parametersCache[$object->getObjectClassName()])) { + $reflection = new ReflectionClass($object->getObjectClassName()); + $constructor = $reflection->getConstructor(); + + self::$parametersCache[$object->getObjectClassName()] = []; + if ($constructor !== null) { + self::$parametersCache[$object->getObjectClassName()] = $constructor->getParameters(); + } + } + + return self::$parametersCache[$object->getObjectClassName()]; + } + + /** + * @param mixed $value + * @return mixed + */ + private function castScalarValue($value, ReflectionParameter $constructorParameter) + { + if ($value === null || ! $constructorParameter->getType() instanceof ReflectionNamedType) { + return $value; + } + + switch ($constructorParameter->getType()->getName()) { + case 'string': + return (string) $value; + case 'int': + return (int) $value; + case 'float': + return (float) $value; + case 'bool': + return (bool) $value; + default: + return $value; + } + } +} diff --git a/src/DelegatingHydrator.php b/src/DelegatingHydrator.php index f3674e87..b9974d06 100644 --- a/src/DelegatingHydrator.php +++ b/src/DelegatingHydrator.php @@ -23,7 +23,10 @@ public function __construct(ContainerInterface $hydrators) */ public function hydrate(array $data, object $object) { - return $this->getHydrator($object)->hydrate($data, $object); + if (! $object instanceof ProxyObject) { + return $this->getHydrator($object)->hydrate($data, $object); + } + return $this->hydrators->get($object->getObjectClassName())->hydrate($data, $object); } /** diff --git a/src/ProxyObject.php b/src/ProxyObject.php new file mode 100644 index 00000000..3c758eb4 --- /dev/null +++ b/src/ProxyObject.php @@ -0,0 +1,29 @@ +objectClassName = $objectClassName; + } + + /** @return class-string */ + public function getObjectClassName(): string + { + return $this->objectClassName; + } + + /** @param array $parameters */ + public function createProxiedObject(array $parameters): object + { + return new $this->objectClassName(...$parameters); + } +} diff --git a/src/Strategy/CollectionStrategy.php b/src/Strategy/CollectionStrategy.php index 99100f2e..31adbc1e 100644 --- a/src/Strategy/CollectionStrategy.php +++ b/src/Strategy/CollectionStrategy.php @@ -6,6 +6,7 @@ use Laminas\Hydrator\Exception; use Laminas\Hydrator\HydratorInterface; +use Laminas\Hydrator\ProxyObject; use ReflectionClass; use function array_map; @@ -24,11 +25,17 @@ class CollectionStrategy implements StrategyInterface /** @var string */ private $objectClassName; + /** @var bool */ + private $useProxyObject; + /** * @throws Exception\InvalidArgumentException */ - public function __construct(HydratorInterface $objectHydrator, string $objectClassName) - { + public function __construct( + HydratorInterface $objectHydrator, + string $objectClassName, + bool $useProxyObject = false + ) { if (! class_exists($objectClassName)) { throw new Exception\InvalidArgumentException(sprintf( 'Object class name needs to be the name of an existing class, got "%s" instead.', @@ -38,6 +45,7 @@ public function __construct(HydratorInterface $objectHydrator, string $objectCla $this->objectHydrator = $objectHydrator; $this->objectClassName = $objectClassName; + $this->useProxyObject = $useProxyObject; } /** @@ -85,6 +93,15 @@ public function hydrate($value, ?array $data = null) )); } + if ($this->useProxyObject) { + return array_map(function ($data) { + return $this->objectHydrator->hydrate( + $data, + new ProxyObject($this->objectClassName) + ); + }, $value); + } + $reflection = new ReflectionClass($this->objectClassName); return array_map(function ($data) use ($reflection) { diff --git a/src/Strategy/HydratorStrategy.php b/src/Strategy/HydratorStrategy.php index 88ce19ec..31220ee3 100644 --- a/src/Strategy/HydratorStrategy.php +++ b/src/Strategy/HydratorStrategy.php @@ -5,6 +5,7 @@ namespace Laminas\Hydrator\Strategy; use Laminas\Hydrator\HydratorInterface; +use Laminas\Hydrator\ProxyObject; use ReflectionClass; use ReflectionException; @@ -23,12 +24,16 @@ class HydratorStrategy implements StrategyInterface /** @var string */ private $objectClassName; + /** @var bool */ + private $useProxyObject; + /** * @throws Exception\InvalidArgumentException */ public function __construct( HydratorInterface $objectHydrator, - string $objectClassName + string $objectClassName, + bool $useProxyObject = false ) { if (! class_exists($objectClassName)) { throw new Exception\InvalidArgumentException( @@ -41,6 +46,7 @@ public function __construct( $this->objectHydrator = $objectHydrator; $this->objectClassName = $objectClassName; + $this->useProxyObject = $useProxyObject; } /** @@ -90,6 +96,10 @@ public function hydrate($value, ?array $data = null) ); } + if ($this->useProxyObject) { + return new ProxyObject($this->objectClassName); + } + $reflection = new ReflectionClass($this->objectClassName); return $this->objectHydrator->hydrate( diff --git a/test/ConstructorParametersHydratorDecoratorTest.php b/test/ConstructorParametersHydratorDecoratorTest.php new file mode 100644 index 00000000..38a60312 --- /dev/null +++ b/test/ConstructorParametersHydratorDecoratorTest.php @@ -0,0 +1,104 @@ + 'bar', + 'bar' => 99, + 'isMandatory' => true, + 'price' => 19.98, + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals(new ObjectWithConstructor('bar', true, 19.98, 99), $object); + } + + public function testWithWrongScalarType(): void + { + $data = [ + 'foo' => 123, + 'bar' => '99', + 'isMandatory' => 1, + 'price' => '19.98', + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals(new ObjectWithConstructor('123', true, 19.98, 99), $object); + } + + public function testWithAdditionalSetter(): void + { + $data = [ + 'foo' => 'bar', + 'bar' => 99, + 'isMandatory' => true, + 'price' => 19.98, + 'baz' => 'Hello world', + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals( + (new ObjectWithConstructor('bar', true, 19.98, 99))->setBaz('Hello world'), + $object + ); + } + + public function testWithMissingOptionalParameter(): void + { + $data = [ + 'foo' => 'bar', + 'isMandatory' => true, + 'price' => 19.98, + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals(new ObjectWithConstructor('bar', true, 19.98, 42), $object); + } + + public function testWithMissingMandatoryParameter(): void + { + $data = []; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + + $this->expectException(TypeError::class); + $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + } + + public function testExtract(): void + { + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $data = $subject->extract(new ObjectWithConstructor('bar', true, 19.98, 99)); + Assert::assertSame( + [ + 'foo' => 'bar', + 'isMandatory' => true, + 'price' => 19.98, + 'bar' => 99, + 'baz' => null, + ], + $data + ); + } +} diff --git a/test/TestAsset/ObjectWithConstructor.php b/test/TestAsset/ObjectWithConstructor.php new file mode 100644 index 00000000..7d501a0f --- /dev/null +++ b/test/TestAsset/ObjectWithConstructor.php @@ -0,0 +1,62 @@ +foo = $foo; + $this->isMandatory = $isMandatory; + $this->bar = $bar; + $this->price = $price; + } + + public function getFoo(): string + { + return $this->foo; + } + + public function isMandatory(): bool + { + return $this->isMandatory; + } + + public function getPrice(): float + { + return $this->price; + } + + public function getBar(): ?int + { + return $this->bar; + } + + public function getBaz(): ?string + { + return $this->baz; + } + + public function setBaz(?string $baz): self + { + $this->baz = $baz; + return $this; + } +}