Often times object hydration isn’t a simple matter, and requires a more nuanced, more sophisticated solution than the out-of-the-box options, available in Zend Framework, provide. Today we showcase one such solution - the Aggregate Hydrator.
Often times object hydration isn’t a simple matter, and requires a more nuanced, more sophisticated solution than the out-of-the-box options, available in Zend Framework, provide. Today we showcase one such solution - the Aggregate Hydrator.
Introduced in Zend Framework 2, and consequently available in Zend Framework 3, three base hydrators are available for populating an object from a set of data, as well as for extracting data from a populated object. These are:
- ObjectProperty: Any data key matching a publicly accessible property will be hydrated; any public properties will be used for extraction
- ClassMethods: Any data key matching a setter method will be called in order to hydrate; any method matching a getter method will be called for extraction
- ArraySerializable: Objects must implement either the
exchangeArray()
or populate()
methods to support hydration, and the getArrayCopy()
method to support extraction
Each of these do a good job of hydrating a simple object, in an increasing level of sophistication and flexibility, from ObjectProperty through ArraySerializable. However, what if the object you’re hydrating is itself composed of several objects, built via something like the composite pattern for example?
Let’s say, for example, you were creating a shopping cart system and were building a user object to model a user in the shopping process. Such a user object could be expected to have the following core details:
- username
- password
- first name
- last name
- email address
- physical addresses
Focusing in on the addresses for a moment, a shopping user usually has two, one for their home, or shipping, address, and one their billing address. These can contain the same information, or be different, but are required to ensure that the goods can be paid for and delivered.
The Data Model
So how would we model and hydrate such a semi-sophisticated entity? If you’re not concerned about database normalization, then you might have a one-to-one mapping between the object and the table in which the information is stored (assuming you’re using a database as your data store).
But if you’re doing even a bit of normalization, you’re likely going to have at least a user table and an address table. Then, you might have a join table which mapped a user to their addresses, potentially also storing an address type as well so as to easily differentiate between the multiple addresses stored for the user.
The Object Model
That’s the data storage side simplistically taken care of. What about the object space? What would the objects look like? How would they be composed? To keep it easy to model and manipulate from a programmatic perspective, you could construct the objects as in the UML class diagram below. There you can see that a user object can have one or more address objects.
Taking this approach, we can maintain some database normalization, and also create objects which has a good respect for separation of concerns. However, whilst we’ve achieved a good data and class design, it’s not going to be too straight-forward to hydrate these objects.
So the question then is, how do we make object construction and hydration easy as well? Good question. To answer that, I suggest that one of the following approaches. Firstly, you could create a query on the user table, performing a right join on the two address tables, so that if the address wasn’t set, you’d still have user data.
The SQL query would look something like this:
SELECT u.id, u.first_name, u.last_name, u.email,
ha.street_number as home_street_number,
ha.street_name as home_street_name,
sa.street_number as shipping_street_number,
sa.street_name as shipping_street_name
FROM user u
RIGHT JOIN address ha ON (ha.id = u.home_address_id)
RIGHT JOIN address sa ON (sa.id = u.shipping_address_id)
Note: You’d have to alias the address columns appropriately so that in the aggregate result, it’s clear as to where each columns came from.
Alternatively, you could query on the user table, joining it on the link table to get the respective address ids. This would retrieve the core user details. With that information available, you could then invoke a method on an address TableGateway to retrieve the shipping and billing addresses, providing the user id to filter on.
If you took that approach, the underlying queries might look something more like this, where the second query is using a named placeholder:
-- Get the user data
SELECT u.id, u.first_name, u.last_name, u.email
FROM user u;
-- Get the address data
SELECT a.street_number, a.street_name
FROM address a
LEFT JOIN user_address ua ON (a.id = ua.address_id)
WHERE u.user_id = :userId
Let’s assume that the latter approach is the one you chose to take. How do you make the process as simple, maintainable, and testable as possible? Well, besides being a lofty set of goals, is it even achievable?
Until recently, I wasn’t sure it was. I’d been building everything separately, the long way, using the base three hydrators in a rather convoluted manner. That is, until the need pushed me to find a better solution.
The AggregateHydrator
The solution I came across was the AggregateHydrator. If you’ve not heard of it, quoting directly from the manual:
You typically want to use an aggregate hydrator when you want to hydrate or extract data from complex objects that implement multiple interfaces, and therefore need multiple hydrators to handle that in subsequent steps.
This description neatly describes the situation I found myself in. When I retrieved a user via its TableGateway, using a method such as findById
etc, I wanted the returned object to be fully hydrated with all the associated sub-objects already hydrated as well.
But, at the same time I didn’t want to pollute the concerns of the user TableGateway with knowledge or functionality of address details which it should be concerned of. So here’s how I approached it.
- Create two TableGateway classes, one for the user and one for the address
- Create a hydrator for the user and address objects
- Create a hydrator factory, which handles instantiation of the various hydrators and executes the aggregate hydration process
- Combine it all in the configuration of a TableGatewayAbstractFactory
Now you might be thinking that that’s a lot to cover in one post, and it is. So I hope you’re ready. Oh, before we go on too much further, I think it’s essential to explain why this is an approach worth pursuing.
You might be wondering why anyone would go to all this effort in the first place. Here’s why:
- Once done, the upfront investment of time and effort pays for itself in spades later
- It’s testable
- It’s easy to document
- It’s easy to maintain and extend
- Assuming you know your way around the ZF2 ServiceManager, it’s very transparent
Stick with me and you’ll see why soon.
The User Entity
The User entity is a simple entity object which has protected properties for all, or most, of the properties which you might expect to be available when working with a user. It also has an addresses property which is an SplObjectStorage object, which will contain two AddressEntity
objects. Setter and getter methods are available for each property.
Note: The reason for using an SplObjectStorage object is that it makes it easy to manage a list of objects using PHP’s native classes, instead of having to code yet another class to handle it.
<?php
namespace App\Entity;
class User
{
protected $id;
protected $firstName;
protected $lastName;
protected $email;
/** @var \SplObjectStorage $addresses */
protected $addresses;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getAddresses()
{
if (is_null($this->addresses)) {
$this->addresses = new addresses();
}
return $this->addresses;
}
public function setAddresses($addresses)
{
$this->addresses = $addresses;
}
public function setFirstName($firstName)
{
$this->firstName = $firstName;
}
public function setLastName($lastName)
{
$this->lastName = $lastName;
}
public function setEmail($email)
{
$this->email = $email;
}
// ...remaining methods
}
The Address Entity
As with the user entity, the address entity is a simple PHP class which has protected properties for the properties which it will store. It also has setters and getters for manipulating each one.
<?php
namespace App\Entity;
class Address
{
protected $streetNumber;
protected $streetName;
protected $city;
protected $state;
protected $postCode;
protected $country;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getStreetNumber()
{
return $this->streetNumber;
}
public function getStreetName()
{
return $this->streetName;
}
public function getCity()
{
return $this->city;
}
public function getState()
{
return $this->state;
}
public function getPostCode()
{
return $this->postCode;
}
public function getCountry()
{
return $this->country;
}
// ...remaining methods
}
The User Hydrator
Now for the user hydrator. This class extends Zend Framework’s AbstractHydrator, which requires that the hydrate method be implemented, which you can find the definition of in Zend\Hydrator\HydrationInterface
.
The method is given an array, called $data
, which is the data to hydrate the object with, and an object, $object
, which is the object to hydrate. I’ve implemented the method, first checking if the object supplied is an instance of User
. If it is, I’ve used the object’s setter methods to hydrate it with the data provided.
<?php
namespace App\Hydrator;
use Zend\Hydrator\AbstractHydrator;
use App\Entity\User;
class UserEntityHydrator extends AbstractHydrator
{
public function hydrate(array $data, $object)
{
if (!$object instanceof User) {
return $object;
}
if ($this->propertyAvailable('id', $data)) {
$object->setId($data['id']);
}
if ($this->propertyAvailable('firstName', $data)) {
$object->setFirstName($data['firstName']);
}
if ($this->propertyAvailable('lastName', $data)) {
$object->setLastName($data['lastName']);
}
return $object;
}
protected function propertyAvailable($property, $data)
{
return (array_key_exists($property, $data)
&& !empty($data[$property]));
}
}
The Address Hydrator
The UserAddressHydrator
is a little different, but similar. This one has a constructor dependency of an AddressTable
TableGateway object, which we’ll see later. During hydration, as with the User object, we first check if the object is a User
entity. If it is, we call the TableGateway object’s fetchByUserId
method, passing in the user id provided in $data
.
<?php
namespace App\Hydrator;
use App\Entity\User;
use App\TableGateway\AddressTable;
use Zend\Hydrator\HydratorInterface;
class UserAddressEntityHydrator implements HydratorInterface
{
protected $tableGateway;
public function __construct(AddressTable $tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function hydrate(array $data, $object)
{
if (!$object instanceof User) {
return $object;
}
if ($this->propertyAvailable('id', $data)) {
$object->setAddresses(
$this->tableGateway->fetchByUserId($data['id'])
);
}
return $object;
}
protected function propertyAvailable($property, $data)
{
return (array_key_exists($property, $data)
&& !empty($data[$property]));
}
}
The User Hydrator Factory
Now for one last hydrator, the UserHydratorFactory
. This is responsible for instantiating an aggregate hydrator which uses the UserEntity
and UserAddressEntity
hydrators to fully hydrate a UserEntity
.
It’s a callable by definition of the fact that it implements the __invoke magic method. This, just for the sakes of simplicity, calls a protected method prepareHydrator
, passing to it the AddressTable
TableGateway object which it was provided.
The prepareHydrator
method then instantiates a new AggregateHydrator
object, adding on to it a UserEntityHydrator
and a UserAddressEntityHydrator
. The result of the method is to return the AggregateHydrator
object.
<?php
namespace App\Hydrator;
use App\Hydrator;
use App\TableGateway\AddressTable;
use Zend\Hydrator\Aggregate\AggregateHydrator;
class UserHydratorFactory
{
protected $hydrator;
public function __invoke(AddressTable $address)
{
return $this->prepareHydrator($address);
}
protected function prepareHydrator(AddressTable $address)
{
$this->hydrator = new AggregateHydrator();
$userHydrator = new Hydrator\UserEntityHydrator();
$this->hydrator->add($userHydrator);
$this->hydrator->add(
new Hydrator\UserAddressEntityHydrator($address)
);
return $this->hydrator;
}
}
Dependency Configuration
Now that we have all of the classes created, we need to ensure that they’re registered with the ServiceLocator. If you’re using Zend Expressive then in config/autoload/dependencies.global.php
, ensure you have a reference in the invokables
array, as I have below.
<?php
return [
'dependencies' => [
'invokables' => [
App\Hydrator\UserHydratorFactory::class
=> App\Hydrator\UserHydratorFactory::class,
]
]
];
The code for this article was developed using Zend Expressive. But if you’re working with Zend Framework 2, then add the line above in config/autoload/global.php
similar to what you see below.
<?php
return [
'service_manager' => [
'invokables' => [
App\Hydrator\UserHydratorFactory::class
=> App\Hydrator\UserHydratorFactory::class,
]
],
];
## ServiceManager Configuration
I’ve covered how to do ServiceManager configuration in a previous post. So if you’re not familiar with the process, definitely check out that post, as you’ll need it to follow along with this section.
That being said, we first retrieve a UserHydratorFactory
service from the ServiceLocator, passing an AddressTable
service to it when calling its __invoke
method. The rest of the instantiation is the same as I’ve covered previously. A new HydratingResultSet object is initialized, passing in the AggregateHydrator object and a rowObjectPrototype, which is a UserEntity. This is then passed to the initialization of the UserTableGateway object as the third parameter.
case ('App\TableGateway\UserTableGateway'):
$hydratorFactory = $serviceLocator->get(
'App\Hydrator\UserHydratorFactory'
);
$hydrator = $hydratorFactory(
$serviceLocator->get('App\TableGateway\AddressTable')
);
$rowObjectPrototype = new UserEntity();
$resultSet = new HydratingResultSet(
$hydrator,
$rowObjectPrototype
);
$tableGateway = new TableGateway(
'tbluser', $dbAdapter, $resultSet
);
break;
Now, when we retrieve data from tbluser
the objects returned will be transparently hydrated with any linked addresses for us. We will never need to do it manually anywhere else in our codebase.
Over To You
And that’s how to use an AggregateHydrator to perform sophisticated, even complex hydration of objects in your Zend Framework applications. Sure, there’s a bit to think about, and a number of moving parts. However - when done correctly, hydration is all done for us, in one central, testable, configurable location. All of the objects are testable with proper separation of concerns.
Have you used AggregateHydrators before? What’s been your experience? Could you see yourself using them?
CC Image (background of main image) Courtesy of Becky Lai on Flickr
Join the discussion
comments powered by Disqus