Hydrating 3 levels deep Nested Objects

I have the following array :

Array
(
    [a] => test_1
    [hydra_b] => Array
        (
            [a] => test_2
            [hydra_c] => Array
                (
                    [a] => test_3
                    [b] => test_4
                )

        )

)

After hydration using ReflectionHydrator I get:

HydraA Object
(
    [a:HydraA:private] => test_1
    [hydraB:HydraA:private] => HydraB Object
        (
            [a:HydraB:private] => test_2
            [hydraC:HydraB:private] => 
        )

)

I have tested hydration for each class HydraA, HydraB and HydraC. The moment the nesting gets over 2 deep the hydration fails. If it is one or two levels deep it works correct. Any tips or tricks to hydrate multi level deep objects? I could not find any information anywhere or examples that someone has done this. I have tried switching between ClassMethodsHydrator and ReflectionHydrator without any luck.

Hello and welcome to our forums! :smiley:

Look at the strategies for hydrators, there you will find the HydratorStrategy:

The HydratorStrategy can be used to hydrate an object and its child objects with data from a nested array and vice versa .

A code example can be found in the documentation and the strategy can also be used for deeper levels of a nested structure:

$artistHydrator = new Laminas\Hydrator\ReflectionHydrator();
$artistHydrator->add(
    'aliases',
    new Laminas\Hydrator\Strategy\HydratorStrategy(
        new Laminas\Hydrator\ReflectionHydrator(),
        Aliases::class
    )
);

$albumHydrator = new Laminas\Hydrator\ReflectionHydrator();
$albumHydrator->addStrategy(
    'artist',
    new Laminas\Hydrator\Strategy\HydratorStrategy(
        $artistHydrator,
        Artist::class
    )
);

A good alternative to map arrays to typed objects is Valinor.

Thank you for the quick reply but these are the exact hydrators and strategies i am using. This is the example that i made to test.

$content = '';

$payload = json_decode('{"a":"test_1", "hydra_b":{"a":"test_2", "hydra_c": {"a":"test_3", "b":"test_4"}}}', TRUE);
$hydra = HydraA::create($payload);
$content .= "<pre>" . print_r($payload, TRUE) . "</pre>";
$content .= "<pre>" . print_r($hydra, TRUE) . "</pre>";

$content .= 'sample : ' . $hydra->getHydraB()->getHydraC()->getA();

$payload = json_decode('{"a":"test_2", "hydra_c": {"a":"test_3", "b":"test_4"}}', TRUE);
$hydra = HydraB::create($payload);
$content .= "<pre>" . print_r($payload, TRUE) . "</pre>";
$content .= "<pre>" . print_r($hydra, TRUE) . "</pre>";

$payload = json_decode('{"a":"test_3", "b":"test_4"}', TRUE);
$hydra = HydraC::create($payload);
$content .= "<pre>" . print_r($payload, TRUE) . "</pre>";
$content .= "<pre>" . print_r($hydra, TRUE) . "</pre>";

echo $content
class HydraA {

	/** @var string $hydraB */
	private $a;

	/** @var HydraB $hydraB */
	private $hydraB;

	/**
	 * @return string
	 */
	public function getA(): string
	{
		return $this->a;
	}

	/**
	 * @param string $a
	 */
	public function setA(string $a): void
	{
		$this->a = $a;
	}

	/**
	 * @return HydraB
	 */
	public function getHydraB(): HydraB
	{
		return $this->hydraB;
	}

	/**
	 * @param HydraB $hydraB
	 */
	public function setHydraB(HydraB $hydraB): void
	{
		$this->hydraB = $hydraB;
	}

	public static function create(array $item): self
	{
		$objectInstance = new self();
		/** @var HydraA $hydratedObject */
		$hydratedObject = self::get_hydrator()->hydrate($item, $objectInstance);

		return $hydratedObject;
	}

	public function asArray(): array
	{
		return self::get_hydrator()->extract($this);
	}
	public static function get_hydrator(): HydratorInterface
	{
		$strategyB = new HydratorStrategy(new ReflectionHydrator(), HydraB::class);
		$strategyC = new HydratorStrategy(new ReflectionHydrator(), HydraC::class);

		$hydrator = new ReflectionHydrator();
		$hydrator->setNamingStrategy(new UnderscoreNamingStrategy());
		$hydrator->addStrategy('hydraB', $strategyB);
		$hydrator->addStrategy('hydraC', $strategyC);

		return $hydrator;
	}
}
class HydraB {

	/** @var string $a */
	private $a;

	/** @var HydraC $hydraC */
	private $hydraC;
	/**
	 * @return string
	 */
	public function getA(): string
	{
		return $this->a;
	}

	/**
	 * @param string $a
	 */
	public function setA(string $a): void
	{
		$this->a = $a;
	}

	/**
	 * @return HydraC
	 */
	public function getHydraC(): HydraC
	{
		return $this->hydraC;
	}

	/**
	 * @param HydraC $hydraC
	 */
	public function setHydraC(HydraC $hydraC): void
	{
		$this->hydraC = $hydraC;
	}

	public static function create(array $item): self
	{
		$objectInstance = new self();
		/** @var HydraB $hydratedObject */
		$hydratedObject = self::get_hydrator()->hydrate($item, $objectInstance);

		return $hydratedObject;
	}

	public function asArray(): array
	{
		return self::get_hydrator()->extract($this);
	}
	public static function get_hydrator(): HydratorInterface
	{
		$strategyC = new HydratorStrategy(new ReflectionHydrator(), HydraC::class);

		$hydrator = new ReflectionHydrator();
		$hydrator->setNamingStrategy(new UnderscoreNamingStrategy());
		$hydrator->addStrategy('hydraC', $strategyC);

		return $hydrator;
	}
}
class HydraC {

	/** @var string $a */
	private $a;

	/** @var string $b */
	private $b;
	/**
	 * @return string
	 */
	public function getA(): string
	{
		return $this->a;
	}

	/**
	 * @param string $a
	 */
	public function setA(string $a): void
	{
		$this->a = $a;
	}

	/**
	 * @param string $b
	 */
	public function setB(string $b): void
	{
		$this->b = $b;
	}

	public static function create(array $item): self
	{
		$objectInstance = new self();
		/** @var HydraC $hydratedObject */
		$hydratedObject = self::get_hydrator()->hydrate($item, $objectInstance);

		return $hydratedObject;
	}

	public function asArray(): array
	{
		return self::get_hydrator()->extract($this);
	}
	public static function get_hydrator(): HydratorInterface
	{
		$hydrator = new ReflectionHydrator();
		$hydrator->setNamingStrategy(new UnderscoreNamingStrategy());

		return $hydrator;
	}
}

Check the Laminas\Hydrator\Strategy\HydratorStrategy and you do not need this complex construct of A, B and C.

The construct is not complex it is exactly the same as the documentation. I have just given example if you try hydrate 3, 2 ,1 level deep structure. And if you run it you will see that it all works well for 1 and 2 level as per the documentation but the moment you go to level 3 it stops working. The only difference is that i use naming mapper with the hydrator.

The naming alone is not for me: HydraA, HydraB, a, b, c, … :wink:

I’m sorry, I don’t know which example from the documentation you mean.

I reworked the example by introducing a Book, Author and Birthday classes with sample parameters. I then went and used the example from the documentation.

The result is the same as before Level 1 is fine Level 2 is fine level 3 is not. This is the result of hydration. As you can see the birthday Object gets hydrated with an array type instead of the actul object. if you use other hydrators or different strategies the result is either the same or the 3rd level class just becomes a NULL instead of hydrated.

Book Object
(
    [title:Book:private] => Book of samples
    [author:Book:private] => Author Object
        (
            [name:Author:private] => John
            [birthday:Author:private] => Array
                (
                    [day] => 12
                    [month] => March
                    [year] => 1988
                )

        )

)
$payload = [
	'title' => 'Book of samples',
	'author' => [
		'name' => 'John',
		'birthday' => [
			'day'=> 12,
			'month' => 'March',
			'year' => 1988
		]
	]
];

