Как использовать преобразователи данных
Дата обновления перевода 2024-07-19
Как использовать преобразователи данных
Преобразователи данных используются для перевода данных для поля в формат, который
может быть отображён в форме (и обратно при отправке). Они уже используются внутренне
для многих типов полей. Например, поле DateType
может быть отображено в виде текстового ввода формата yyyy-MM-dd
. Внутренне, преобразователь
данных конвертирует начальное значение поля DateTime
в строку yyyy-MM-dd
, чтобы
отобразить форму, а потом обратно в объект DateTime
при отправке.
Caution
Если в поле формы установлена опция inherit_data
, преобразователи данных
не будут применены к такому полю.
See also
Если, вместо преобразования репрезентации значения, вам нужно вывести значения в поле формы и обратно, вам нужно использовать преобразователь data mapper. Смотрите Как и когда использовать отображатели данных.
Пример #1: Преобразование строковых тегов данных из ввода пользователя в массив
Представьте, что у вас есть форма задачи с типом тегов 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/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Entity\Task;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('tags', TextType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'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/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
// ...
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('tags', TextType::class);
$builder->get('tags')
->addModelTransformer(new CallbackTransformer(
function ($tagsAsArray): string {
// преобразовать массив в строку
return implode(', ', $tagsAsArray);
},
function ($tagsAsString): array {
// преобразовать строку обратно в массив
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(...)
);
Пример #2: Преобразование номера проблемы в сущность проблемы
Скажем, у вас есть отношение многие-к-одному между сущностью Задачи и сущностью Проблемы (т.е. каждая задача имеет необязательный внешний ключ к относящейся к ней проблеме). Добавление окна списка со всеми возможными проблемами может быть очень долгим и долго загружаться. Вместо этого, вы рашаете, что вы хотите добавить текстовое окно, где пользователь может просто вводить номер проблемы.
Начните с установки текстового поля, как обычно:
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/Form/Type/TaskType.php
namespace App\Form\Type;
use App\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): void
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'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
// src/Form/DataTransformer/IssueToNumberTransformer.php
namespace App\Form\DataTransformer;
use App\Entity\Issue;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
) {
}
/**
* Преобразует объект (проблему) в строку (цифру).
*
* @param Issue|null $issue
*/
public function transform($issue): string
{
if (null === $issue) {
return '';
}
return $issue->getId();
}
/**
* Преобразует строку (число) в объект (проблему).
*
* @param string $issueNumber
* @throws TransformationFailedException if object (issue) is not found.
*/
public function reverseTransform($issueNumber): ?Issue
{
// нет номера проблемы? Это необязательно, так что всё хорошо
if (!$issueNumber) {
return null;
}
$issue = $this->entityManager
->getRepository(Issue::class)
// запросить проблему по этому 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
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
// ...
class TaskType extends AbstractType
{
public function __construct(
private IssueToNumberTransformer $transformer,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('description', TextareaType::class)
->add('issue', TextType::class, [
// сообщение валидации при ошибке преобразователя данных
'invalid_message' => 'That is not a valid issue number',
]);
// ...
$builder->get('issue')
->addModelTransformer($this->transformer);
}
// ...
}
Каждый раз, когда преобразователь вызывает исключение, пользователю отображается
invalid_message
. Вместо отображения одного и того же сообщения каждый раз, вы
можете установить сообщение об ошибке для конечного пользователя в преобразователе данных,
используя метод setInvalidMessage()
. Это также позволит вам добавить значения пользователей:
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
// src/Form/DataTransformer/IssueToNumberTransformer.php
namespace App\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
// ...
public function reverseTransform($issueNumber): ?Issue
{
// ...
if (null === $issue) {
$privateErrorMessage = sprintf('An issue with number "%s" does not exist!', $issueNumber);
$publicErrorMessage = 'The given "{{ value }}" value is not a valid issue number.';
$failure = new TransformationFailedException($privateErrorMessage);
$failure->setInvalidMessage($publicErrorMessage, [
'{{ value }}' => $issueNumber,
]);
throw $failure;
}
return $issue;
}
}
Вот и всё! Если вы используете
конфигурацию services.yaml по умолчанию ,
Symfony автоматически будет знать, что надо передать вашему TaskType
экземпляр
IssueToNumberTransformer
благодаря автомантированию и
автоконфигурации .
В других случаях, зарегистрируйте класс формы в качестве сервиса
и тегируйте его тегом form.type
.
Теперь вы можете использовать ваш TaskType
:
1 2 3 4
// например, где-то в контроллере
$form = $this->createForm(TaskType::class, $task);
// ...
Супер, вы закончили! Ваш пользователь будет иметь возможность ввести номер
проблемы в текстовое поле и он будет преобразован в объект проблемы (issue).
Это означает, что после успешной отправки, компонент Form передаст 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
// src/Form/IssueSelectorType.php
namespace App\Form;
use App\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IssueSelectorType extends AbstractType
{
public function __construct(
private IssueToNumberTransformer $transformer,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'invalid_message' => 'The selected issue does not exist',
]);
}
public function getParent(): string
{
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/Form/Type/TaskType.php
namespace App\Form\Type;
use App\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
, смотрите
Как создать пользовательский тип поля формы, чтобы узнать, как сконфигурировать
ваш новый IssueSelectorType
.
О преобразователях модели и просмотра
В примере выше, преобразователь был использован как "модель". На самом деле, существует два разных вида преобразователей и три разных типа лежащих в основе данных.
Так зачем использовать преобразователь модели?
В этом примере, поле - это поле text
, а текстовое поле всегда должно быть
простым скалярным форматом в форматах "нормы" и "просмотра". По этой причине,
наиболее подходящим преобразователем был преобразователь "модели" (который конвертирует
из/в формата нормы - строки номера проблемы - в формат модели - объект проблемы).
Разница между преобразователями тонкая, и вы всегда должны думать о том, какими
на самом деле должны быть данные "нормы" для поля. Например, данные "нормы" для
поля text
- это строка, но для поля date
- это объект DateTime
.
Tip
В качестве общего правила, нормализированные данные должны содержать максимально возможное количество информации.