Skip to content

Hydrators

Bram Gotink edited this page Jul 5, 2014 · 2 revisions
This page applies to branch dev-form.

This page talks about hydrators, how we implement them and how to use them.

What are hydrators?

Hydrators are objects that hydrate objects with data found in an array, or extracts the data from an object into an array.

ZF2 contains a couple of default hydrators, e.g. Zend\Hydrator\ClassMethods. This hydrator can hydrate any non-null object, and works using methods.

Hydrator example

Suppose you have an Entity $object, and a data array containing the following data:

$arr = array(
    'active' => true,
    'name' => 'MyEntity',
);

The Entity class looks like this:

class Entity
{
    public $active;
    public $entityName;
    public $public;

    public function setEntityName($name) { $this->name = $name; }
    public function getEntityName() { return $this->name; }

    public function setActive($active) { $this->active = !!$active'; }
    public function isActive() { return $this->active; }

    public function setPublic($public) { $this->public = !!$public; }
    public function isPublic() { return $this->public; }
}

If you use ClassMethods, calling

$hydrator->hydrate($arr, $object);

will result in

$object->setActive($arr['active']);
$object->setEntityName($arr['entity_name']);

and

$arr = $hydrator->extract($object);

is the same as

$arr = array(
    'active'      => $object->isActive(),
    'entity_name' => $object->getEntityName(),
    'public'      => $object->isPublic(),
);

ZF2 provides other hydrators as well, e.g. one that uses variables directly:

$hydrator->hydrate($arr, $object);
$arr = $hydrator->extract($object);

would become

$object->active = $arr['active'];
$object->entityName = $arr['entity_name'];

$arr = array(
    'active'      => $object->active,
    'entity_name' => $object->entityName,
    'public'      => $object->public,
);

As you can (hopefully) imagine, we will use the ClassMethods hydrator and leave the others alone.

ZF2 Hydrators

ZF2's hydrators expose two important functions:

interface HydratorInterface
{
    /**
     * @param array $data
     * @param object $object
     * @return object
     */
    public function hydrate(array $data, $object);

    /**
     * @param object $object
     * @return array
     */
    public function extract($object);
}

You will notice that these methods don't support null objects. This is logical, as the hydrator wouldn't know what kind of object to create.

Our Hydrators

Our hydrators work slightly different. We will create a hydrator for every entity type, so we can create a new object. Our hydrators will support null objects by default. This can still be disabled though, e.g. because some entities require the Person object of the user who created the instance (which is an object the Hydrator cannot get to, but the Controller can).

ZF2 hydrators don't expect a certain type, but our hydrators will only support one class of entities.

CommonBundle\Component\Hydrator\Hydrator is the superclass of all our hydrators. It implements the hydrate and extract methods above to add type checking. Two new abstract methods are added that need to be implemented by the subclasses. These have the exact same interface as hydrate and extract, but they're guaranteed that the $object is of the right type. The Hydrator superclass also exposes two handy methods to call the ClassMethods hydrator, which can be used by the subclasses.

All in all, the class looks like this:

class Hydrator implements HydratorInterface
{
    /** @var string|null */
    protected $entity = false;

    public function __construct() {
        // see subsection __construct()
    }

    // see subsection extracting
    public function extract($object = null) { ... }
    abstract protected function doExtract($object = null);

    // see subsection hydrating
    public function hydrate(array $data, $object = null) { ... }
    abstract protected function doHydrate(array $data, $object = null);

    // see subsection helpers
    protected function stdExtract($object, array $keys = null);
    protected function stdHydrate(array $data, $object, array $keys = null);
}

__construct()

The value for $this->entity is the entity class this hydrator supports. As you can see, it is set to false. That's because the constructor will fill it in. If the value is still false when the constructor is called, it will try to fill in the entity class. If the Hydrator is located in

MyBundle\Hydrator\Path\To\Class

it will assume the entity is

MyBundle\Entity\Path\To\Class

You can prevent this behaviour by explicitly setting $entity in the subclass:

class MyHydrator extends Hydrator
{
    protected $entity = 'MyBundle\Entity\Path\To\Entity';
    // ...
}

An example of where this must be used: CommonBundle\Entity\General\Address has two ways of being hydrated: the CommonBundle\Hydrator\General\PrimaryAddress works using the known streets in the database, while CommonBundle\Hydrator\General\Address simply loads the data from a form.

You can also disable typechecking by setting $entity to null.

extracting

The extracting methods support null values because sometimes an empty object still has some default values set. These defaults can be returned here. Most hydrators will simply have if ($object === null) { return array(); } at the start of doExtract.

Ensure that doExtract always returns a non-null array!

hydrating

If your hydrator doesn't support creating new objects, have it throw a CommonBundle\Component\Hydrator\Exception\InvalidObjectException. Otherwise, make sure to create an entity instance if $object is null.

While you can assume that the data array is properly formatted (e.g. a date is in d#m#Y H#i), it is probably best not to assume anything about the data!

helpers

The helper methods are merely interfaces for the ClassMethods hydrator provided by ZF2. It supports an extra parameter however. This parameter tells the hydrator which variables to hydrate/extract. All other data will be ignored.

Suppose we have an object of the Entity class used above. Suppose that all objects have $active = true at the start. The $active property must not be changed by the hydrator, a separate method is used for that. The hydrator will look like this:

namespace MyBundle\Hydrator;

use MyBundle\Entity\Entity as EntityEntity;

class Entity extends \CommonBundle\Component\Hydrator\Hydrator
{
    private static $std_keys = array('entity_name', 'public');

    protected function doExtract($object = null)
    {
        if (null === $object) return array();

        return $this->stdExtract($object, array(self::$std_keys, 'active'));
    }

    protected function doHydrate(array $array, $object = null)
    {
        if (null === $object) {
            $object = new EntityEntity();
            $object->setActive(true);
        }

        return $this->stdHydrate($array, $object, self::$std_keys);
    }
}

As you can see, the last parameter for stdHydrate and stdExtract can be any array such that a flattened version of the array contains only strings. Please note that doExtract could also have used return $this->stdExtract($object);, because all data is extracted.

Using a hydrator

Hydrators are used in the forms. A form specifies which hydrator to use. This defaults to ClassMethods, but can be any class implementing Zend\Hydrator\HydratorInterface. To define which class to use, write your form like this:

class MyForm extends \CommonBundle\Component\Form\Admin\Form
{
    protected $hydrator = 'MyBundle\Hydrator\Path\To\Hydrator';
}

If you bind an object to the form, the data will automatically be extracted from it using the hydrator to populate the form, and the data entered by the user will automatically be hydrated into the object. You could use:

$form = // get a form
$form->bind($object);
$form->setData($myData); // <-- this line will modify $object!!
$entityManager->flush();

If you need to create a new object, you can use Form::hydrateObject:

$form = // get a form
$form->setData($myData);
$object = $form->hydrateObject();
$entityManager->persist($object);
$entityManager->flush();

You can still use Form::hydrateObject if the hydrator doesn't support creating a new object:

$form = // get a form
$object = new Entity($this->getAuthentication()->getPersonObject()); // e.g. an entity that requires the Person object

$form->setData($myData);
$form->hydrateObject($object);

$entityManager->persist($object);
$entityManager->flush();
Clone this wiki locally