Using Data Transformers in Symfony2

Forms - one of the most powerful tools in Symfony2, they represent many features. Many secrets to working with Symfony2 are described in the Recet Book . I want to introduce you the translation of one recipe for working with forms, in Symfony 2 - the use of date transformers .
Often there is a need to convert the data entered by the user into the form in a different format for use in your program. You can easily do this manually in the controller, but what if you want to use this form in different places? Let's say you have a “Task” object associated with a code-to-one relationship with an “Issue” (problem) object, for each “Task” an “Issue” can be specified, which it solves. If we add a drop-down list of Issue problems to the task editing form, then it will be very difficult for us to navigate it. You can add a text field instead of a drop-down list and simply enter the Issue number.
You can try to do the conversion in the controller, but this is not the best idea. It would be much better if the Issue number automatically converted the Issue object. In this case, “Data Transformers” come into play.

Creation of transformers.


First, create an IssueToNumberTransformer class - this class will be responsible for converting from the Issue number to the Issue object:
// src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace Acme\TaskBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;
class IssueToNumberTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;
    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }
    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return "";
        }
        return $issue->getNumber();
    }
    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $number
     * @return Issue|null
     * @throws TransformationFailedException if object (issue) is not found.
     */
    public function reverseTransform($number)
    {
        if (!$number) {
            return null;
        }
        $issue = $this->om
            ->getRepository('AcmeTaskBundle:Issue')
            ->findOneBy(array('number' => $number))
        ;
        if (null === $issue) {
            throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $number
            ));
        }
        return $issue;
    }
}

You can create a new “Issue” object when the user has entered an unknown number and not throw a TransformationFailedException.

Using Transformers


Now we have a transformer, you just need to add it to our “Issue” field in one form or another.
use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ...
        // this assumes that the entity manager was passed in as an option
        $entityManager = $options['em'];
        $transformer = new IssueToNumberTransformer($entityManager);
        // add a normal text field, but add our transformer to it
        $builder->add(
            $builder->create('issue', 'text')
                ->addModelTransformer($transformer)
        );
    }
    // ...
}


In this example, you must pass the EntityManager as an option when creating the form. Later you will learn how to create a custom field for the Issue number to avoid having to pass an EntityManager.
$taskForm = $this->createForm(new TaskType(), $task, array(
    'em' => $this->getDoctrine()->getEntityManager(),
));


Cool, we did it! Now the user will be able to enter the number in the text box, and it will be converted to the object "Issue". This means that after successful binding ($ form-> bindRequest ($ request)), the form framework will provide the actual “Issue” object to the :: setIssue () method instead of the “Issue” number.
Note that when adding a transformer, you need to use a slightly more complex syntax than when adding a field. The following is not a correct example of how a transformer will be applied to the entire form, and not to a specific field:
// Это не правильно - трансформер будет применен ко всей форме
// смотрите пример выше для правильного использования трансформера
$builder->add('issue', 'text')
    ->addModelTransformer($transformer);


Model view and transformers


New in version 2.1: the name of the transformer methods has been changed in Symfony 2.1. prependNormTransformer became addModelTransformer and appendClientTransformer became addViewTransformer.
In the above example, a transformer was used as a “model transformer”. In fact, there are two different types of transformers and three different types of input data.
In any form, there are three different types of data:

  1. Model data is data in the format that is used in your application (for example, for example, an object of type "Issue"). If you call the :: GetData or :: SetData methods in the form, you will be dealing with “Model data”.
  2. Norm Data are normalized versions of your data, they are usually the same as “Model data” data (although not in our example). Directly, they are not often used.
  3. View Data is a format that is used to populate form fields. This is also the format in which the user will transmit data (submit form). When we call the Form :: bind ($ data) method, $ data is presented in the "View Data" format

There are two different types of transformers that help us transform data from one view to another.
  • "Model transformer" model transformers
    • transform: "model data" => "norm data"
    • reverseTransform: "norm data" => "model data"

  • "View transformer" view transformers:
    • transform: "norm data" => "view data"
    • reverseTransform: "view data" => "norm data"


Which transformer you need depends on the specific situation.
To use View Transformer, call the addViewTransformer method.

So why do we need to use data transformers?


In our example, the field is a text field, and we expect the text field to always return scalar data in "norm" and "view" formats. And in this case, the most acceptable transformer is “model transformer”, which converts “norm data” into “model data” and vice versa (Issue number into an “Isuuse” object and vice versa).
The difference between the transformers is very subtle and you should always think that normalized data should be “norm”. For example, for the “norm” text field, normalized data is a text string, and for the “date” field, a DateTime object.

The use of transformers in "custom fields"


In the example that we described above, we use a transformer for the text field. This was pretty easy to do, but this approach has two drawbacks:
  1. You should always remember when using a transformer when using the “isuue” field
  2. You should take care to pass in the em => EntityManager option whenever you create a form that uses a transformer.

Therefore, we probably need to create a custom "custom" type of field. First, create a class for the custom field type:
// src/Acme/TaskBundle/Form/Type/IssueSelectorType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IssueSelectorType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $om;
    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new IssueToNumberTransformer($this->om);
        $builder->addModelTransformer($transformer);
    }
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'invalid_message' => 'The selected issue does not exist',
        ));
    }
    public function getParent()
    {
        return 'text';
    }
    public function getName()
    {
        return 'issue_selector';
    }
}


Next, register your service type and mark it with the form.type tag so that the field is recognized as a user type:


Now we can use our special issue_selector type:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'));
            ->add('issue', 'issue_selector');
    }
    public function getName()
    {
        return 'task';
    }
}

Also popular now: