Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add constructor decorator #79

Open
wants to merge 4 commits into
base: 4.4.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/ConstructorParametersHydratorDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Laminas\Hydrator;

use ReflectionClass;
use ReflectionException;
use ReflectionParameter;

final class ConstructorParametersHydratorDecorator implements HydratorInterface
{
/** @var array<class-string, ReflectionParameter[]> */
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()];
}
}
5 changes: 4 additions & 1 deletion src/DelegatingHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/ProxyObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Laminas\Hydrator;

final class ProxyObject
{
/** @var class-string */
private $objectClassName;

/** @param class-string $objectClassName */
public function __construct(string $objectClassName)
{
$this->objectClassName = $objectClassName;
}

/** @return class-string */
public function getObjectClassName(): string
{
return $this->objectClassName;
}

/** @param array<int, mixed> $parameters */
public function createProxiedObject(array $parameters): object
{
return new $this->objectClassName(...$parameters);
}
}
21 changes: 19 additions & 2 deletions src/Strategy/CollectionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Laminas\Hydrator\Exception;
use Laminas\Hydrator\HydratorInterface;
use Laminas\Hydrator\ProxyObject;
use ReflectionClass;

use function array_map;
Expand All @@ -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.',
Expand All @@ -38,6 +45,7 @@ public function __construct(HydratorInterface $objectHydrator, string $objectCla

$this->objectHydrator = $objectHydrator;
$this->objectClassName = $objectClassName;
$this->useProxyObject = $useProxyObject;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 11 additions & 1 deletion src/Strategy/HydratorStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Laminas\Hydrator\Strategy;

use Laminas\Hydrator\HydratorInterface;
use Laminas\Hydrator\ProxyObject;
use ReflectionClass;
use ReflectionException;

Expand All @@ -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(
Expand All @@ -41,6 +46,7 @@ public function __construct(

$this->objectHydrator = $objectHydrator;
$this->objectClassName = $objectClassName;
$this->useProxyObject = $useProxyObject;
}

/**
Expand Down Expand Up @@ -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(
Expand Down
85 changes: 85 additions & 0 deletions test/ConstructorParametersHydratorDecoratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Hydrator;

use Laminas\Hydrator\ClassMethodsHydrator;
use Laminas\Hydrator\ConstructorParametersHydratorDecorator;
use Laminas\Hydrator\ProxyObject;
use LaminasTest\Hydrator\TestAsset\ObjectWithConstructor;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use TypeError;

final class ConstructorParametersHydratorDecoratorTest extends TestCase
{
public function testWithAllParametersPresent(): void
{
$data = [
'foo' => '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
);
}
}
55 changes: 55 additions & 0 deletions test/TestAsset/ObjectWithConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Hydrator\TestAsset;

final class ObjectWithConstructor
{
/** @var string */
private $foo;

/** @var int|null */
private $bar;

/** @var bool */
private $isMandatory;

/** @var string|null */
private $baz;

/** @codingStandardsIgnoreStart */
public function __construct(string $foo, ?int $bar = 42, bool $isMandatory)
{
$this->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;
}
}