-
Notifications
You must be signed in to change notification settings - Fork 5
Hydrators
This page applies to branch dev-form.
This page talks about hydrators, how we implement them and how to use them.
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.
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'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 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);
}
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
.
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!
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!
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.
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();