What is and how to create a data transformer in Symfony2

Unlikes its predecessor (Symfony 1.4) in the insertion of an entity in your database with Doctrine, if your entity had one to many relations , you could easily create it with a couple of lines :

Given a one to many relation between a role and a user :

$user = new User(); 
$user->setName('Joe'); 
$user->setAge(28); 
$user->setRoleId(1); 
$user->save();

But since symfony 2, if you try to do this :

$em = $this->getDoctrine()->getManager(); 
$user = new User(); 
$user->setName('Joe'); 
$user->setAge(28); 
$user->setRole(1); 
$em->persist($user); 
$em->flush();

An error will be thrown because your setRole method is tryng to save only a number instead a Role Entity , you will need to insert an original role object into your user entity

$em = $this->getDoctrine()->getManager(); 
$roleEntity = $em->getRepository('ourcodeworldBundle:Roles')->find(1); 
$user = new User(); 
$user->setName('Joe'); 
$user->setAge(28); 
$user->setRole($roleEntity);
 $em->persist($user); 
$em->flush();

So easy the object will be persisted succesfully.

Using Datatransformers

Now , the datatransformers are used to translate from entity to form widgets, think about an entity Product with a one to many relation, if you create a CRUD  a  select widget will be created to select the role for the product in the form.

But what about if there are more than 200 categories in the select ? uncomfortable way to select the category isn't ? You are encouraged to design an easy method to select the category with a simple number that the user knows.

Here is when the datatransformers enter in action, they will translate the number into an entity and the entity into a number automatically.

If you don't use data transformer you are likely to see these errors when a CRUD is created from an existing database with one to many relations :

Catchable Fatal Error: Argument 1 passed to xxxxxxxxxxxxx::setRole() must           be an instance of xxxxxxxxxxxxxxx\Role, string given, called in xxxxxxxxx on line xxxx and defined in xxxxxx line 
The form's view data is expected to be an instance of class xxxxxxxxxxxxx  but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of xxxxxxxxxxx

1) Create the transformer in the a new folder called DataTransformer into the Form folder of the bundle

Create a php file in the form folder of the entity and insert the following code

// src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace AppBundle\Form\DataTransformer;

use AppBundle\Entity\Issue;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class IssueToNumberTransformer implements DataTransformerInterface
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return '';
        }

        return $issue->getId();
    }

    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $issueNumber
     * @return Issue|null
     * @throws TransformationFailedException if object (issue) is not found.
     */
    public function reverseTransform($issueNumber)
    {
        // no issue number? It's optional, so that's ok
        if (!$issueNumber) {
            return;
        }

        $issue = $this->manager
            ->getRepository('AppBundle:Issue')
            // query for the issue with this id
            ->find($issueNumber)
        ;

        if (null === $issue) {
            // causes a validation error
            // this message is not shown to the user
            // see the invalid_message option
            throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $issueNumber
            ));
        }

        return $issue;
    }
}

Change the obvious parameters that needs to be replaced like the bundle, the namespace and the property you need to replace everytime the transformer is executed.

2) Add the datatransformer to the Form builder of our entity

We need to apply the previous datatransformer in the form builder (located outside of the folder we have created)

// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use AppBundle\Form\DataTransformer\IssueToNumberTransformer; // We include the datatransformer created previously
use Doctrine\Common\Persistence\EntityManager;

// ...
class TaskType extends AbstractType
{
    private $entityManager;

    public function __construct(EntityManager $entityManager) // Create the constructor if not exist and add the entity manager as first parameter (we will add it later)
    {
        $this->entityManager = $entityManager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('description', 'textarea')
            ->add('issue', 'text', array(
                // validation message if the data transformer fails
                'invalid_message' => 'That is not a valid issue number',
            ));

        // ...

        $builder->get('issue')
            ->addModelTransformer(new IssueToNumberTransformer($this->entityManager)); // finally we apply the transformer
    }

    // ...
}

3) We send the required EntityManager $entityManager in the controller to the taskType

Is necesary to provide the entity manager in the datatransformer (the doctrine manager) which will be passed as first argument in our declaration of formType in the controller (newAction , editAction)

// e.g. in a controller somewhere
$manager = $this->getDoctrine()->getManager();
$form = $this->createForm(new TaskType($manager), $task);

Of this way our long select with more than 200 options will be replaced with a text input in which the user will type only the id (or your custom identificator) of the role instead load 200 rows of our database.

Become a more social person