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

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

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

Для того, чтобы создать пользовательский тип поля, для начала вам надо создать класс, представляющий поле. В этой ситуации класс, содержащий тип поля, будет называться ShippingType, а файл будет храниться в локации для полей формы по умолчанию, то есть в <BundleName>\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/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\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

Когда имя вашего класса формы совпадает с любым из встроенных типов поля, ваша форма может быть отображена неправильно. Тип формы, называнный AppBundle\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
    {# app/Resources/views/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
    <!-- app/Resources/views/form/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 ?>
    

Note

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

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

  • YAML
    1
    2
    3
    4
    # app/config/config.yml
    twig:
        form_themes:
            - 'form/fields.html.twig'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- app/config/config.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
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form_themes' => array(
            'form/fields.html.twig',
        ),
    ));
    

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

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/config.yml
    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
    <!-- app/config/config.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
    // app/config/config.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/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\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/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\Form\Type;

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

class ShippingType extends AbstractType
{
    private $em;

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

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

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

Tip

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

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

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