Entity Hydration Done Right

I was recently given a tutorial by @odino on how to use doctrine annotation reader along with PHP’s Magic Methods to dynamically hydrate entities and bind raw data coming from a data source to custom entities.

We basically have a data source (say, a key value store) which provides data (which we don’t have control over) that might contain irrelevant information or its keys might not be declarative enough. So we need a generic and clean way to perform filtering, transform key names and “hydrate” our entities. By clean I mean, loops and array manipulation should be kept to a minimum.

Say we have the following data coming from our data source:

1
2
p1 => 'yellow',
p2 => '15'

From the point of view of our data source, p1 refers to the color and p2, let’s say, height variable.

What we aim to is having a class of an imaginary tool which contains properties describing its color and height as follows:–

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

Class Tool
{
  protected color;

  protected height;

  public function setColor($color)
  {
      $this->color = $color;
  }

  public function setHeight($height)
  {
      $this->height = $height;
  }

  public function getColor()
  {
      return $this->color;
  }

  public function getHeight()
  {
      return $this->height;
  }
}

Now, to set the properties of this entity, it would be easy to do something like this:–

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

Class EntityHydrator
{
  public function Hydrate(array $data)
  {
      $tool = new Tool();
      $tool->setColor($data['p1']);
      $tool->setHeight($data['p2']);

      return $tool;
  }
}

But what if we have tens or maybe hundreds of variables similar to p1 and p2? Will it be feasible to set the $tool object properties one by one?

Here comes our first piece of the puzzle, PHP’s Magic Methods!

We can have something like the following in the entity class instead of the getters and setters:–

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
use Doctrine\Common\Util\Inflector;

Class Tool
{
    protected $color;
    protected $height;

    //Magic Method
    public function __call($method, $value)
    {
        if (substr($method, 0, 3) == 'set') {
            $property  = Inflector::camelize(substr($method, 3));

            if(property_exists($this, $property)) {
                if(isset($value[0])){
                    $this->$property = $value[0];
                } else {
                    $this->$property = null;
                }
            }
        } else if(substr($method, 0, 3) == 'get') {
            $property  = Inflector::camelize(substr($method, 3));

            if(property_exists($this, $property)) {
                return $this->$property;
            }

            return null;
        }
    }

The previous implementation of the getters and setters makes use of the Doctrine Inflector Class to extract the proper property name from the passed method.

Now we have achieved the following goals: if some expected values are missing from the data source response, they will be set to null without breaking the program. Also, we can loop through the data and dynamically call the set methods. Moreover, we can actively filter unneeded properties by just not including them in the entity class!

So, what’s next?

If you’re following well, you will notice that our main issue, which is mapping different keys to different properties, has not been solved yet. At this point, Doctrine’s Annotation Reader can kick into action.

How to do it?

Firstly, create a class which holds the annotation names you wish to use:–

1
2
3
4
5
6
7
8
9
<?php

/**
 * @Annotation
 * @Target({"PROPERTY"})
 */
Class Property
{
    protected $name;

The annotations in the comment box will later tell the annotations reader that this class contains annotation names and it is targeted for properties.

Secondly, let’s go back and modify our Tool class to add the annotations!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

use Doctrine\Common\Util\Inflector;
use Path\To\Property\Class as OAM;

Class Tool
{
     /**
     * @OAM\Property(name="p1")
     */
    protected $color;

     /**
     * @OAM\Property(name="p2")
     */
    protected $height;

    public function __call($method, $value)
    {
        if (substr($method, 0, 3) == 'set') {
            $property  = Inflector::camelize(substr($method, 3));

            if(property_exists($this, $property)) {
                if(isset($value[0])){
                    $this->$property = $value[0];
                } else {
                    $this->$property = null;
                }
            }
        } else if(substr($method, 0, 3) == 'get') {
            $property  = Inflector::camelize(substr($method, 3));

            if(property_exists($this, $property)) {
                return $this->$property;
            }

            return null;
        }
    }

Now, let’s go back to our EntityHydrator Class, create the Doctrine Annotations Reader and the PHP Reflection Object to read the available properties in our entity and provide it to the annotation reader that will read off the annotations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

use ReflectionObject;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Util\Inflector;

Class EntityHydrator
{
  public function hydrate($entityClass, array $data)
  {
      $object = new $entityClass;
      $refObj = new ReflectionObject($object);
      $reader = new Reader();

      foreach ($refObj->getProperties() as $key => $property) {
          //Dynamically create the setter method name depending on the current property name
          $setter     = sprintf('set%s', ucfirst(Inflector::camelize($property->getName())));
          //Read of the specified annotations in the Property Class, which is just $name in our example
          $annotation = $reader->getPropertyAnnotation($property, 'Path\To\Property\Class');
          //The contents of the (name) annotation will be used as a key to the data array received from the data source!
          $object->$setter($data[Inflector::tabelize($annotation->name)]);
      }

      return $object;
  }
}

This is it! Now in your main class you can call the EntityHydrator Class and pass your rawData to it!

1
2
3
4
5
6
7
8
9
10
<?php

Class main
{
    public function handleRawData(array $rawData)
    {
        $hydrator = new EntityHydrator();
        $tool     = hydrator->hydrate('Tool', $rawData);
    }
}

Comments