Обзор компонентов Symfony2 : Применение преобразователя данных

Довольно часто нам приходится сталкиваться с ситуацией, когда введенные пользователям данные необходимо привести к какому либо другому виду. Конечно можно вручную преобразовать их в контроллере. А что если эти данные вам понадобятся в еще одной форме в другом месте в приложении?

Допустим вы используете связь один к одному между сущностями Task (задача) и Issue (проблема), т.е. задача может иметь проблему, а может и не иметь. Если вы просто добавите выпадающий список с полным набором возможных проблем, то отыскать в нем нужную вам будет практически невозможно. А можете создать текстовое поле, в которое пользователь будет вводить номер проблемы.

Попробуйте реализовать такую логику в своем контроллере. Думаю, довольно быстро вы почувствуете все недостатки подобного решения. Было бы намного проще, если бы эта задача автоматически преобразовывалась бы в объект типа Task. Именно тут то вам и понадобится преобразование данных (Data Transformers).

Создание преобразователя

Для начала создайте класс IssueToNumberTransformer — этот класс будет отвечать за преобразование объекта Issue в число и обратно.

<?php
// 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;
   }
}

Вы можете создать новую сущность Issue, когда обрабатываете незнакомое число, вместо того, чтобы бросать исключение (TransformationFailedException).

Если в метод transform() передать null, то он должен вернуть эквивалентное значение типа, к которому вы приводите число (например пустая строка или 0 для целочисленных значений, 0.0 для чисел с плавающей точкой).

Применение преобразователей

Итак, мы создали преобразователь. Осталось найти ему применение в форме, для сущности Issue.

Вам вовсе необязательное создавать отдельный тип формы для этого, достаточно вызвать addModelTransformer или addViewTransformer для любого из полей формы:

<?php
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 your transformer to it
       $builder->add(
           $builder
               ->create('issue', 'text')
               ->addModelTransformer($transformer)
       );
   }
   public function setDefaultOptions(OptionsResolverInterface $resolver)
   {
       $resolver
           ->setDefaults(array(
               'data_class' => 'Acme\TaskBundle\Entity\Task',
           ))
           ->setRequired(array(
               'em',
           ))
           ->setAllowedTypes(array(
               'em' => 'Doctrine\Common\Persistence\ObjectManager',
           ))
       ;
       // ...
   }
   // ...
}

В этом примере мы должны передать Entity Manager в качестве параметра во время создания формы. Позднее мы рассмотрим как создать отдельное поле issue, чтобы избежать подобного поведения в контроллере.

<?php
$taskForm = $this->createForm(new TaskType(), $task, array(
   'em' => $this->getDoctrine()->getManager(),
));

Отлично, все работает! Достаточно пользователю будет ввести номер проблемы (issue) в текстовое поле, как этот номер будет преобразован в объект Issue. Это означает, что после удачной обработки формы, фреймворк Form передаст объект Issue методу Task::setIssue(), а не число.

Если же объект Issue не будет найден, то будет вызвана ошибка формы, которую вы сможете показать пользователю при помощи опции invalid_message.

Обратите внимание, что при добавлении преобразователя, приходится использовать более сложный синтаксис для добавления поля. Следующей пример неверен. В таком случае преобразователь будет применен ко всей форме, а не к одному полю.

<?php
// THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
// see above example for correct code
$builder
   ->add('issue', 'text')
   ->addModelTransformer($transformer)
;

Преобразователи модели и вида

В предыдущем примере преобразователь был применен на модели. На самом деле существует 2 вида преобразователей и 3 типа данных, которые они обрабатывают.

Три типа данных:

  1. Данные модели — данные в том виде, в котором они используются в вашем приложении (то есть объект класса Issue). При вызове Form::getDatа или Form::setData вы имеет дело с таким видом данных.
  2. Нормализованные данные — название говорит само за себя, это данные, в таком же виде, как и ваша модель. Такой вид данных встречается довольно редко.
  3. Данные вида — такой формат используется для наполнения полей формы. Также пользователь отправляет данные именно в этом формате. С этим видом вы встречаетесь при вызове Form::submit($data).

Вышеупомянутые виды преобразователей переводят между этими тремя типами.

Преобразователи модели:

  • transform: данные модели => нормализованные данные
  • reverseTransform: нормализованные данные => данные модели

Преобразователи вида:

  • transform: нормализованные данные => данные вида
  • reverseTransform: данные вида => нормализованные данные

Для использования преобразователей вида, обращайтесь к addViewTransformer.

Разница между преобразователями не всегда лежит на поверхности. Старайтесь четко определить что для вашего приложения является нормализованным видом данных. Например для текстового поля ввода нормализованным видом является строка, а для поля даты — объект DateTime.

Применение преобразователей в дополнительном поле формы

В вышеописанном примере мы использовали преобразователь на стандартном текстовом поле ввода. Такой подход довольно прост, но таит в себе некоторые недостатки:

  1. Вам всегда надо использовать преобразователь при построении формы.
  2. Вам всегда следует передавать Entity Manager при работе с формой с преобразователями.

Этих проблем можно избежать при использовании дополнительного поля. Для начала создадим класс нашего нового поля формы:

<?php
// 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';
   }
}

Затем, зарегистрируйте поле в качестве службы, добавив тег form.type. Таким образом эта служба будет восприниматься как поле формы:

services:
   acme_demo.type.issue_selector:
       class: Acme\TaskBundle\Form\Type\IssueSelectorType
       arguments: ["@doctrine.orm.entity_manager"]
       tags:
           - { name: form.type, alias: issue_selector }

Применить это поле можно следующим образом:

<?php
// 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';
   }
}