Як використовувати перетворювачі даних
Дата оновлення перекладу 2024-05-27
Як використовувати перетворювачі даних
Перетворювачі даних використовуються для переведення даних для поля в формат, який
може бути відображений в формі (та назад при відправленні). Вони вже використовуються
внутрішньо для багатьох типів полів. Наприклад, поле DateType
може бути відображене у вигляді текстового введення формату yyyy-MM-dd
. Внутрішньо,
перетворювач даних конвертує початкове значення поля DateTime
в рядок yyyy-MM-dd
,
щоб відобразити форму, а потім назад в об'єкт DateTime
при відправленні.
Caution
Якщо в полі форми встановлена опція inherit_data
, перетворювачі даних
не будуть застосовані до такого поля.
See also
Якщо замість перетворення репрезентації значення вам необхідно вивести значення в поле форми та назад, вам необхідно використовувати перетворювач відображення даних. Дивіться Як і коли використовувати мапувальники даних.
Приклад #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 62 63
// 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
* @return строку
*/
public function transform($issue): string
{
if (null === $issue) {
return '';
}
return $issue->getId();
}
/**
* Перетворює рядок (число) в об'єкт (проблему).
*
* @param string $issueNumber
* @return Issue|null
* @throws TransformationFailedException if object (issue) is not found.
*/
public function reverseTransform($issueNumber): ?Issue
{
// немає номеру проблеми? Це необов'язково, так що все добре
if (!$issueNumber) {
return null;
}
$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
// 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);
// ...
Супер, ви закінчили! Ваш користувач матиме можливість ввести номер проблеми
у текстове поле і він буде перетворений в об'єкт проблеми. Це означає, що
вісля успішного відправлення, компонент Form передасть Task::setIssue()
справжній об'єкт Issue
замість номеру проблеми.
Якщо проблему не буде знайдено, для цього поля буде створена помилка форми;
повідомлення про помилку можна контролювати за допомогою опції поля
invalid_message
.
Caution
Будьте обережні при додаванні власних перетворювачів. Наприклад, наступне - неправильно, так як перетворювач буде застосований до всієї форми, замість одного поля:
1 2 3 4
// ЦЕ НЕПРАВИЛЬНО - ПЕРЕТВОРЮВАЧ БУДЕ ЗАСТОСОВАНИЙ ДО ВСІЄЇ ФОРМИ
// див. приклад вище для правильного коду
$builder->add('issue', TextType::class)
->addModelTransformer($transformer);
Cтворення повторно використовуваного поля 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): void
{
$builder
->add('description', TextareaType::class)
->add('issue', IssueSelectorType::class)
;
}
// ...
}
Tip
Якщо ви не використовуєте autowire
та autoconfigure
, дивіться
Як створити користувацький тип поля форми, щоб дізнатися, як сконфігурувати
ваш новий IssueSelectorType
.
Про перетворювачі моделі та перегляду
У прикладі вище, перетворювач було використано як "модель". Насправді, існує два різних види перетворювачів та три різних типи даних, що лежать в основі.
В будь-якій форми, три різних типи даних - це:
- Дані моделі - Це дані у форматі, який використовується у вашому додатку (наприклад,
об'єкт
Issue
). Якщо ви викличетеForm::getData()
абоForm::setData()
, ви маєте справу з даними "моделі". - Дані норми - Це нормалізована версія ваших даних і вона часто співпадає з даними "моделі" (але не у нашому прикладі). Зазвичай вони не використовуються напряму.
- Дані перегляду - Це формат, який використовується для заповнення самих полів
форми. Це також формат, в якому користувач відправлятиме дані. Коли ви викликаєте
Form::submit($data)
,$data
маєть формат даних "перегляду".
Два різних типи перетворювачів допомогають конвертувати з та в кожний з цих типів даних:
- Перетворювачі моделі:
-
transform()
: "дані моделі" => "дані норми"reverseTransform()
: "дані норми" => "дані моделі"
- Перетворювачі перегляду:
-
transform()
: "дані норми" => "дані перегляду"reverseTransform()
: "дані перегляду" => "дані норми"
Те, який перетворювач вам потрібен, залежить від ситуації.
Щоб використовувати перетворювач перегляду, викличте addViewTransformer()
.
Caution
Будьте обережні з перетворювачами моделей та типамі полів
Колекція. Доньки Колекції
створються на ранніх етапах у PRE_SET_DATA
його ResizeFormListener
,
і їхні дані заповнюються пізніше, з нормалізованих даних. Тому ваш
перетворювач моделі не може зменшити кількість об'єктів у Колекції
(тобто, відфільтрувати деякі об'єкти), і в такому випадку, колекція
виходить з деякими пустими доньками.
Можливим обхідним шляхом йього обмеження може бути не пряме використання підлеглого об'єкту, а використання DTO (Об'єкту перенесення даних), який реалізує перетворення таких несумісних структур даних.
Так навіщо використовувати перетворювачі моделі?
В цьому прикладі, поле - це поле text
, а текстове поле завжди має бути
простим скалярним форматом в форматах "норми" та "перегляду". За цією причиною,
найкращим перетворювачем був перетворювач "моделі" (який конвертує з/в формату
норми - рядки номеру проблеми - у формат моделі - об'єкт проблеми).
Різниця між перетворювачами тонка, і ви завжди маєте думати про те, якими насправді
мають бути дані "норми" для поля. Наприклад, дані "норми" для поля text
- це
рядок, але для поля date
- це об'єкт DateTime
.
Tip
У якості загального правила, нормалізовані дані мають містити максимально можливу кількість інформації.