Как использовать преобразователи данных¶
Преобразователи данных используются для перевода данных для поля в формат, который
может быть отображён в форме (и обратно при отправке). Они уже используются внутренне
для многих типов полей. Например, поле DateType
может быть отображено в виде текстового ввода формата yyyy-MM-dd
. Внутренне, преобразователь
данных конвертирует начальное значение поля DateTime
в строку yyyy-MM-dd
, чтобы
отобразить форму, а потом обратно в объект DateTime
при отправке.
Caution
Если в поле формы установлена опция inherit_data
, преобразователи данных
не будут применены к такому полю.
Простой пример: Преобразование строковых тегов из ввода пользователя в массив¶
Представьте, что у вас есть форма задачи (task) с типом тегов text
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Task;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('tags', TextType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Task::class,
));
}
// ...
}
|
Внутренне, tags
хранятся в этом массиве, но отображаются пользователю, как обычная
строка, разделяемая запятой, чтобы облегчить их редактирование.
Это идеальное время, чтобы присоединить пользовательский преобразователь данных к
полю tags
. Легчевсего это сделать с помощью класса CallbackTransformer
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('tags', TextType::class);
$builder->get('tags')
->addModelTransformer(new CallbackTransformer(
function ($tagsAsArray) {
// преобразовать массив в строку
return implode(', ', $tagsAsArray);
},
function ($tagsAsString) {
// преобразовать строку обратно в массив
return explode(', ', $tagsAsString);
}
))
;
}
// ...
}
|
CallbackTransformer
берёт две функции обратного вызова в качестве аргументов.
Первый преобразует первоначальное значение в формат, который будет использован для
отображения поля. Второй делает обратное - он преобразует отправленное значение
обратно в формат, который вы увидите в своём коде.
Tip
Метод addModelTransformer()
принимает любой объект, который реализует
DataTransformerInterface
- так что вы можете
создать ваши собственные классы, вместо того, чтобы вставлять всю логику в форму
(смотрите следующий раздел).
Вы также можете добавить преобразователь во время добавления поля, слегка изменив формат:
1 2 3 4 5 6 7 | use Symfony\Component\Form\Extension\Core\Type\TextType;
$builder->add(
$builder
->create('tags', TextType::class)
->addModelTransformer(...)
);
|
Более сложный пример: Преобразование номера проблемы в сущность проблемы¶
Скажем, у вас есть отношение многие-к-одному между сущностью Задачи (task) и сущностью Проблемы (issue) (т.е. каждая задача имеет необязательный внешний ключ к относящейся к ней проблеме). Добавление окна списка со всеми возможными проблемами может быть очень долгим и долго загружаться. Вместо этого, вы рашаете, что вы хотите добавить текстовое окно, где пользователь может просто вводить номер проблемы.
Начните с установки текстового поля, как обычно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Task;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Task::class,
));
}
// ...
}
|
Хорошее начало! Но если бы вы остановились здесь и отправили форму, то свойство
задачи issue
было бы строкой (например, "55"). Как вы можете преобразовать это
в сущность Issue
при отправке?
Создание преобразователя¶
Вы можете использовать CallbackTransformer
, как ранее. Но так как это немного сложнее,
создание нового класса преобразователя будет упрощать класс формы TaskType
.
Создайте класс IssueToNumberTransformer
: он будет отвечать за конвертирование из номера
проблемы в объект Issue
и наоборот:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace AppBundle\Form\DataTransformer;
use AppBundle\Entity\Issue;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* 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)
{
// нет номера проблемы? Это необязательно, так что всё хорошо
if (!$issueNumber) {
return;
}
$issue = $this->em
->getRepository('AppBundle:Issue')
// запрос проблемы по этому id
->find($issueNumber)
;
if (null === $issue) {
// вызывает ошибку валидации
// это сообщение не отображается пользователю
// смотрите опцию invalid_message
throw new TransformationFailedException(sprintf(
'An issue with number "%s" does not exist!',
$issueNumber
));
}
return $issue;
}
}
|
Так же. как и в первом примере, преобразователь имеет два направления. Метод
transform()
отвечает за конвертирование данных, используемых в вашем коде,
в формат, который может быть отображён в вашей форме (например, объект Issue
в строку id
). Метод reverseTransform()
делает обратное: он конвертирует
отправленные данные обратно в формат, который вы хотите (например, конвертирует
id
обратно в объект Issue
).
Чтобы вызвать ошибку валидации, используйте TransformationFailedException
.
Но сообщение, которое вы передаёте этому исключению, не будет отображено пользователю.
Вы установите это сообщение с помощью опции invalid_message
(см. ниже).
Note
Когда методу transform()
передаётся null
, ваш преобразователь должен
вернуть эквивалентное значение типа, в который он трансформирует (например,
пустую строку, 0 для целых чисел или 0.0 для плавающих).
Использование преобразователя¶
Далее вам нужно использовать объект IssueToNumberTransformer
внутри TaskType
,
и добаивть его в поле issue
. Не проблема! Просто добавьте метод __construct()
и типизируйте новый класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
private $transformer;
public function __construct(IssueToNumberTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class, array(
// validation message if the data transformer fails
'invalid_message' => 'That is not a valid issue number',
));
// ...
$builder->get('issue')
->addModelTransformer($this->transformer);
}
// ...
}
|
Вот и всё! Если вы используете автомонтирование и
автоконфигурацию, Symfony автоматически будет
передавать вашему TaskType
экземпляр IssueToNumberTransformer
.
Tip
Чтобы узнать больше об определении типов форм в качестве сервисов, прочтите регистрация вашего типа формы, как сервиса.
Теперь вы с лёгкостью можете использовать ваш TaskType
:
1 2 3 4 | // например, где-то в контроллере
$form = $this->createForm(TaskType::class, $task);
// ...
|
Супер, вы закончили! Ваш пользователь будет иметь возможность ввести номер
проблемы в текстовое поле и он будет преобразован в объект проблемы (issue).
Это означает, что после успешной отправки, компонент формы передаст Task::setIssue()
настоящий объект Issue
вместо номера проблемы.
Если проблема не будет найдена, для этого поля будет создана ошибка формы;
сообщение об ошибке можно контролировать с помощью опции поля invalid_message
.
Caution
Юудьте осторожны при добавлении собственных преобразователей. Например, следующее неправильно, так как преобразователь будет применён ко всей форме, вместо одного поля:
1 2 3 4 | // ЭТО НЕПРАВИЛЬНО - ПРЕОБРАЗОВАТЕЛЬ БУДЕТ ПРИМЕНЁН КО ВСЕЙ ФОРМЕ
// см. пример выше для правильного кода
$builder->add('issue', TextType::class)
->addModelTransformer($transformer);
|
Создание повторно используемого поля issue_selector¶
В примере выше, вы применяли преобразователь к нормальному полю text
. Но
если вы часто делаете это преобразование, то может быть лучше
создать пользовательский тип поля. который
делает это автоматически.
Для начала, создайте пользовательский класс типа поля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // src/AppBundle/Form/IssueSelectorType.php
namespace AppBundle\Form;
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IssueSelectorType extends AbstractType
{
private $transformer;
public function __construct(IssueToNumberTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected issue does not exist',
));
}
public function getParent()
{
return TextType::class;
}
}
|
Отлично! Он будет действоваь и отображать, как текстовое поле (getParent()
), но будет
автоматически иметь преобразователь данных и хорошее значение по умолчанию для опции
invalid_message
.
Если вы используете автомонтирование и автоконфигурацию, вы можете незамедлительно начать использование формы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', TextareaType::class)
->add('issue', IssueSelectorType::class)
;
}
// ...
}
|
Tip
Если вы не используете autowire
и autoconfigure
, смотрите
How to Create a Custom Form Field Type, чтобы узнать, как сконфигурировать
ваш новый IssueSelectorType
.
О преобразователях модели и просмотра¶
В примере выше, преобразователь был использован как "модель". На самом деле, существует два разных вида преобразователей и три разных типа лежащих в основе данных.

