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

Дата обновления перевода 2023-09-04

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

Когда форма составная, изначальные данные должны быть переданы дочерним, чтобы каждая могла отображать собственное значение ввода. При отправке, значения дочерних форм должны быть записаны обратно в форму.

Отображатели данных отвечают за чтение и запись данных из/в родительскую форму.

Основной встроенный отображатель данных использует компонент PropertyAccess, который подойдет в большинстве случаев. Однако, вы можете создать собственную реализацию, которая может, к примеру, передавать отправленные данные постоянным объектам через свой конструктор.

Разница между отображателями и преобразователями данных

Важно знать разницу между преображателями данных и отображателями.

  • Преображатели данных изменяют представление значения (например, из "2016-08-12" в экземпляр DateTime);
  • Отображатели данных отображают данные (например, объект или массив) в полях форм и обратно, например, используя один экземпляр DateTime для наполнения внутренних полей (например, года, часа и т.д.) составного типа даты.

Создание отображателя данных

Представьте, что вы хотите сохранить набор цветов в базе данных. Для этого, вы используете постоянный объект цвета:

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/Painting/Color.php
namespace App\Painting;

final class Color
{
    public function __construct(
        private int $red,
        private int $green,
        private int $blue,
    ) {
    }

    public function getRed(): int
    {
        return $this->red;
    }

    public function getGreen(): int
    {
        return $this->green;
    }

    public function getBlue(): int
    {
        return $this->blue;
    }
}

Тип формы должен иметь разрешение на изменение цвета. Н так как вы решили сделать объект Color постоянным, новый объект цвета должен быть создан каждый раз, когда изменяетя одно из значений.

Tip

Если вы используете изменяемые объект с аргументами конструктора, вместо использования отображателя данных, вам нужно сконфигурировать опцию empty_data с замыканием, как описывается в статье Как сконфигурировать пустые данные для класса формы .

Поля формы красный, зеленый и синий должны быть отображены аргументам конструктора, а экземпляр Color должен быть отображен полям формы красный, зеленый и голубой. Узнаёте знакомый паттерн? Пришло время отображателя данных. Самый простой создать его - реализовать DataMapperInterface в вашем типе формы:

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
// src/Form/ColorType.php
namespace App\Form;

use App\Painting\Color;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormInterface;

final class ColorType extends AbstractType implements DataMapperInterface
{
    // ...

    /**
     * @param Color|null $viewData
     */
    public function mapDataToForms($viewData, \Traversable $forms): void
    {
        // здесь еще нет данных, поэтому нечего предварительно наполнять
        if (null === $viewData) {
            return;
        }

        // невалидный тип данных
        if (!$viewData instanceof Color) {
            throw new UnexpectedTypeException($viewData, Color::class);
        }

        /** @var FormInterface[] $forms */
        $forms = iterator_to_array($forms);

        // инициализировать значения полей формы
        $forms['red']->setData($viewData->getRed());
        $forms['green']->setData($viewData->getGreen());
        $forms['blue']->setData($viewData->getBlue());
    }

    public function mapFormsToData(\Traversable $forms, &$viewData): void
    {
        /** @var FormInterface[] $forms */
        $forms = iterator_to_array($forms);

        // так как данные передаются по ссылке, переопределение изменит их и в
        // объекте формы
        // не забывайте о несовместимости типов, см. предупреждение ниже
        $viewData = new Color(
            $forms['red']->getData(),
            $forms['green']->getData(),
            $forms['blue']->getData()
        );
    }
}

Caution

Данные, переданные отображателю, еще не валидированы. Это означает, что ваши объекты должны позволять свое создание в невалидном состоянии, чтобы предоставить дружелюбные по отношению к пользователю ошибки в форме.

Использование отображателя

После создания отображателя данных, вам нужно сконфигурировать форму для его использования. Этого можно достичь используя метод setDataMapper():

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
// src/Form/Type/ColorType.php
namespace App\Form\Type;

// ...
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class ColorType extends AbstractType implements DataMapperInterface
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('red', IntegerType::class, [
                // форсировать строгость типа, чтобы гарантировать, что конструктор
                // класса Color не сломается
                'empty_data' => '0',
            ])
            ->add('green', IntegerType::class, [
                'empty_data' => '0',
            ])
            ->add('blue', IntegerType::class, [
                'empty_data' => '0',
            ])
            // сконфигурировать отображатель данных для этого FormType
            ->setDataMapper($this)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        // при создании нового цвета, изначальные данные должны быть null
        $resolver->setDefault('empty_data', null);
    }

    // ...
}

Круто! При использовании формы ColorType, пользовательские методы отображателя данных теперь создадут новый объект Color.

Отображение полей формы с использованием обратных вызовов

Удобно, что вы также можете отображать данные из и в поле формы, используя опции getter и setter. Например, представьте, что у вас есть форма с некоторыми полями, и только одно из них должно быть отображено каким-то особым образом. Или вам нужно изменить то, как оно записывается в подлежащий объект. В таком случае, зарегистрируйте PHP-вызываемое, которое может писать или читать из/в этот конкретный объект:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('state', ChoiceType::class, [
        'choices' => [
            'active' => true,
            'paused' => false,
        ],
        'getter' => function (Task $task, FormInterface $form): bool {
            return !$task->isCancelled() && !$task->isPaused();
        },
        'setter' => function (Task &$task, bool $state, FormInterface $form): void {
            if ($state) {
                $task->activate();
            } else {
                $task->pause();
            }
        },
    ]);
}

Если они доступны, эти опции имеют главенство над свойством метода пути и отображатель данных по умолчанию все еще будет использовать компонент PropertyAccess для других полей формы.

Caution

Когда форма имеет опцию inherit_data установленную, как true, она не использует отображатель данных и позволяет родительской форме отображать внутренние значения.