The solution is a bit misleading. Sure, you can write code like this. But it binds the hydrators directly to your data objects, which is not best practice since the hydrators can be used in this context only. Let us revisit the example together and refactor the solution a bit.
The Array
The following array is our example we want to hydrate into an object, which can have nested objects of nested objects. Just a multidimensional array …
$data = [
'title' => 'Book of samples',
'author' => [
'name' => 'John',
'birthday' => [
'day' => 12,
'month' => 'March',
'year' => 1988
]
]
];
This array has a child array which holds data for the author. The author has a child array too, which holds data for a birthday. Because we want to hydrate three dimensions of an array into the corresponding objects, we need to define the corresponding objects.
Normally you don 't split up dates like the birthday. Instead use a DateTimeImmutable object for which laminas has already hydrator strategies ready to use. But for example purpose I 'll leave your array as it is …
The objects
To keep it as simple as possible we only define some loose data objects, which only exist to hold the data that comes from the array. No additional logic for hydrators or something else. Just data.
<?php
declare(strict_types=1);
namespace Marcel\Data;
class Book
{
public function __construct(
protected ?string $title = null,
protected ?Author $author = null
) {}
public function getTtitle(): ?string
{
return $this->title;
}
public function getAuthor(): ?Author
{
return $this->author;
}
}
class Author
{
public function __construct(
protected ?string $name = null,
protected ?Birthday $birthday = null
) {}
public function getName(): ?string
{
return $this->name;
}
public function getBirthday(): ?Birthday
{
return $this->birthday;
}
}
class Birthday
{
public function __construct(
protected ?int $day = null,
protected ?string $month = null,
protected ?int $year = null
) {}
public function getDay(): ?int
{
return $this->day;
}
public function getMonth(): ?string
{
return $this->month;
}
public function getYear(): ?int
{
return $this->year;
}
}
To keep it simple as possible I just used some constructor property promotion and the getter methods. Since we 're using \Laminas\Hydrator\ReflectionHydrator
we don 't need the setters. If you want to use \Laminas\Hydrator\ClassMethodsHydrator
you have to define the setters too.
Thus, the data objects do exactly what they are supposed to do: They hold the data. That’s all they have to do. Separation of concerns is important here.
Hydrator Strategies
Hydrator strategies allow you to place special functionality on keys of the array to be processed during the hydration process. If the hydrator reached the author
key, we want the hydrator to hydrate this key with an Author
data object. Within the author array we want to hydrate the Birthday
data object when the birthday
key of the array is reached.
For this we only have to write some factories, which create the instances of the hydrators with the respective strategies attached.
<?php
declare(strict_types=1);
namespace Marcel\Hydrator\Factory;
use Laminas\Hydrator\Strategy\HydratorStrategy;
use Laminas\Hydrator\HydratorPluginManager;
use Laminas\Hydrator\ReflectionHydrator;
use Marcel\Data\Author;
use Psr\Container\ContainerInterface;
class BookHydratorFactory
{
public function __invoke(ContainerInterface $container): ReflectionHydrator
{
$hydrator = new ReflectionHydrator();
$hydrator->addStrategy(
'author',
new HydratorStrategy(
$container->get(HydratorPluginManager::class)->get(AuthorHydrator::class),
Author::class
)
);
return $hydrator;
}
}
We have to do the same for the author hydrator. The birthday just needs a strategy and no factory, since it has no children, which should be hydrated into objects. Yup, seems to be a bit overdeveloped. But you 'll love the outcome.
use Laminas\Hydrator\Strategy\HydratorStrategy;
use Laminas\Hydrator\ReflectionHydrator;
use Marcel\Data\Birthday;
use Psr\Container\ContainerInterface;
class AuthorHydratorFactory
{
public function __construct(ContainerInterface $container): ReflectionHydrator
{
$hydrator = new ReflectionHydrator();
$hydrator->addStrategy(
'author',
new HydratorStrategy(
new ReflectionHydrator(),
Birthday::class
)
);
}
}
Just add the two factories to the hydrator manager in your config on module, local or global level.
'hydrators' => [
'factories' => [
\Marcel\Hydrator\AuthorHydrator::class => \Marcel\Hydrator\Factory\AuthorHydratorFactory::class,
\Marcel\Hydrator\BookHydrator::class => \Marcel\Hydrator\Factory\BookHydratorFactory::class,
],
],
...
With this factories every hydrator is reusable. Think of authors, which have written something other than books. You can easily use the author hydrator in another context.
Glue it all together
I assume you have a service container instance in a factory or somewhere else in your application to get all the objects we 've coded. Think of a controller, which takes the array from a HTTP POST request and after using an input filter you want to hydrate it into objects …
// initiates the book hydrator from the hydrator manager
$hydrator = $container
->get(\Laminas\Hydrator\HydratorPluginManager::class)
->get(\Marcel\Hydrator\BookHydrator::class);
// results into a clean hydrated book with an author, which has a birthday
$book = $hydrator->hydrate($data, new Book());
In this example every concern is separated. Data objects just hold data and don 't have logic. Hydrators just hydrate. All glued together in a controller (or another class, which uses hydrators and data objects). Everything can be re-used in every context you want. At the end you have to code much less lines and everything works as expected.