From d5ff2ce5b26b4b156d93bc02da3ea56ccf5a709f Mon Sep 17 00:00:00 2001 From: Marcel Kempf Date: Wed, 26 Jan 2022 11:24:48 +0100 Subject: [PATCH 1/4] Add constructor parameters decorator Signed-off-by: Marcel Kempf --- ...ConstructorParametersHydratorDecorator.php | 72 ++++++++++++++++ src/DelegatingHydrator.php | 5 +- src/ProxyObject.php | 29 +++++++ src/Strategy/CollectionStrategy.php | 21 ++++- src/Strategy/HydratorStrategy.php | 12 ++- ...tructorParametersHydratorDecoratorTest.php | 85 +++++++++++++++++++ test/TestAsset/ObjectWithConstructor.php | 55 ++++++++++++ 7 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 src/ConstructorParametersHydratorDecorator.php create mode 100644 src/ProxyObject.php create mode 100644 test/ConstructorParametersHydratorDecoratorTest.php create mode 100644 test/TestAsset/ObjectWithConstructor.php diff --git a/src/ConstructorParametersHydratorDecorator.php b/src/ConstructorParametersHydratorDecorator.php new file mode 100644 index 00000000..85d6a5c4 --- /dev/null +++ b/src/ConstructorParametersHydratorDecorator.php @@ -0,0 +1,72 @@ + */ + 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; + } + $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()]; + } +} 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..256ee104 --- /dev/null +++ b/test/ConstructorParametersHydratorDecoratorTest.php @@ -0,0 +1,85 @@ + 'bar', + 'bar' => 99, + 'isMandatory' => true, + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals(new ObjectWithConstructor('bar', 99, true), $object); + } + + public function testWithAdditionalSetter(): void + { + $data = [ + 'foo' => 'bar', + 'bar' => 99, + 'isMandatory' => true, + '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', 99, true))->setBaz('Hello world'), + $object + ); + } + + public function testWithMissingOptionalParameter(): void + { + $data = [ + 'foo' => 'bar', + 'isMandatory' => true, + ]; + $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); + $object = $subject->hydrate($data, new ProxyObject(ObjectWithConstructor::class)); + + Assert::assertInstanceOf(ObjectWithConstructor::class, $object); + Assert::assertEquals(new ObjectWithConstructor('bar', 42, true), $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', 99, true)); + Assert::assertSame( + [ + 'foo' => 'bar', + 'bar' => 99, + 'isMandatory' => true, + 'baz' => null, + ], + $data + ); + } +} diff --git a/test/TestAsset/ObjectWithConstructor.php b/test/TestAsset/ObjectWithConstructor.php new file mode 100644 index 00000000..b5428774 --- /dev/null +++ b/test/TestAsset/ObjectWithConstructor.php @@ -0,0 +1,55 @@ +foo = $foo; + $this->bar = $bar; + $this->isMandatory = $isMandatory; + } + /** @codingStandardsIgnoreEnd */ + + public function getFoo(): string + { + return $this->foo; + } + + public function getBar(): ?int + { + return $this->bar; + } + + public function isMandatory(): bool + { + return $this->isMandatory; + } + + public function getBaz(): ?string + { + return $this->baz; + } + + public function setBaz(?string $baz): self + { + $this->baz = $baz; + return $this; + } +} From 85de64a3dcc56d1d8ed3fa0217cb9102572f127e Mon Sep 17 00:00:00 2001 From: Marcel Kempf Date: Wed, 16 Feb 2022 14:03:12 +0100 Subject: [PATCH 2/4] Cast scalar values in decorator Signed-off-by: Marcel Kempf --- ...ConstructorParametersHydratorDecorator.php | 28 ++++++++++++++++++ ...tructorParametersHydratorDecoratorTest.php | 29 +++++++++++++++---- test/TestAsset/ObjectWithConstructor.php | 29 ++++++++++++------- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/ConstructorParametersHydratorDecorator.php b/src/ConstructorParametersHydratorDecorator.php index 85d6a5c4..e411edbe 100644 --- a/src/ConstructorParametersHydratorDecorator.php +++ b/src/ConstructorParametersHydratorDecorator.php @@ -6,6 +6,7 @@ use ReflectionClass; use ReflectionException; +use ReflectionNamedType; use ReflectionParameter; final class ConstructorParametersHydratorDecorator implements HydratorInterface @@ -48,6 +49,8 @@ public function hydrate(array $data, object $object) } catch (ReflectionException $e) { $value = null; } + + $value = $this->castScalarValue($value, $constructorParameter); $parameters[] = $this->decoratedHydrator->hydrateValue($parameterName, $value, $data); } @@ -69,4 +72,29 @@ private function getConstructorParameters(ProxyObject $object): array return self::$parametersCache[$object->getObjectClassName()]; } + + /** + * @param mixed $value + * @param ReflectionParameter $constructorParameter + * @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/test/ConstructorParametersHydratorDecoratorTest.php b/test/ConstructorParametersHydratorDecoratorTest.php index 256ee104..38a60312 100644 --- a/test/ConstructorParametersHydratorDecoratorTest.php +++ b/test/ConstructorParametersHydratorDecoratorTest.php @@ -20,12 +20,28 @@ public function testWithAllParametersPresent(): void 'foo' => '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', 99, true), $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 @@ -34,6 +50,7 @@ public function testWithAdditionalSetter(): void 'foo' => 'bar', 'bar' => 99, 'isMandatory' => true, + 'price' => 19.98, 'baz' => 'Hello world', ]; $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); @@ -41,7 +58,7 @@ public function testWithAdditionalSetter(): void Assert::assertInstanceOf(ObjectWithConstructor::class, $object); Assert::assertEquals( - (new ObjectWithConstructor('bar', 99, true))->setBaz('Hello world'), + (new ObjectWithConstructor('bar', true, 19.98, 99))->setBaz('Hello world'), $object ); } @@ -51,12 +68,13 @@ 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', 42, true), $object); + Assert::assertEquals(new ObjectWithConstructor('bar', true, 19.98, 42), $object); } public function testWithMissingMandatoryParameter(): void @@ -71,12 +89,13 @@ public function testWithMissingMandatoryParameter(): void public function testExtract(): void { $subject = new ConstructorParametersHydratorDecorator(new ClassMethodsHydrator(false)); - $data = $subject->extract(new ObjectWithConstructor('bar', 99, true)); + $data = $subject->extract(new ObjectWithConstructor('bar', true, 19.98, 99)); Assert::assertSame( [ 'foo' => 'bar', - 'bar' => 99, 'isMandatory' => true, + 'price' => 19.98, + 'bar' => 99, 'baz' => null, ], $data diff --git a/test/TestAsset/ObjectWithConstructor.php b/test/TestAsset/ObjectWithConstructor.php index b5428774..25ab306c 100644 --- a/test/TestAsset/ObjectWithConstructor.php +++ b/test/TestAsset/ObjectWithConstructor.php @@ -9,37 +9,44 @@ final class ObjectWithConstructor /** @var string */ private $foo; - /** @var int|null */ - private $bar; - /** @var bool */ private $isMandatory; + /** @var float */ + private $price; + + /** @var int|null */ + private $bar; + /** @var string|null */ private $baz; - /** @codingStandardsIgnoreStart */ - public function __construct(string $foo, ?int $bar = 42, bool $isMandatory) + public function __construct(string $foo, bool $isMandatory, float $price, ?int $bar = 42) { $this->foo = $foo; - $this->bar = $bar; $this->isMandatory = $isMandatory; + $this->bar = $bar; + $this->price = $price; } - /** @codingStandardsIgnoreEnd */ public function getFoo(): string { return $this->foo; } - public function getBar(): ?int + public function isMandatory(): bool { - return $this->bar; + return $this->isMandatory; } - public function isMandatory(): bool + public function getPrice(): float { - return $this->isMandatory; + return $this->price; + } + + public function getBar(): ?int + { + return $this->bar; } public function getBaz(): ?string From fa0d519857dd7304fca0d22b540aa09f76a13398 Mon Sep 17 00:00:00 2001 From: Marcel Kempf Date: Wed, 16 Feb 2022 14:06:57 +0100 Subject: [PATCH 3/4] Align equal sign Signed-off-by: Marcel Kempf --- src/ConstructorParametersHydratorDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConstructorParametersHydratorDecorator.php b/src/ConstructorParametersHydratorDecorator.php index e411edbe..83addb6d 100644 --- a/src/ConstructorParametersHydratorDecorator.php +++ b/src/ConstructorParametersHydratorDecorator.php @@ -50,7 +50,7 @@ public function hydrate(array $data, object $object) $value = null; } - $value = $this->castScalarValue($value, $constructorParameter); + $value = $this->castScalarValue($value, $constructorParameter); $parameters[] = $this->decoratedHydrator->hydrateValue($parameterName, $value, $data); } From cd0442a0171c95aa9088af302fd223f155ff5adf Mon Sep 17 00:00:00 2001 From: Marcel Kempf Date: Wed, 16 Feb 2022 14:08:59 +0100 Subject: [PATCH 4/4] Fix code style Signed-off-by: Marcel Kempf --- src/ConstructorParametersHydratorDecorator.php | 11 +++++------ test/TestAsset/ObjectWithConstructor.php | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ConstructorParametersHydratorDecorator.php b/src/ConstructorParametersHydratorDecorator.php index 83addb6d..bc878c54 100644 --- a/src/ConstructorParametersHydratorDecorator.php +++ b/src/ConstructorParametersHydratorDecorator.php @@ -75,24 +75,23 @@ private function getConstructorParameters(ProxyObject $object): array /** * @param mixed $value - * @param ReflectionParameter $constructorParameter * @return mixed */ private function castScalarValue($value, ReflectionParameter $constructorParameter) { - if ($value === null || !$constructorParameter->getType() instanceof ReflectionNamedType) { + if ($value === null || ! $constructorParameter->getType() instanceof ReflectionNamedType) { return $value; } switch ($constructorParameter->getType()->getName()) { case 'string': - return (string)$value; + return (string) $value; case 'int': - return (int)$value; + return (int) $value; case 'float': - return (float)$value; + return (float) $value; case 'bool': - return (bool)$value; + return (bool) $value; default: return $value; } diff --git a/test/TestAsset/ObjectWithConstructor.php b/test/TestAsset/ObjectWithConstructor.php index 25ab306c..7d501a0f 100644 --- a/test/TestAsset/ObjectWithConstructor.php +++ b/test/TestAsset/ObjectWithConstructor.php @@ -23,10 +23,10 @@ final class ObjectWithConstructor public function __construct(string $foo, bool $isMandatory, float $price, ?int $bar = 42) { - $this->foo = $foo; + $this->foo = $foo; $this->isMandatory = $isMandatory; - $this->bar = $bar; - $this->price = $price; + $this->bar = $bar; + $this->price = $price; } public function getFoo(): string