В любой форме, три разных типа данных - это:
- Данные модели - Это данные в формате, используемом в вашем приложении (например,
объект
Issue
). Если вы вызоветеForm::getData()
илиForm::setData()
, вы имеете дело с данными "модели". - Данные нормы - Это нормализированная версия ваших данных и она зачастую совпадает с данными "модели" (но не в нашем примере). Обычно они не используются напрямую.
- Данные просмотра - Это формат, который используется для заполнения самих
полей формы. Это также формат, в котором пользователь будет отправлять данные.
Когда вы вызываете
Form::submit($data)
,$data
имеет формат данных "просмотра".
Два разных типа преобразователей помогают конвертировать из и в каждый из этих типов данных:
- Преобразователи модели:
transform()
: "данные модели" => "данные нормы"reverseTransform()
: "данные нормы" => "данные модели"
- Преобразователи просмотра:
transform()
: "данные нормы" => "данные просмотра"reverseTransform()
: "данные просмотра" => "данные нормы"
То, какой преобразователь вам нужен, зависит от ситуации.
Чтобы использовать преобразователь просмотра, вызовите addViewTransformer()
.
Так зачем использовать преобразователь модели?¶
В этом примере, поле - это поле text
, а текстовое поле всегда должно быть
простым скалярным форматом в форматах "нормы" и "просмотра". По этой причине,
наиболее подходящим преобразователем был преобразователь "модели" (который конвертирует
из/в формата нормы - строки номера проблемы - в формат модели - объект проблемы).
Разница между преобразователями тонкая, и вы всегда должны думать о том, какими
на самом деле должны быть данные "нормы" для поля. Например, данные "нормы" для
поля text
- это строка, но для поля date
- это объект DateTime
.
Tip
В качестве общего правила, нормализированные данные должны содержать максимально возможное количество информации.
Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.