Довольно часто нам приходится сталкиваться с ситуацией, когда введенные пользователям данные необходимо привести к какому либо другому виду. Конечно можно вручную преобразовать их в контроллере. А что если эти данные вам понадобятся в еще одной форме в другом месте в приложении?
Допустим вы используете связь один к одному между сущностями 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 типа данных, которые они обрабатывают.
Три типа данных:
- Данные модели — данные в том виде, в котором они используются в вашем приложении (то есть объект класса
Issue
). При вызовеForm::getDatа
илиForm::setData
вы имеет дело с таким видом данных. - Нормализованные данные — название говорит само за себя, это данные, в таком же виде, как и ваша модель. Такой вид данных встречается довольно редко.
- Данные вида — такой формат используется для наполнения полей формы. Также пользователь отправляет данные именно в этом формате. С этим видом вы встречаетесь при вызове
Form::submit($data)
.
Вышеупомянутые виды преобразователей переводят между этими тремя типами.
Преобразователи модели:
- transform: данные модели => нормализованные данные
- reverseTransform: нормализованные данные => данные модели
Преобразователи вида:
- transform: нормализованные данные => данные вида
- reverseTransform: данные вида => нормализованные данные
Для использования преобразователей вида, обращайтесь к addViewTransformer
.
Разница между преобразователями не всегда лежит на поверхности. Старайтесь четко определить что для вашего приложения является нормализованным видом данных. Например для текстового поля ввода нормализованным видом является строка, а для поля даты — объект DateTime
.
Применение преобразователей в дополнительном поле формы
В вышеописанном примере мы использовали преобразователь на стандартном текстовом поле ввода. Такой подход довольно прост, но таит в себе некоторые недостатки:
- Вам всегда надо использовать преобразователь при построении формы.
- Вам всегда следует передавать
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';
}
}