Как использовать преобразователи данных

Преобразователи данных используются для перевода данных для поля в формат, который может быть отображён в форме (и обратно при отправке). Они уже используются внутренне для многих типов полей. Например, поле 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.

О преобразователях модели и просмотра

В примере выше, преобразователь был использован как "модель". На самом деле, существует два разных вида преобразователей и три разных типа лежащих в основе данных.

../_images/data-transformer-types.png

В любой форме, три разных типа данных - это:

  1. Данные модели - Это данные в формате, используемом в вашем приложении (например, объект Issue). Если вы вызовете Form::getData() или Form::setData(), вы имеете дело с данными "модели".
  2. Данные нормы - Это нормализированная версия ваших данных и она зачастую совпадает с данными "модели" (но не в нашем примере). Обычно они не используются напрямую.
  3. Данные просмотра - Это формат, который используется для заполнения самих полей формы. Это также формат, в котором пользователь будет отправлять данные. Когда вы вызываете Form::submit($data), $data имеет формат данных "просмотра".

Два разных типа преобразователей помогают конвертировать из и в каждый из этих типов данных:

Преобразователи модели:
  • transform(): "данные модели" => "данные нормы"
  • reverseTransform(): "данные нормы" => "данные модели"
Преобразователи просмотра:
  • transform(): "данные нормы" => "данные просмотра"
  • reverseTransform(): "данные просмотра" => "данные нормы"

То, какой преобразователь вам нужен, зависит от ситуации.

Чтобы использовать преобразователь просмотра, вызовите addViewTransformer().

Так зачем использовать преобразователь модели?

В этом примере, поле - это поле text, а текстовое поле всегда должно быть простым скалярным форматом в форматах "нормы" и "просмотра". По этой причине, наиболее подходящим преобразователем был преобразователь "модели" (который конвертирует из/в формата нормы - строки номера проблемы - в формат модели - объект проблемы).

Разница между преобразователями тонкая, и вы всегда должны думать о том, какими на самом деле должны быть данные "нормы" для поля. Например, данные "нормы" для поля text - это строка, но для поля date - это объект DateTime.

Tip

В качестве общего правила, нормализированные данные должны содержать максимально возможное количество информации.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.