$bookObject = new Book();
$hydrator = new Laminas\Hydrator\ReflectionHydrator();
$hydrator->addStrategy(
	'author',
	new Laminas\Hydrator\Strategy\HydratorStrategy(
		new Laminas\Hydrator\ReflectionHydrator(),
		Author::class
	)
);
$hydrator->addStrategy(
	'birthday',
	new Laminas\Hydrator\Strategy\HydratorStrategy(
		new Laminas\Hydrator\ReflectionHydrator(),
		Birthday::class
	)
);

/** @var Book $hydratedObject */
$hydratedObject = $hydrator->hydrate($payload, $bookObject);  
class Book {

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

	/** @var Author|null $author */
	private $author;

	public function __construct(?string $title = null, ?Author $author = null)
	{
		$this->title = $title;
		$this->author = $author;
	}

	/**
	 * @return string
	 */
	public function getTitle(): string
	{
		return $this->title;
	}

	/**
	 * @param string $title
	 */
	public function setTitle(string $title): void
	{
		$this->title = $title;
	}

	/**
	 * @return Author
	 */
	public function getAuthor(): Author
	{
		return $this->author;
	}

	/**
	 * @param Author $author
	 */
	public function setAuthor(Author $author): void
	{
		$this->author = $author;
	}
}
class Author {

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

	/** @var Birthday|null $birthday */
	private $birthday;

	public function __construct(?string $name = null, ?Birthday $birthday = null)
	{
		$this->name = $name;
		$this->birthday = $birthday;
	}

	public function getName(): string
	{
		return $this->name;
	}

	public function getBirthday(): Birthday
	{
		return $this->birthday;
	}

	public function setName(string $name): self
	{
		$this->name = $name;
		return $this;
	}
	public function setBirthday(Birthday $birthday): self
	{
		$this->birthday = $birthday;
		return $this;
	}
}
class Birthday {

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

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


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

	public function __construct(?int $day = null, ?string $month = null, ?int $year = null)
	{
		$this->day = $day;
		$this->month = $month;
		$this->year = $year;
	}

	public function getDay(): int
	{
		return $this->day;
	}

	public function getMonth(): ?string
	{
		return $this->month;
	}

	public function getYear(): ?int
	{
		return $this->year;
	}

	public function setDay(string $day): self
	{
		$this->day = $day;
		return $this;
	}

	public function setMonth(string $month): self
	{
		$this->month = $month;
		return $this;
	}

	public function setYear(int $year): self
	{
		$this->year = $year;
		return $this;
	}
}

This result is correct as far, because the book has no birthday.

Look at my code example above and you will see that your author needs a separate hydrator that handles the birthday.

Thank you for your help! I finally put the puzzle together. I hope this example helps someone else that was just as confused as myself:

$payload_book = [
	'title'  => 'Book of samples',
	'author' => [
		'name'     => 'John',
		'birthday' => [
			'day'   => 12,
			'month' => 'March',
			'year'  => 1988
		]
	]
];

$payload_author = [
	'name'     => 'John',
	'birthday' => [
		'day'   => 12,
		'month' => 'March',
		'year'  => 1988
	]
];

$payload_birthday = [
	'day'   => 12,
	'month' => 'March',
	'year'  => 1988
];

$hydratedBookObject = Book::create($payload_book);
$hydratedAuthorObject = Author::create($payload_author);
$hydratedBirthdayObject = Birthday::create($payload_birthday);
class Book {

	/** @var string $title */
	private $title;

	/** @var Author $author */
	private $author;

	/**
	 * @return string
	 */
	public function getTitle(): string
	{
		return $this->title;
	}

	/**
	 * @param string $title
	 */
	public function setTitle(string $title): void
	{
		$this->title = $title;
	}

	/**
	 * @return Author
	 */
	public function getAuthor(): Author
	{
		return $this->author;
	}

	/**
	 * @param Author $author
	 */
	public function setAuthor(Author $author): void
	{
		$this->author = $author;
	}

