Как создать пользовательский тип поля формы

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

Как создать пользовательский тип поля формы

Symfony поставляется с десятками типов формы (которые называются "поля формы" в других проектах), готовыми к использованию в ваших приложениях. Однако, распространено создание пользовательских типов формы для решения особенных задач в ваших проектах.

Создание типов формы, основанных на встроенных типах Symfony

Самый простой способ создать тип формы - основать его на одном из существующих типов формы. Представьте, что ваш проект отображает список "опций отправки" как HTML-элемент <select>. Это может быть реализовано с помощью ChoiceType, где опция choices установлена в виде списка доступных опций отправки.

Однако, если вы используете один тип формы в нескольких формах, повторение списка choices каждый раз при использовании быстро становится скучным. В этом примере, лучшим решением будет создать пользовательский тип формы, основанный на ChoiceType. Пользовательский тип выглядит и ведёт себя как ChoiceType, но список вариантов уже наполнен опциями отправки, поэтому вам не нужно их определять.

Типы формы - это PHP-классы, которые реализуют FormTypeInterface, но вы должны вместо этого расширять из AbstractType, который уже реализует этот интерфейс и предоставляет некоторые утилиты. По соглашению, они хранятся в каталоге src/Form/Type/:

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

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ShippingType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'choices' => [
                'Standard Shipping' => 'standard',
                'Expedited Shipping' => 'expedited',
                'Priority Shipping' => 'priority',
            ],
        ]);
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }
}

getParent() сообщает Symfony взять ChoiceType как начальную точку, затем configureOptions() переопределяет некоторые из её опций. (Все методы FormTypeInterface детально разъясняются позже в этой статье). Результирующий тип формы - это поле выбора с предопределёнными опциями.

Теперь вы можете добавить этот тип формы при создании форм Symfony:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\ShippingType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('shipping', ShippingType::class)
        ;
    }

    // ...
}

Это всё. Поле формы shipping будет корректно отображено в любом шаблоне, так как оно повторно использует логику шаблонизации, определённую его родительским типом ChoiceType. По вашему желанию, вы также можете определить шаблон для ваших пользовательских типов, как разъясняется позже в этой статье.

Создание типов формы с нуля

Некоторые типы формы настолько специфичны для ваших проектов, что они не могут быть основаны на любом из существующих типов формы, так как они слишком отличаются. Рассмотрите приложение, которое хочет повторно использовать в разных формах следующий набор полей как "почтовый адрес":

Как объясняется выше, типы формы - это PHP-классы, которые реализуют FormTypeInterface, хотя удобнее вместо этого расширять из AbstractType:

1
2
3
4
5
6
7
8
9
10
11
// src/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...
}

Вот самые важные методы, которые может определять класс типа формы:

buildForm()
Добавляет и конфигурирует другие типы в этот тип. Тот же метод, что используется при создании классов формы Symfony .
buildView()
Устанавливает все дополнительные переменные, которые вам понадобятся при отображении поля в шаблоне.
finishView()
Этот метод позволяет изменять "просмотр" любого отображённого виджета. Это полезно, если ваш тип формы состоит из множества полей или содержит тип, который производит много HTML-элементов (например, ChoiceType). Для любого другого случая использования, рекомендуется вместо этого использовать buildView().
configureOptions()
Определяет конфигурируемые опции при использовании типа формы, которые также являются опциями, которые можно использовать в методах buildForm() и buildView(). Опции наследуются из родительских типов и расширений родительских типов, но вы можете создать любую пользовательскую опцию, которая вам нужна.
getParent()

Если ваш пользовательский тип основывается на другом типе (т.е. они имеют некоторую общую функциональность), добавьте этот метод, чтобы вернуть полностью квалифицированное имя класс изначального типа. Не используйте для этого PHP-наследование. Symfony вызовет все методы типов формы (buildForm(), buildView() и т.д.) и расширения типов родителя перед вызовом тех, которые определены в вашем пользовательском типе.

В других случаях, если ваш пользовательский тип построен с нуля, вы можете опустить getParent().

По умолчанию, класс AbstractType возвращает общий тип FormType, который есть корневым родителем для всех типов формы в компоненте Form.

Определения типа формы

Начните с добавления метода buildForm() для конфигуации всех типов, включённых в почтовый адрес. На данный момент, все поля имеют тип TextType:

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

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('addressLine1', TextType::class, [
                'help' => 'Street address, P.O. box, company name',
            ])
            ->add('addressLine2', TextType::class, [
                'help' => 'Apartment, suite, unit, building, floor',
            ])
            ->add('city', TextType::class)
            ->add('state', TextType::class, [
                'label' => 'State',
            ])
            ->add('zipCode', TextType::class, [
                'label' => 'ZIP Code',
            ])
        ;
    }
}

Tip

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

1
$ php bin/console debug:form

Этот тип формы готов к использованию внутри другигх форм, а все его поля будут корректно отображены в любом шаблоне:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\PostalAddressType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class)
        ;
    }

    // ...
}

Однако, реальная сила пользовательских типов формы достигается с помощью пользовательских опций формы (чтобы сделать их гибкими) и пользовательских шаблонов (чтобы сделать их красивее).

Добавление опций конфигурации для типа формы

Представьтье, что ваш проект требует сделать PostalAddressType конфигурируемым двумя способами:

  • В дополнение к "адресной строке 1" и "адресной строке 2", некоторые адреса должны иметь возможность отображать "адресную строку 3", чтобы хранить расширенную адресную информацию;
  • Вместо отображения свободного текстового водда, некоторые адреса должны иметь возможность ограничивать возможные штаты до заданного списка.

Это решается с помощью "опций типа формы", которые позволяют сконфигурировать поведение типов формы. Опции определяются в методе configureOptions(), и вы можете использовать все функции компонента OptionsResolver для определения, валидации и обработки их значений:

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

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        // это определяет доступные опции и их значения по умолчанию, если
        // они не сконфигурированы ясно при использовании типа формы
        $resolver->setDefaults([
            'allowed_states' => null,
            'is_extended_address' => false,
        ]);

        // по желанию, вы также можете ограничить тип(ы) опций (чтобы получить
        // автоматическую валидацию типа и полезные сообщения об ошибках для конечных пользователей)
        $resolver->setAllowedTypes('allowed_states', ['null', 'string', 'array']);
        $resolver->setAllowedTypes('is_extended_address', 'bool');

        // по желанию, вы можете преобразовать заданные значения для опций, чтобы
        // упростить дальнейшую обработку этих опций
        $resolver->setNormalizer('allowed_states', static function (Options $options, $states) {
            if (null === $states) {
                return $states;
            }

            if (is_string($states)) {
                $states = (array) $states;
            }

            return array_combine(array_values($states), array_values($states));
        });
    }
}

Теперь вы можете сконфигуриовать эти опции при использовании типа формы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Form/Type/OrderType.php
namespace App\Form\Type;

// ...

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class, [
                'is_extended_address' => true,
                'allowed_states' => ['CA', 'FL', 'TX'],
                // in this example, this config would also be valid:
                // 'allowed_states' => 'CA',
            ])
        ;
    }

    // ...
}

Последний шаг - использовать эти опции при построении формы:

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

// ...

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // ...

        if (true === $options['is_extended_address']) {
            $builder->add('addressLine3', TextType::class, [
                'help' => 'Extended address info',
            ]);
        }

        if (null !== $options['allowed_states']) {
            $builder->add('state', ChoiceType::class, [
                'choices' => $options['allowed_states'],
            ]);
        } else {
            $builder->add('state', TextType::class, [
                'label' => 'State/Province/Region',
            ]);
        }
    }
}

Создание шаблона типа формы

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

Сначала создайте новый шаблон Twig где угодно в приложении, чтобы хранить фрагменты, используемые для отображения типов:

1
2
3
{# templates/form/custom_types.html.twig #}

{# ... здесь вы добавите код Twig ... #}

Затем, обновите опцию form_themes, чтобы добавить этот новый шаблон к началу списка (первый переопределяет остальные файлы):

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/packages/twig.yaml
twig:
    form_themes:
        - 'form/custom_types.html.twig'
        - '...'

Последний шаг - создать реальный шаблон Twig, который будет отображать тип. Содержание шаблона зависит от того, какие фреймворки HTML, CSS и JavaScript и библиотеки используются в вашем приложении:

1
2
3
4
5
6
7
8
9
10
11
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {% for child in form.children|filter(child => not child.rendered) %}
        <div class="form-group">
            {{ form_label(child) }}
            {{ form_widget(child) }}
            {{ form_help(child) }}
            {{ form_errors(child) }}
        </div>
    {% endfor %}
{% endblock %}

Первая часть имени блока Twig (например, postal_address) походит из имени класса (PostalAddressType -> postal_address). Это можно контролировать путём переопределения метода getBlockPrefix() в PostalAddressType. Вторая часть имени блока Twig (например, _row) определяет, какая часть типа формы будет отображена (ряд, виджет, помощь, ошибки и т.д.).

Статья о темах форм детально разъясняет правила именования фрагментов формы . Следующая диаграмма отображает некоторые из имён блоков Twig, определённых в этом примере:

Caution

Если имя вашего класса формы совпадает с любым из встроенных типов поля, ваша форма может быть отображена неправильно. Тип формы с именем App\Form\PasswordType будет иметь то же имя блока, как и встроенный PasswordType, и не будет отображён корректно. Переопределите метод getBlockPrefix(), чтобы вернуть уникальный префикс блока (например, app_password) для избежания коллизий.

Передача перменных шаблону типа формы

Symfony передаёт серию переменных шаблону, используемому для отображения типа формы. Вы также можете передать ваши собственные переменные, которые могут быть основаны на опциях, определённых формой, или быть абсолютно независимыми:

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

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
// ...

class PostalAddressType extends AbstractType
{
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    // ...

    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        // передать опцию типа формы напрямую шаблону
        $view->vars['isExtendedAddress'] = $options['is_extended_address'];

        // сделать запрос базы данных, чтобы найти возможные уведомления, связанные с почтовыми адресами (например,
        // чтобы отобразить динамические сообщения вроде 'Доставка в штаты XX и YY будет добавлена на следующей неделе!')
        $view->vars['notification'] = $this->entityManager->find('...');
    }
}

Если вы используете конфигурацию services.yaml по умолчанию , этот пример уже будет работать! Иначе, создайте сервис для этого класса формы и добавьте тег form.type.

Переменные, добавленные в buildView(), доступны в шаблоне типа формы как любые другие обычные переменные Twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {# ... #}

    {% if isExtendedAddress %}
        {# ... #}
    {% endif %}

    {% if notification is not empty %}
        <div class="alert alert-primary" role="alert">
            {{ notification }}
        </div>
    {% endif %}
{% endblock %}