Know your components: A simple serializer with Symfony’s property access

Recetly, I needed to implement a quite simple serializer that would be used to prepare the entities from the database for indexing in ElasticSearch. I didn’t really need to get JSON or XML out of it, I only needed to transform my objects into arrays.
Instead of hacking around Symfony’s built-in Serializer or JMSSerializer (I’m not sure whether it’s simple to get just the data array out of those), I’ve decided I can do it in a lot more simple way just with PropertyAccess.

PropertyAccess is a base Symfony component, commonly used in forms, but there’s nothing limiting its usage in different contexts. The whole aim of the component is to allow an easy way to read and write from and to objects and arrays. Its API is quite simple and straightforward:

use Symfony\Component\PropertyAccess\PropertyAccess;

$accessor = PropertyAccess::createPropertyAccessor();
$hello = array(
    'greeted' => 'World',
);

var_dump($accessor->getValue($hello, '[greeted]')); // string(5) "World"

$accessor->setValue($hello, '[greeted]', 'People');

var_dump($hello['greeted'); // string(6) "People"

As we can see here, we first create an accessor with a constructor named ::createPropertyAccessor(), and that’s mostly it: we’re ready to go. We can write values to and from an array, and we can work with objects, too:

use Symfony\Component\PropertyAccess\PropertyAccess;

$accessor = PropertyAccess::createPropertyAccessor();
$hello = new Hello();
$hello->setGreeted('Mike');

var_dump($accessor->getValue($hello, 'greeted')); // string(4) "Mike"

$accessor->setValue($hello, 'greeted', 'John');

var_dump($hello['greeted'); // string(4) "John"

So, what it does here is it guesses whether the properties we want can be accessed using getters or setters, and also it plays with has/is methods nicely.

Where does this get us? Well, I’ve ended up implementing this simple serializer (or, actually, a flattener, as it’s a lot more common to use flat arrays with ElasticSearch). Here it is, with my comments here and there:

<?php

namespace Acme\Elastica\Flattener;

class PropertyAccessFlattener 
{
    /**
     * @var array
     */
    private $rules;

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

    /**
     * @param string $className
     * @param array  $rules
     */
    public function __construct($className, array $rules)
    {
        $this->rules = $rules;
        $this->className = $className;
    }

What happens here is we accept a $className and a $config array for the model class and the way to serialize it, respectively.

    public function transform($entity)
    {
        $entityClass = get_class($entity);

        if ($entityClass !== $this->className) {
            throw new \InvalidArgumentException(sprintf(
                'Unsupported entity of class "%s" passed while only "%s" is supported',
                $entityClass,
                $this->className
            ));
        }

And of course, since we only support only a specific class, we should not accept anything else that does not match.

        $propertyAccess = new PropertyAccessor(true, true);
        $result = [];

        foreach ($this->rules as $targetPath => $sourcePath) {
            $value = $propertyAccess->getValue(
                $entity, 
                $sourcePath
            );

            $propertyAccess->setValue(
                $result,
                $targetPath,
                $value
            );
        }

        return $result;
    }
}

And now, the PropertyAccess magic comes in. This is actually all we need to do here. Read a value, put it into $result.

As you might have already guessed, the vital part here is the $rules array that we pass as a second argument to the constructor. What should it look like? Well, here’s the YML configuration for a typical Transformer:

acme.simple_transformer:
        class: Acme\Elastica\Flattener\PropertyAccessTransformer
        arguments:
          - Acme\AppBundle\Entity\Person
          -
            '[firstName]': 'firstName'
            '[lastName]': 'lastName'
            '[city]': 'address.city'

Note the second argument’s syntax: we need to pass an array as the second argument, and that’s how it’s done in YAML. Once this is done, we’re good to go:

$person = $personRepository->find(1);
$transformer = $container->get('acme.simple_transformer');
$result = $transformer->transform($person);

And guess what, the '[city]': 'address.city' line does all the magic for us. Since we’re using PropertyAccess, it guesses that address.city is a PropertyPath. Dots in it mean properties on nested objects, so here we get a person’s Address and get the city property from it. This is what we would get with $person->getAddress()->getCity().

So, you should never underestimate the power of Symfony components: some things are there that you might not be aware about yet, but take a good look at Symfony’s documentation on components and you might find a lot of things that can deal with a lot of common problems.

 
11
Kudos
 
11
Kudos

Now read this

Why Propel is bad

There’s always the need to abstract out our data persistence mechanisms so that we don’t have to mess with mapping user data to tables by hand and handling the relations between user data entities. I’ve been always using Doctrine ORM in... Continue →