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

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

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

Для того, чтобы создать пользовательский тип поля, для начала вам надо создать класс, представляющий поле. В этой ситуации класс, содержащий тип поля, будет называться ShippingType, а файл будет храниться в локации для полей формы по умолчанию, то есть в App\Form\Type. Убедитесь в том, что поле расширяет AbstractType:

 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\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

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

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

Tip

Месторасположение этого файла не важно - каталог Form\Type - это всего-лишь договорённость.

Здесь, возврат значения функции getParent() сигнализирует, что вы расширяете поле ChoiceType. Это означает, что по умолчанию, вы наследуете всю логику и отображение этого типа поля. Чтобы увидеть часть логики, проверьте класс ChoiceType. Существует три метода, которые особенно важны:

buildForm()
Каждый тип поля имеет метод buildForm(), в котором вы конфигурируете и строите поле (поля). Отметьте, что это тот же метод, что вы используете для установки ваших форм, и тут он работает так же.
buildView()
Этот метод используется для установки любых дополнительных переменных, которые вам понадобятся при отображении вашего поля в шаблоне. Например, в ChoiceType, переменная multiple установлена и используется в шаблоне, чтобы усноавить (или не устанавливать) атрибут multiple в поле select. Смотрите Создание шаблона для поля, чтобы узнать больше.
configureOptions()
Определяет опции для вашего типа форма, которые могут быть использованы в buildForm() и buildView(). Существует много опций, общих для всех полей (смотрите FormType Field), но тут вы можете создать любые другие, которые вам нужны.

Tip

Если вы создаёте поле, которое состоит из множества полей, тогда убедитесь в том, что вы установили ваш "родительский" (parent) тип как form, или как что-то расширяющее form. Также, если вам нужно отличть "вид" (view) любого из ваших дочерних типов от родительского типа, используйте метод finishView().

Целью этого поля было расширить тип выбора, чтобы подключить выбор типа отправки. Это достигается путём прикрепления choices к списку доступных опций отправки.

Создание шаблона для поля

Каждый тип поля отображается фрагментом шаблона, который частично определяется именем класса вашего типа. Для большей информации, смотрите Что такое темы форм?.

Note

Первая часть префикса (например, shipping) походит от имени класса (ShippingType -> shipping). Это можно контролировать, переопределив getBlockPrefix() в ShippingType.

Caution

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

В этом случае, так как родительское поле - ChoiceType, вам не нужно делать какую-либо работу, так как пользовательский тип поля автоматически будет отображён как ChoiceType. Но ради этого примера, предположите, что когда ваше "раскрыто" (т.е. селективные или отмечаемые кнопки, вместо поля выбора), вы хотите всегда отображать его в элементе ul. В шаблоне темы вашей формы (смотрите ссылку выше, чтобы узнать детали), создайте блок shipping_widget, чтобы справиться с этим:

  • Twig
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {# templates/form/fields.html.twig #}
    {% block shipping_widget %}
        {% spaceless %}
            {% if expanded %}
                <ul {{ block('widget_container_attributes') }}>
                {% for child in form %}
                    <li>
                        {{ form_widget(child) }}
                        {{ form_label(child) }}
                    </li>
                {% endfor %}
                </ul>
            {% else %}
                {# просто позвольте виджету выбора отобразить тег выбора #}
                {{ block('choice_widget') }}
            {% endif %}
        {% endspaceless %}
    {% endblock %}
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- src/Resources/shipping_widget.html.php -->
    <?php if ($expanded) : ?>
        <ul <?php $view['form']->block($form, 'widget_container_attributes') ?>>
        <?php foreach ($form as $child) : ?>
            <li>
                <?php echo $view['form']->widget($child) ?>
                <?php echo $view['form']->label($child) ?>
            </li>
        <?php endforeach ?>
        </ul>
    <?php else : ?>
        <!-- просто позвольте виджету выбора отобразить тег выбора -->
        <?php echo $view['form']->renderBlock('choice_widget') ?>
    <?php endif ?>
    

Tip

Вы можете детальнее настраивать шаблон, используемый для отображения каждого ребенка типа выбора. В этом случае, блок для переопределения называется "имя блока" + _entry + "имя элемента" (label, errors или widget) (например, чтобы настроить ярлыки детей виджета Отправик (shipping), вам нужно было бы определить {% block shipping_entry_label %} ... {% endblock %}).

Note

Убедитесь в том, что используется правильный префикс виджета. В этом примере, имя должно быть shipping_widget (смотрите Что такое темы форм?). Далее, главный файл конфигурации должен указывать на пользовательский шаблон формы, чтобы он был использован при отображении всех форм.

При использовании Twig:

  • YAML
    1
    2
    3
    4
    # config/packages/twig.yaml
    twig:
        form_themes:
            - 'form/fields.html.twig'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/packages/twig.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:twig="http://symfony.com/schema/dic/twig"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/twig
            http://symfony.com/schema/dic/twig/twig-1.0.xsd">
    
        <twig:config>
            <twig:form-theme>form/fields.html.twig</twig:form-theme>
        </twig:config>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // config/packages/twig.php
    $container->loadFromExtension('twig', array(
        'form_themes' => array(
            'form/fields.html.twig',
        ),
    ));
    

Для шаблонизатора PHP, ваша конфигурация должна выглядеть так:

  • YAML
    1
    2
    3
    4
    5
    6
    # config/packages/framework.yaml
    framework:
        templating:
            form:
                resources:
                    - ':form:fields.html.php'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/packages/framework.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:templating>
                <framework:form>
                    <framework:resource>:form:fields.html.php</twig:resource>
                </framework:form>
            </framework:templating>
        </framework:config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // config/packages/framework.php
    $container->loadFromExtension('framework', array(
        'templating' => array(
            'form' => array(
                'resources' => array(
                    ':form:fields.html.php',
                ),
            ),
        ),
    ));
    

Использоваие типа поля

Теперь вы можете использовать ваш пользовательский тип поля незамедлительно, просто создав новый экземпляр типа в одной из ваших форм:

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

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

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('shipping_code', ShippingType::class, array(
            'placeholder' => 'Choose a delivery option',
        ));
    }
}

Но это работает только потому, что ShippingType() очень прост. Что, если бы коды отправки хранились в конфигурации или базе данных? Следующий раздел разъясняет, как более сложные типы полей решают эту проблему.

Доступ к сервисам и конфигурации

Если вам нужно получить доступ к сервисам из вашего класса формы, добавьте метод __construct(), как обычно:

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

// ...
use Doctrine\ORM\EntityManagerInterface;

class ShippingType extends AbstractType
{
    private $em;

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

    // используйте $this->em где угодно ниже ...
}

Если вы используете конфигурацию services.yaml по умолчанию (т.е. сервисы из Form/ загружаются, а autoconfigure включена), то это уже будет работать! Смотрите Creating/Configuring Services in the Container, чтобы узнать больше.

Tip

Если вы не используете автоконфигурацию, то убедитесь, что вы тегируете ваш сервис с помощью form.type.

Повеселитесь!

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.