Как работать с темами формы

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

Как работать с темами формы

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

Встроенные темы формы Symfony

Symfony поставляется с несколькими встроенными темами формы, которые заставляют ваши формы выглядеть потрясающе с использованием наиболее популярных фреймворков CSS. Каждая тема определяется в одном шаблоне Twig, и они подключаются в опции twig.form_themes:

Tip

Прочтите статьи о теме формы Symfony Bootstrap 4 и теме формы Symfony Bootstrap 5, чтобы узнать о них больше.

Применение тем ко всем формам

Формы Symfony по умолчанию используют тему form_div_layout.html.twig. Если вы хотите использовать другую тему для всех форм вашего приложения, сконфигурируйте это в опции twig.form_themes:

  • YAML
  • XML
  • PHP
1
2
3
4
# config/packages/twig.yaml
twig:
    form_themes: ['bootstrap_4_horizontal_layout.html.twig']
    # ...

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

Порядок тем в опции twig.form_themes важен. Каждая тема переопределяет все предыдущие темы, поэтому вы должны размещать самые важные темы в конце списка.

Применение тем к одной форме

Хотя в большинстве случаев вы будете применять темы форм глобально, вам может понадобиться применить тему к какой-то конкретной форме. Вы можете сделать это с помощью тега Twig form_theme:

1
2
3
4
5
6
{# эта тема формы будет применена только к форме этого шаблона #}
{% form_theme form 'foundation_5_layout.html.twig' %}

{{ form_start(form) }}
    {# ... #}
{{ form_end(form) }}

Первый аргмент тега form_theme (в этом примере, form) является именем переменной, которая хранит объект просмотра формы. Второй аргумент - путь шаблона Twig, который определяет тему формы.

Применение нескольких тем к одной форме

Форма также может быть настроеня, путем применения нескольких тем. Чтобы сделать это, передайте путь всех шаблонов Twig в виде массива, используя ключевое слово with (их порядок важен, так как каждая тема переопределяет все предыдущие):

1
2
3
4
5
6
7
{# применить несколько тем форм, но только к форме этого шаблона #}
{% form_theme form with [
    'foundation_5_layout.html.twig',
    'forms/my_custom_theme.html.twig'
] %}

{# ... #}

Применние разных тем к дочерним формам

Вы также можете применить ему форме к конкретной дочке вашей формы:

1
{% form_theme form.a_child_form 'form/my_custom_theme.html.twig' %}

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

1
2
{% form_theme form 'form/my_custom_theme.html.twig' %}
{% form_theme form.a_child_form 'form/my_other_theme.html.twig' %}

Отключение глобальных тем для одной формы

Глобальные темы форм, определенные в приложении, всегда применяются ко всем формам, даже к тем, которые используют тег form_theme для применения собственной темы. Вы можете захотеть отключить это, к примеру, при создании интерфейса админа для пакета, который может быть установлен в других приложениях Symfony (и вы не сможете контролировать, какие темы будут включены глобально). Чтобы сделать это, добавьте ключевое слово only после списка тем формы:

1
2
3
{% form_theme form with ['foundation_5_layout.html.twig'] only %}

{# ... #}

Caution

При использовании ключевого слова only, ни одна из встроенных тем формы Symfony (form_div_layout.html.twig, и др.) не будет применена, Чтобы отобразить ваши формы корректно, вам нужно либо предоставить полностью функционирующую тему формы самостоятельно, или расширить одну из встроенных тем формы с помощью ключевого слова Twig вместо extends, чтобы повторно использовать содержание изначальной темы.

1
2
3
4
{# templates/form/common.html.twig #}
{% use "form_div_layout.html.twig" %}

{# ... #}

Создание вашей собственной темы формы

Symfony использует блоки Twig, чтобы отображать каждую часть формы - ярлыки полей, ошибки, текстовые поля <input>, теги <select>, и т.д. Тема - это шаблон Twig с одним или более таких блоков, которые вы хотите использовать при отображении формы.

Рассмотрите, к примеру, поле формы, которое представляет собой свойство целого числа под названием age. Если вы добавите это к шаблону:

1
{{ form_widget(form.age) }}

Сгенерированное HTML-содержание будет чем-то вроде этого (оно будет отличаться в зависимости от тем формы, включенных в вашем приложении):

1
<input type="number" id="form_age" name="form[age]" required="required" value="33"/>

Symfony использует блок Twig под названием integer_widget, чтобы отобразить это поле. Это потому, что тип поля - integer, и вы отображаете его widget (а не его label, или errors, или help). Первый шаг создания темы формы - знать, какой блок Twig переопределить, как разъясняется в следующем разделе.

Именование фрагмента формы

Именование фрагментов формы отличается в зависимости от ваших потребностей:

  • Если вы хотите настроить все поля одного типа (например, все <textarea>), используйте паттерн field-type_field-part (например, textarea_widget).
  • Если вы хотите настроить только одно конкретное поле (например, <textarea>, используемое для поля description формы, которая редактирует продукты), используйте паттерн _field-id_field-part (например, _product_description_widget).

В обоих случаях, field-part может быть любой из этих валидных частей поля формы:

Именование фрагментов для всех полей одного типа

Эти имена фрагментов следуют паттерну type_part, где type соответствует типу отображаемого поля (например, textarea, checkbox, date, и др.), а part - тому, что отображается (например, label, widget, и т.д.)

Несколько примеров имен фрагментов:

  • form_row - используется form_row() для отображения большинства полей;
  • textarea_widget - используется form_widget() для отображения поля типа textarea;
  • form_errors - используется form_errors() для отображения ошибок поля;

Именование фрагментов для отдельных полей

Эти имена фрагментов следуют паттерну _id_part, где id соответствует атрибуту поля id (например, product_description, user_age, и др.), а part - тому, что отображается (например, label, widget, и т.д.)

Атрибут id содержит как имя формы, так и имя поля (например, product_price). Имя формы может быть установлено вручную, или сгенерировано автоматически, основываясь на имени типа вашей формы (например, ProductType приравнивается к product). Если вы не уверены, какое имя у вашей формы, посмотрите в HTML-код отображенный для вашей формы. Вы также можете определить это значение ясно, с опцией block_name:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

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

    $builder->add('name', TextType::class, [
        'block_name' => 'custom_name',
    ]);
}

В этом примере, имя фрагмента будет _product_custom_name_widget вместо имени по умолчанию _product_name_widget.

Пользовательское именование фрагментов для индивидуальных полей

Опция block_prefix позволяет полям формы определять собственное пользовательское имя фрагмента. Это в основном полезно для настраивания некоторых экземпляров одного и того же поля, без необходимости создавать пользовательский тип формы:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder->add('name', TextType::class, [
        'block_prefix' => 'wrapped_text',
    ]);
}

Теперь вы можете использовать wrapped_text_row, wrapped_text_widget, и др. в качестве имен блоков.

Именование фрагментов для коллекций

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

1
2
3
4
5
{% block collection_row %} ... {% endblock %}
{% block collection_label %} ... {% endblock %}
{% block collection_widget %} ... {% endblock %}
{% block collection_help %} ... {% endblock %}
{% block collection_errors %} ... {% endblock %}

Вы также можете настроить каждую запись всех коллекций со следующими блоками:

1
2
3
4
5
{% block collection_entry_row %} ... {% endblock %}
{% block collection_entry_label %} ... {% endblock %}
{% block collection_entry_widget %} ... {% endblock %}
{% block collection_entry_help %} ... {% endblock %}
{% block collection_entry_errors %} ... {% endblock %}

Наконец, вы можете настроить конкретные коллекции форм, а не их все. Например, рассмотрите следующий сложный пример, где TaskManagerType имеет коллекцию TaskListType, которая, в свою очередь, имеет коллекцию TaskType:

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
class TaskManagerType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        // ...
        $builder->add('taskLists', CollectionType::class, [
            'entry_type' => TaskListType::class,
            'block_name' => 'task_lists',
        ]);
    }
}

class TaskListType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        // ...
        $builder->add('tasks', CollectionType::class, [
            'entry_type' => TaskType::class,
        ]);
    }
}

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options = []): void
    {
        $builder->add('name');
        // ...
    }
}

Затем вы получите все следующие настраиваемые блоки (где * может быть заменен на row, widget, label, или help):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{% block _task_manager_task_lists_* %}
    {# поле коллекции TaskManager #}
{% endblock %}

{% block _task_manager_task_lists_entry_* %}
    {# внутренний TaskListType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_* %}
    {# поле коллекции  TaskListType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_entry_* %}
    {# внутренний TaskType #}
{% endblock %}

{% block _task_manager_task_lists_entry_tasks_entry_name_* %}
    {# поле TaskType #}
{% endblock %}

Наследование фрагментов шаблона

Каждый тип поля имеет родительский тип (например, родительский тип textarea - text, а родительский тип text - form), и Symfony использует фрагмент для ролительского типа, если базовый фрагмент не существует.

Когда Symfony отображает, к примеру, ошибки для типа textarea, она сначала ищет фрагмент textarea_errors, перед тем, как обращаться к фрагментам text_errors и form_errors.

Tip

"Родительский" тип каждого типа поля доступен в справочнике типов формы для каждого типа поля.

Создание темы формы в том же шаблоне, что и форма

Это рекомендуется, когда проводится настройка для конкретной формы в вашем приложении, вроде изменения всех элементов <textarea> в форме, или настраивания очень особенного поля формы, которое будет обработано с помощью JavaScript.

Вам просто нужно добавить специальный тег {% form_theme form _self %} к тому же шаблону, где отображается форма. Это заставляет Twig искать внутри шаблона все переопределенные блоки формы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends 'base.html.twig' %}

{% form_theme form _self %}

{# это переопределяет виджет любого поля типа целого числа, но только в
   формах, отображенных внутри этого шаблона #}
{% block integer_widget %}
    <div class="...">
        {# ... отобразить HTML-элемент, чтобы отобразить это поле ... #}
    </div>
{% endblock %}

{# это переопределяет весь ряд полей, чей "id" = "product_stock" (и чье
   "name" = "product[stock]"), но только в формах, отображенных внутри этого шаблона #}
{% block _product_stock_row %}
    <div class="..." id="...">
        {# ... отобразить все содержание поля, включая его ошибки ... #}
    </div>
{% endblock %}

{# ... отобразить форму ... #}

Главеный недостаток этого метода в том, что он работает только, если ваш шаблон расширяет друго й (в предыдущем примере - 'base.html.twig'). Если ваш шаблон этого не делает, вы должны указать form_theme на отдельный шаблон, как объясняется в следующем разделе.

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

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

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

Например, если ваша тема формы простая, и вы только хотите переопределить элементы <input type="integer">, создайте такой шаблон:

1
2
3
4
5
6
{# templates/form/my_theme.html.twig #}
{% block integer_widget %}

    {# ... добавьте весь необходимый HTML, CSS и JavaScript для отображения этого поля #}

{% endblock %}

Теперь вам нужно сказать Symfony, чтобы она использовала эту тему формы вместо (или в дополнение к) теме по умолчанию. Как объяснялось в предыдущих разделах этой статьи, если вы хотите применить тему глобально ко всем формам, определите опцию twig.form_themes:

  • YAML
  • XML
  • PHP
1
2
3
4
# config/packages/twig.yaml
twig:
    form_themes: ['form/my_theme.html.twig']
    # ...

Если вы только хотите применить это к некоторым определённым формам, используйте тег form_theme:

1
2
3
4
5
{% form_theme form 'form/my_theme.html.twig' %}

{{ form_start(form) }}
    {# ... #}
{{ form_end(form) }}

Повторное использование частей встроенной темы формы

Создание полной темы формы требует много работы, так как существует очень много типов полей формы. Вместо определения всех этих блоков Twig, вы можете определить только блоки, в которых вы заинтересованы, и затем сконфигурировать множество тем формы в вашем приложении или шаблоне. Это работает, так как при отображении блока, который не переопределен в вашей пользовательской теме,, Symfony резервно использует другие темы.

Другое решение - сделать так, чтобы ваш шаблон темы формы расширялся из одной из встроенных тем, используя тег Twig "use" вместо тега extends, чтобы вы могли наследовать все его блоки (если вы не уверены, расширьте форму из темы по умолчанию form_div_layout.html.twig):

1
2
3
4
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}

{# ... переопределить только блоки, в которых вы заинтересованы #}

Наконец, вы можете также использовать функцию Twig parent(), чтобы повторно использовать изначальное содержание встроенной темы. Это полезно, когда вы просто хотите сделать небольшие изменения, вроде как обернуть сгенерированный HTML с некоторым элементом:

1
2
3
4
5
6
7
8
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}

{% block integer_widget %}
    <div class="some-custom-class">
        {{ parent() }}
    </div>
{% endblock %}

Эта техника также работает при определении темы формы в том же шаблоне, который отображает форму. Однако, импортирование этих болков из встроенных тем, немного сложнее:

1
2
3
4
5
6
7
8
9
10
11
12
13
{% form_theme form _self %}

{# импортировать блок из встроенной темы и переименовать его, чтобы он не
   конфликтовал с тем же блоком, определенным в этом шаблоне #}
{% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %}

{% block integer_widget %}
    <div class="some-custom-class">
        {{ block('base_integer_widget') }}
    </div>
{% endblock %}

{# ... отобразить форму ... #}

Настраивание ошибок валидации формы

Если вы определяете правила валидации для ваших объектов, вы увидите некоторые сообщения ошибок валидации, если отправленные данные не будут валидными. Эти сообщения отображаются с функцией form_errors(), и могут быть настроены с блоком Twig form_errors в любой теме формы, как объяснялось в предыдущих разделах.

Важно помнить о том, что некоторые ошибки ассоциируются со всей формой, а не отдельным полем. Для того, чтобы различать глобальные и локальные ошибки, используйте одну из переменных, доступных в формах, под названием compound. Если она true, это означает, что то, что сейчас отображается, является коллекцией полей (например, целая форма), а не просто отдельным полем:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{# templates/form/my_theme.html.twig #}
{% block form_errors %}
    {% if errors|length > 0 %}
        {% if compound %}
            {# ... отобразить глобальные ошибки формы #}
            <ul>
                {% for error in errors %}
                    <li>{{ error.message }}</li>
                {% endfor %}
            </ul>
        {% else %}
            {# ... отобразить ошибки для одного поля #}
        {% endif %}
    {% endif %}
{% endblock form_errors %}