Як і коли використовувати мапувальники даних

Дата оновлення перекладу 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, [
                // форсуйте жорсткість типу, щоб гарантувати, що конструктор
                // класу Кольору не зламається
                '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, вона не використовує мапувальник даних та дозволяє батьківській формі мапувати внутрішні значення.