	public static function create(array $data): self
	{
		$book = new self();

		/** @var Book $hydratedBook */
		$hydratedBook = self::getHydrator()->hydrate($data, $book);

		return $hydratedBook;
	}

	public static function getHydrator(): HydratorInterface
	{
		$bookHydrator = new ReflectionHydrator();
		$bookHydrator->addStrategy(
			'author',
			new Laminas\Hydrator\Strategy\HydratorStrategy(
				Author::getHydrator(),
				Author::class
			)
		);

		return $bookHydrator;
	}
}
class Author {

	/** @var string $name */
	private $name;

	/** @var Birthday $birthday */
	private $birthday;

	public function getName(): string
	{
		return $this->name;
	}

	public function getBirthday(): Birthday
	{
		return $this->birthday;
	}

	public function setName(string $name): self
	{
		$this->name = $name;
		return $this;
	}
	public function setBirthday(Birthday $birthday): self
	{
		$this->birthday = $birthday;
		return $this;
	}

	public static function create(array $data): self
	{
		$author = new Author();

		/** @var Author $hydratedAuthor */
		$hydratedAuthor = self::getHydrator()->hydrate($data, $author);

		return $hydratedAuthor;
	}

	public static function getHydrator():HydratorInterface
	{
		$authorHydrator = new Laminas\Hydrator\ReflectionHydrator();
		$authorHydrator->addStrategy(
			'birthday',
			new Laminas\Hydrator\Strategy\HydratorStrategy(
				Birthday::getHydrator(),
				Birthday::class
			)
		);

		return $authorHydrator;
	}
}
class Birthday {

	/** @var int $day */
	private $day;

	/** @var string $month */
	private $month;


	/** @var int $year */
	private $year;

	public function getDay(): int
	{
		return $this->day;
	}

	public function getMonth(): ?string
	{
		return $this->month;
	}

	public function getYear(): ?int
	{
		return $this->year;
	}

	public function setDay(string $day): self
	{
		$this->day = $day;
		return $this;
	}

	public function setMonth(string $month): self
	{
		$this->month = $month;
		return $this;
	}

	public function setYear(int $year): self
	{
		$this->year = $year;
		return $this;
	}

	public static function create(array $data): self
	{
		$birthday = new self();

		/** @var Birthday $hydratedBirthday */
		$hydratedBirthday = self::getHydrator()->hydrate($data, $birthday);

		return $hydratedBirthday;
	}

	public static function getHydrator(): HydratorInterface
	{
		return new Laminas\Hydrator\ReflectionHydrator();
	}
}
Book Object
(
    [title:Book:private] => Book of samples
    [author:Book:private] => Author Object
        (
            [name:Author:private] => John
            [birthday:Author:private] => Birthday Object
                (
                    [day:Birthday:private] => 12
                    [month:Birthday:private] => March
                    [year:Birthday:private] => 1988
                )

        )

)

Author Object
(
    [name:Author:private] => John
    [birthday:Author:private] => Birthday Object
        (
            [day:Birthday:private] => 12
            [month:Birthday:private] => March
            [year:Birthday:private] => 1988
        )

)

Birthday Object
(
    [day:Birthday:private] => 12
    [month:Birthday:private] => March
    [year:Birthday:private] => 1988
)

1 Like

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.

2 Likes

@ezkimo
Great and clean revision! :+1:t2:


Unfortunately, solutions like the hydrators tempt you to adapt your classes, which means that we have designed our objects for a framework or library. This also leads to objects that have no valid state and are therefore meaningless containers.

Shortened example from above:

class Book
{
	private $title;

	private $author;

	public function getTitle(): string
	{
		return $this->title;
	}

	public function setTitle(string $title): void
	{
		$this->title = $title;
	}

	// …
}

This object is useless:

$book = new Book();

It should look something like this:

final class Book
{
    public function __construct(
        public readonly string $title,
        public readonly Author $author
    ) {}
}

Therefore I prefer libraries that respect/use the constructor and can handle data types, for example: