Як створити користувацький тип поля форми

Дата оновлення перекладу 2022-12-14

Як створити користувацький тип поля форми

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 конфігурованим у два способи:

  • На додаток до "address line 1" та "address line 2", деякі адреси повинні мати дозвіл відображати "address line 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'],
                // у цьому приклади, ця конфігурація також буде валідною:
                // '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 %}