Pull to refresh

Расширяем Symfony 2 Forms

Reading time 4 min
Views 20K

Прелюдия


Формы, возможно, один из самых сложных компонентов Symfony2. Но за всей его сложностью скрывается поразительно гибкая архитектура, предоставляющая широкие возможности для расширения. Мы, как разработчики, можем добавлять (и изменять) типы полей форм (Form Type), использовать слушатели (Event Listeners), преобразователи данных (Data Transformers) и расширения типов (Type Extensions). О последних сегодня и поговорим.

Теория


Расширения типов предоставляют мощный механизм для изменения поведения и представления (FormView) типов полей. В пределах расширения предоставляется 4 точки входа для реализации необходимой логики:
public function buildForm(FormBuilderInterface $builder, array $options)
{
}

public function buildView(FormView $view, FormInterface $form, array $options)
{
}

public function finishView(FormView $view, FormInterface $form, array $options)
{
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
}

Рассмотрим их подробнее:
  1. buildForm — предоставляет доступ к объекту FormBuilder, что позволяет добавить, изменить или удалить поля формы, а также прикрепить слушателей;
  2. buildView — предоставляет доступ к объектам FormView и Form, для модификации представления формы. В пределах данного метода невозможно изменить дочерние представления;
  3. finishView — схож с buildView, но позволяет изменять дочерние представления;
  4. setDefaultOptions — предоставляет доступ к объекту OptionsResolver, для расширения или изменения списка опций.

В большинстве случаев мы будем оперировать методами setDefaultOptions и buildView. Один из распространенных алгоритмов применения расширений форм:
  1. setDefaultOptions — регистрируем новую опцию в OptionsResolver;
  2. buildView — реализуем логику обработки значения опции и передаем новый параметр в представление;
  3. изменяем шаблон формы для вывода значения параметра.

Контролировать тип полей для которого применяется расширение нам позволяет метод getExtendedType:
public function getExtendedType()
{
    // расширение будет применено для всех полей типа textarea
    return 'textarea';
}

Для использования расширения в Symfony 2 Framework его необходимо зарегистрировать как сервис в контейнере зависимостей (Dependency Injection Container) с помощью специального тэга — form.type_extension. Для тэга необходимо указать параметр alias с указанием типа полей к которому будет применяться расширение:
services:
    acme_demo.form.my_extension:
        class: Acme\DemoBundle\Form\Extension\MyExtension
        tags:
            - { name: form.type_extension, alias: textarea }

Практика


Для примера реализуем расширение позволяющее группировать поля формы и выводить их внутри элемента <fieldset>.
<?php

namespace Acme\DemoBundle\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\Options;

class FieldsetExtension extends AbstractTypeExtension
{
    private $rootView;

    public function getExtendedType()
    {
        // расширение будет работать с любым типом полей
        return 'form';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            // По умолчанию группировка не происходит.
            'group' => null,
        ));
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $group = $options['group'];

        if (null === $group) {
            return;
        }

        $root = $this->getRootView($view);
        $root->vars['groups'][$group][] = $form->getName();
    }

    public function getRootView(FormView $view)
    {
        $root = $view->parent;

        while (null === $root) {
            $root = $root->parent;
        }

        return $root;
    }
}

Создадим шаблон форм:
{# src Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% extends 'form_div_layout.html.twig' %}

{% block form_widget_compound %}
<div {{ block('widget_container_attributes') }}>
    {% if form.parent is empty %}
        {{ form_errors(form) }}
    {% endif %}
    {% if form.vars.groups is defined %}
        {% for group,items in form.vars.groups %}
        <fieldset>
            <legend>{{ group|title|trans({}, translation_domain) }}</legend>
            {% for item in items %}
                {{ form_row(form[item]) }}
            {% endfor %}
        </fieldset>
        {% endfor %}
    {% endif %}
    {{ form_rest(form) }}
</div>
{% endblock form_widget_compound %}

После регистрации расширения в контейнере зависимостей, можно приступать к использованию. Создадим новую форму:
<?php

namespace Acme\DemoBundle\Form\Extension;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text', array(
                'group' => 'fio'
            ))
            ->add('surname', 'text', array(
                'group' => 'fio'
            ))
            ->add('midname', 'text', array(
                'group' => 'fio'
            ))
            ->add('phone', 'text', array(
                'group' => 'contacts'
            ))
            ->add('skype', 'text', array(
                'group' => 'contacts'
            ))
            ->add('email', 'text', array(
                'group' => 'contacts'
            ))
        ;
    }
}

И шаблон для неё:
{# src Acme/DemoBundle/Resources/views/Person/new.html.twig #}
{% form_theme form 'AcmeDemoBundle:Form:fields.html.twig' %}

<form action="{{ path('person_create') }}" >
    {{ form_widget(form) }}
</form>

Итак расширение работает, но реализация не достаточно удобна, на мой взгляд. Попробуем упростить и добавить немного синтаксического сахара. Для этого создадим класс-обертку:
<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\FormBuilder;

class FormMapper
{
    /**
     * Form builder
     * @var FormBuidler
     */
    private $builder;

    /**
     * Active group
     * @var mixed null|string
     */
    private $group = null;

    public function __construct(FormBuilder $builder)
    {
        $this->builder = $builder;
    }

    /**
     * Add child to builder with group option
     */
    public function add($child, $type = null, array $options = array())
    {
        if (!array_key_exists('group', $options) and null !== $this->group) {
            $options['group'] = $this->group;
        }

        $this->builder->add($child, $type, $options);

        return $this;
    }

    /**
     * Set active group
     */
    public function with($group)
    {
        $this->group = $group;

        return $this;
    }
}

Теперь управление группами стало проще:
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $mapper = new FormMapper($builder);
    $mapper
        ->with('fio')
            ->add('name', 'text')
            ->add('surname', 'text')
            ->add('midname', 'text')
        ->with('contacts')
            ->add('phone', 'text')
            ->add('skype', 'text')
            ->add('email', 'text')
    ;
}

Финал


При подготовки статьи использовались материалы:
  1. Документация Symfony 2 Form Component
  2. Рецепты Symfony 2 Forms
Tags:
Hubs:
+10
Comments 1
Comments Comments 1

Articles