Як використовувати перетворювачі даних

Дата оновлення перекладу 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.

Про перетворювачі моделі та перегляду

У прикладі вище, перетворювач було використано як "модель". Насправді, існує два різних види перетворювачів та три різних типи даних, що лежать в основі.

В будь-якій форми, три різних типи даних - це:

  1. Дані моделі - Це дані у форматі, який використовується у вашому додатку (наприклад, об'єкт Issue). Якщо ви викличете Form::getData() або Form::setData(), ви маєте справу з даними "моделі".
  2. Дані норми - Це нормалізована версія ваших даних і вона часто співпадає з даними "моделі" (але не у нашому прикладі). Зазвичай вони не використовуються напряму.
  3. Дані перегляду - Це формат, який використовується для заповнення самих полів форми. Це також формат, в якому користувач відправлятиме дані. Коли ви викликаєте Form::submit($data), $data маєть формат даних "перегляду".

Два різних типи перетворювачів допомогають конвертувати з та в кожний з цих типів даних:

Перетворювачі моделі:
  • transform(): "дані моделі" => "дані норми"
  • reverseTransform(): "дані норми" => "дані моделі"
Перетворювачі перегляду:
  • transform(): "дані норми" => "дані перегляду"
  • reverseTransform(): "дані перегляду" => "дані норми"

Те, який перетворювач вам потрібен, залежить від ситуації.

Щоб використовувати перетворювач перегляду, викличте addViewTransformer().

Caution

Будьте обережні з перетворювачами моделей та типамі полів Колекція. Доньки Колекції створються на ранніх етапах у PRE_SET_DATA його ResizeFormListener, і їхні дані заповнюються пізніше, з нормалізованих даних. Тому ваш перетворювач моделі не може зменшити кількість об'єктів у Колекції (тобто, відфільтрувати деякі об'єкти), і в такому випадку, колекція виходить з деякими пустими доньками.

Можливим обхідним шляхом йього обмеження може бути не пряме використання підлеглого об'єкту, а використання DTO (Об'єкту перенесення даних), який реалізує перетворення таких несумісних структур даних.

Так навіщо використовувати перетворювачі моделі?

В цьому прикладі, поле - це поле text, а текстове поле завжди має бути простим скалярним форматом в форматах "норми" та "перегляду". За цією причиною, найкращим перетворювачем був перетворювач "моделі" (який конвертує з/в формату норми - рядки номеру проблеми - у формат моделі - об'єкт проблеми).

Різниця між перетворювачами тонка, і ви завжди маєте думати про те, якими насправді мають бути дані "норми" для поля. Наприклад, дані "норми" для поля text - це рядок, але для поля date - це об'єкт DateTime.

Tip

У якості загального правила, нормалізовані дані мають містити максимально можливу кількість інформації.