Symfony2 Формы | Forms Symfony2 (RUS) | МБ Блог

:

Работа с HTML формами является одной из самых обычных и, в то же время, сложных задач для программиста. Symfony2 включает в себя компонент Form, облегчающий работу с формами. В этой главе Вы создадите сложную форму, попутно изучая наиболее важные особенности библиотеки форм.

Компонент форм Symfony — это обособленная библиотека, которую можно использовать вне Symfony2 проектов. Чтобы получить более подробную информацию, смотрите Symfony2 Form Component на Github.

Создание простой формы

Предположим, Вы создаёте простое приложение, которое понадобится для отображения «заданий». Поскольку Вашим пользователям потребуется возможность редактировать и создавать задания, Вам потребуется создать форму. Однако, перед тем, как начать, обратите внимание на класс Task, предоставляющий и хранящий данные для одного задания:

// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;

class Task
{
protected $task;

protected $dueDate;

public function getTask()
{
return $this->task;
}
public function setTask($task)
{
$this->task = $task;
}

public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
}

Если Вы кодируете, придерживаясь данного примера, создайте, для начала, AcmeTaskBundle путём выполнения следующей команды (принимая все стандартные опции):

php app/console generate:bundle —namespace=Acme/TaskBundle

Этот класс является «старым-добрым PHP объектом», поскольку он не имеет отношения к Symfony или другой библиотеке. Это достаточно простой PHP объект, который напрямую решает задачи в Вашем приложении (т.е. необходимость представлять задание в приложении). Естественно, к завершению данной главы Вы сможете вводить данные в Task (посредством HTML формы), проверять данные и сохранять их в базе данных.

Создание формы

Теперь, когда Вы создали класс Task, следующим шагом будет создание и визуализация фактической HTML формы. В Symfony2 это совершается путём создания объекта формы и визуализации его в темплейте. Теперь всё перечисленное можно сделать в контроллере:

// src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\TaskBundle\Entity\Task;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
public function newAction(Request $request)
{
// create a task and give it some dummy data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));

$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->getForm();

return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}

Эти примеры демонстрируют Вам, каким образом можно создать форму в контроллере. Позже, в разделе «Создание классов форм», Вы научитесь создавать форму в обособленном классе, что рекомендуется, поскольку Ваша форма допускает многократное использование.

Для создания формы требует относительно небольшой код, поскольку объекты форм Symfony2 создаются с помощью специального «построителя». Построитель форм предоставляет Вам возможность написать простой «рецепт» формы и заставить его выполнять всю тяжёлую работу при создании фактической формы.

В приведённом примере Вы добавили в форму два поля: task и dueDate, соответствующие свойствам task и dueDate класса Task. Вы также назначили каждый «тип» (например, text, date), который, помимо всего прочего, определяет HTML тэг(-и) визуализации указанного поля.

Symfony2 содержит множество встроенных типов, о которых мы вскоре поговорим (читайте «Встроенные типы полей»).

Визуализация формы

После создания формы нам необходимо визуализировать её. Это можно сделать с помощью вставки специального объекта «вид» в темплейт (обратите внимание на $form->createView() в контроллеле), а также использования набора хелперов:

Twig:

{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}

<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}

<input type="submit" />
</form>

PHP:

<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->

<form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?> >
<?php echo $view['form']->widget($form) ?>

<input type="submit" />
</form>

перевод форм symfony2

Данный пример предполагает, что Вы уже создали маршрут под названием task_new, который указывает на созданный ранее контроллер AcmeTaskBundle:Default:new.

Вот и всё! После введения form_widget(form), каждое поле в форме визуализируется наряду с маркировкой и сообщением об ошибке (если таковое имеется). Однако, пока поле не будет достаточно гибким. Обычно, Вам будет необходимо визуализировать каждое поле формы отдельно, чтобы отслеживать внешний вид формы. Этому Вы научитесь в разделе «Визуализация формы в темплейте».

Прежде, чем идти дальше, обратите внимание на то, каким образом поле task получает значение свойства task из объекта $task (т.е. «Написать сообщение на блоге»). В этом заключается первая задача формы: изымать данные из объекта и переводить их в формат, пригодный для визуализации в HTML форме.

Система достаточно умна для того, чтобы получать доступ к значению защищённого свойства task, с помощью функций getTask() и setTask() класс Task. До тех пор, пока свойство доступно для совместного пользования, ему необходимо иметь «геттер» и «сеттер», чтобы компонент form смог получать и вставлять данные из/в свойства. Для свойства Boolean Вы можете использовать функцию «isser» (например, isPublished()) вместо геттера (например, getPublished()).

Добавление формы

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

// ...

public function newAction(Request $request)
{
// just setup a fresh $task object (remove the dummy data)
$task = new Task();

$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->getForm();

if ($request->getMethod() == 'POST') {
$form->bindRequest($request);

if ($form->isValid()) {
// perform some action, such as saving the task to the database

return $this->redirect($this->generateUrl('task_success'));
}
}

// ...
}

Теперь, при добавлении формы контроллер соединяет введённые данные в форму, которая переводит их в свойства task и dueDate объекта $task. Всё это происходит с помощью функции bindRequest().

После вызова функции bindRequest() добавленные данные немедленно переводятся в основной объект. Происходит это независимо от валидности основных данных.

Контроллер следует общей модели обработки форм и имеет три возможных направления:

1. При первой загрузке страницы в браузере используется функция GET. При это создаётся и визуализируется форма;

2. Если пользователь добавляет форму (т.е. использует функцию POST), используя неверные данные (валидация рассматривается в следующем разделе), эта форма создаётся и визуализируется с отображением всех ошибок при валидации;

3. Если пользователь добавляет форму с верными данными, форма создаётся и у Вас появляется возможность предпринять некоторые действия, используя объект $task (например, сохранить форму в базе данных), перед тем, как перенаправить пользователя на другую страницу (например, «thank you» или «success»).

Перенаправление пользователя после успешного добавления формы избавляет последнего от обновления страницы и повторного ввода данных.

Валидация формы

В предыдущем разделе Вы узнали как добавить форму с помощью верных или неверныз данных. В Symfony2 валидация применяется к основным объектам (например, Task). Иными словами, вопрос заключается не в валидности «формы», а в валидности объекта $task после приёма добавленных в него данных. Вызов функции $form->isValid() является ярлыком, запрашивающим объект $task с целью определения валидности содержащихся в нём данных.

Валидация выполняется путём добавления набора правил (называемых уточнениями) в класс. Чтобы увидеть процесс в действии, добавьте уточнения валидации. При этом поля task и dueDate должны быть заполненными, а поле dueDate должно представлять собой валидный DateTime объект.

YAML:

# Acme/TaskBundle/Resources/config/validation.yml
Acme\TaskBundle\Entity\Task:
properties:
task:
- NotBlank: ~
dueDate:
- NotBlank: ~
- Type: \DateTime

Annotations

// Acme/TaskBundle/Entity/Task.php
use Symfony\Component\Validator\Constraints as Assert;

class Task
{
/**
* @Assert\NotBlank()
*/
public $task;

/**
* @Assert\NotBlank()
* @Assert\Type("\DateTime")
*/
protected $dueDate;
}

XML:

<!-- Acme/TaskBundle/Resources/config/validation.xml -->
<class name="Acme\TaskBundle\Entity\Task">
<property name="task">
<constraint name="NotBlank" />
</property>
<property name="dueDate">
<constraint name="NotBlank" />
<constraint name="Type">
<value>\DateTime</value>
</constraint>
</property>
</class>

PHP

// Acme/TaskBundle/Entity/Task.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class Task
{
// ...

public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('task', new NotBlank());

$metadata->addPropertyConstraint('dueDate', new NotBlank());
$metadata->addPropertyConstraint('dueDate', new Type('\DateTime'));
}
}

Вот и всё! Если Вы повторно добавите форму, содержащую неверные данные, то увидите соответствующее сообщение об ошибке.

HTML5 Валидация

Что касается HTML5, многие браузеры имеют определённые встроенные валидационные уточнения со стороны пользователя. Самый обычный способ валидации авктивируется путём визуализации описателя required в обязательных полях. Для браузеров, поддерживающих HTML5, тот же результат достигается путём отображения встроенного сообщения в случае, когда пользователь пытается добавить форму с пустыми обязательными полями.

Генерированные формы полностью использует эту особенность путём добавления HTML описателей, приводящих в действие процесс валидации. Валидацию со стороны пользователя можно отключить, добавив описатель novalidate к тэгу form или formnovalidate для добавления. Это особенно полезно, когда Вы хотите протестировать уточнения валидации со стороны сервера, но браузер не позволяет этого сделать (например, добавить форму с пустыми полями).

Валидация — очень важный элемент Symfony2, которому посвящена отдельная глава.

Валидационные группы

Если Вы не используете валидационные группы, то можете пропустить этот раздел.

Если Ваш объект использует валидационные группы, Вам необходимо будет указать те из них, которые будут использованы формой:

$form = $this->createFormBuilder($users, array(
'validation_groups' => array('registration'),
))->add(...)
;

Если Вы используете классы форм (хорошая практика), в функцию getDefaultOptions() Вам необходимо добавить следующее:

public function getDefaultOptions(array $options)
{
return array(
'validation_groups' => array('registration')
);
}

В обоих случаях только группа registration будет использоваться для валидации основных объектов.

Встроенные типы полей:

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

Текстовые поля

  • text
  • textarea
  • email
  • integer
  • money
  • number
  • password
  • percent
  • search
  • url

Поля выбора

  • choice
  • entity
  • country
  • language
  • locale
  • timezone

Поля даты и времени

  • date
  • datetime
  • time
  • birthday

Другие поля

Группы полей

Скрытые поля

Основные поля

Вы также можете создавать собственные типы полей.

Опции типа поля

Каждый тип поля имеет набор опций, используемых для его настройки. Например, поле dueDate отображается в виде трёх окон выбора. Однако, поле date можно настроить таким образом, что оно будет отображаться в виде одного текстового окна (куда пользователь будет вводить дату в виде строки):

->add('dueDate', 'date', array('widget' => 'single_text'))

form symfony2
Каждый тип поля имеет набор различных, подходящих для него, опций. Многие из них соотносятся с конкретным типом поля (подробности можно узнать в документации по каждому типу).

Опция required

Наиболее обычной является опция required, которую можно применить к любому полю. По умолчанию, опция required имеет значение true. Это означает, что браузеры, поддерживающие HTML5 будут применять валидацию на стороне пользователя в случае, когда поле будет незаполненным. Если такая линия поведения Вас не устраивает, Вы можете установить значение false для опции required Вашего поля, либо отключить HTML5 валидацию.

Обратите внимание, что установка значения true для опции required не приведёт к применению валидации на стороне сервера. Иными словами, если пользователь добавляет в поле пустое значение (например, в совокупности со «старым» браузером или веб-сервисом), оно будет считаться валидным до тех пор, пока Вы не используете уточнения NotBlank или NotNull от Symfony.

Иными словами, опция required достаточно хороша, однако всегда следует использовать настоящую валидацию на стороне сервера.

«Угадывание» типа поля

Теперь, когда Вы добавили метаданные валидации в класс Task, Symfony получила информацию о используемых Вами полях. Если пожелаете, Symfony может «угадать» тип Вашего поля и установить его. В следующем примере Symfony, используя правила валидации, может «угадать», что поле task является обычным полем text, а поле dueDate является полем date:

public function newAction()
{
$task = new Task();

$form = $this->createFormBuilder($task)
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->getForm();
}

Процесс «угадывания» запускается, когда Вы пропускаете второй параметр функции add() (или вводите параметр null). Если Вы передаёте массив опций в качестве третьего параметра (что было сделано выше для dueDate), эти опции применяются в «угаданном» поле.

Если форма использует особую группу валидации, механизм, «угадывающий» тип поля, будет учитывать все валидационные уточнения во время «угадывания» типа поля (включая уточнения, не являющиеся частью использующихся валидационных групп).

«Угадывание» опций типа поля

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

Когда эти опции установлены, поле визуализируется с помощью особых HTML атрибутов, обеспечивающих HTML5 валидацию на стороне пользователя. Однако, при этом не генерируются эквивалентные уточнения на стороне сервера (например, Assert\MaxLength). Хотя Вам необходимо будет вручную добавлять валидацию на стороне сервера, эти опции типа поля затем могут быть «угаданы», благодаря введённой информации.

  • required: Опцию required можно «угадать», исходя из правил валидации (т.е. является ли поле NotBlank или NotNull) или из метаданных Doctrine (т.е. является ли поле nullable). Это очень полезно, поскольку валидация на стороне пользователя автоматически будет соответствовать правилам валидации.
  • min_length: Если поле является каким-либо текстовым полем, опцию min_length можно «угадать», благодаря валидационным уточнениям (при использовании MinLength или Min) или метаданным Doctrine (посредством длины поля).
  • max_length: Сходна с min_length, можно «угадать» максимальную длину.

Эти опции поля можно «угадать» только тогда, когда для «угадывания» типа поля Вы используете Symfony (т.е. пропуская или добавляя null в качестве второго параметра функции add()).

Если Вы захотите изменить одно из «угаданных» значений, ВЫ можете заменить его с помощью вставки опции в массив опций поля:

->add('task', null, array('min_length' => 4))

Визуализация формы в темплейте

Итак, Вы увидели, как можно визуализировать форму с помощью, всего лишь, одной строчки кода. Естественно, Вам потребуется большая гибкость в процессе визуализации:

Twig:

{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}

<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
{{ form_errors(form) }}

{{ form_row(form.task) }}
{{ form_row(form.dueDate) }}

{{ form_rest(form) }}

<input type="submit" />
</form>

PHP:

<!-- // src/Acme/TaskBundle/Resources/views/Default/newAction.html.php -->

<form action="<?php echo $view['router']->generate('task_new') ?>" method="post" <?php echo $view['form']->enctype($form) ?>>
<?php echo $view['form']->errors($form) ?>

<?php echo $view['form']->row($form['task']) ?>
<?php echo $view['form']->row($form['dueDate']) ?>

<?php echo $view['form']->rest($form) ?>

<input type="submit" />
</form>

Давайте рассмотрим каждый этап:

  • form_enctype(form) — Если хоть одно поле является полем загрузки файлов, обязательно визуализируется enctype=»multipart/form-data»;
  • form_errors(form) — Визуализирует общие для всей системы ошибки (ошибки отдельных полей отображаются вслед за каждым полем);
  • form_row(form.dueDate) — Визуализирует ярлык, любые ошибки, а также виджет HTML формы для заданного поля (например, dueDate) в элементе div (по умолчанию);
  • form_rest(form) — Визуализирует любое не визуализированное прежде поле. В нижней части каждой формы полезно размещать вызов данного хелпера (в случае, если Вы забыли вывести поле или не желаете трогать скрытые поля, настраиваемые вручную). Этот хелпер также полезен для использования автоматической защиты от CSRF.

Большую часть работы выполняет хелпер form_row, визуализирующий ярлык, ошибки и виджет HTML формы каждого поля в пределах тэга div (по умолчанию). В разделе «Темизация форм» Вы познакомитесь с тем, как можно модифицировать выход form_row на различных уровнях.

Визуализация каждого поля вручную

Хелпер form_row предоставляет Вам возможность быстро визуализировать каждое поле формы (также можно изменить разметку, используемую для «строки»). Однако, поскольку в жизни не всё так просто, Вы также можете визуализировать каждое поле вручную. Результат не будет отличаться от такового, достигнутого с использованием хелпера form_row:

Twig:

{{ form_errors(form) }}

<div>
{{ form_label(form.task) }}
{{ form_errors(form.task) }}
{{ form_widget(form.task) }}
</div>

<div>
{{ form_label(form.dueDate) }}
{{ form_errors(form.dueDate) }}
{{ form_widget(form.dueDate) }}
</div>

{{ form_rest(form) }}

PHP:

<?php echo $view['form']->errors($form) ?>

<div>
<?php echo $view['form']->label($form['task']) ?>
<?php echo $view['form']->errors($form['task']) ?>
<?php echo $view['form']->widget($form['task']) ?>
</div>

<div>
<?php echo $view['form']->label($form['dueDate']) ?>
<?php echo $view['form']->errors($form['dueDate']) ?>
<?php echo $view['form']->widget($form['dueDate']) ?>
</div>

<?php echo $view['form']->rest($form) ?>

Если созданный автоматически ярлык не совсем верен, Вы можете подкорректировать его:

Twig:

{{ form_label(form.task, 'Task Description') }}

PHP:

<?php echo $view['form']->label($form['task'], 'Task Description') ?>

Наконец, некоторые типы полей имеют дополнительные опции визуализации, которые можно ввести в виджет. Такие опции задокументированы для каждого типа, но существует опция attr, которая позволяет изменять атрибуты в элементе формы. Это добавляет класс task_field в визуализированное входное текстовое поле:

Twig:

{{ form_widget(form.task, { 'attr': {'class': 'task_field'} }) }}

PHP:

<?php echo $view['form']->widget($form['task'], array(
'attr' => array('class' => 'task_field'),
)) ?>

Справочник по функциям Twig темплейта

При использовании Twig Вам потребуется справочник по функциям визуализации форм. В этом справочнике Вы узнаете о доступных хелперах, а также об опциях, которые можно использовать с каждым из них.

Создание классов форм

Как Вы уже видели, форму можно создать и использовать непосредственно в контроллере. Однако, гораздо практичнее создавать формы в отдельных, обособленных PHP классах, которые затем можно повторно использовать в Вашем приложении. Создайте новый класс, который будет содержать логику построения формы задачи:

// src/Acme/TaskBundle/Form/Type/TaskType.php

namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('task');
$builder->add('dueDate', null, array('widget' => 'single_text'));
}

public function getName()
{
return 'task';
}
}

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

// src/Acme/TaskBundle/Controller/DefaultController.php

// add this new use statement at the top of the class
use Acme\TaskBundle\Form\Type\TaskType;

public function newAction()
{
$task = // ...
$form = $this->createForm(new TaskType(), $task);

// ...
}

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

Создание data_class

Для каждой формы необходимо указать имя класса, содержащего основные данные (например, Acme\TaskBundle\Entity\Task). Как правило, оно просто «угадывается», исходя из объекта, введённого в качестве второго аргумента для createForm (т.е. $task). Позднее, когда Вы начнёте добавлять формы, это уже будет неэффективно. Таким образом, хотя и не всегда необходимо, но, в общем, полезно уточнять опцию data_class, добавляя следующее в класс типа формы:

public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
);
}

Формы и Doctrine

Задача формы заключается в переводе данных из объекта (например, Task) в HTML форму с последующим переводом данных, отправленных пользователем, обратно в исходных объект. По сути, тема сохранения объекта Task в базе данных полностью отличается от темы форм. Однако, если Вы настроили класс Task на сохранение посредством Doctrine (т.е. Вы добавили для него метаданные маппинга), сохранение после добавления формы будет осуществляться в случае валидности формы:

if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($task);
$em->flush();

return $this->redirect($this->generateUrl('task_success'));
}

Если, по некоторым причинам, у Вас нет доступа к оригинальному объекту $task, Вы можете извлечь его из формы:

$task = $form->getData();

Ключевой момент, который необходимо понять, заключается в том, что если форма связана, добавленные данные немедленно передаются основному объекту. Если Вы хотите сохранить эти данные, необходимо просто сохранить сам объект (который уже содержит добавленные данные).

Вставленные формы

Часто, Вы можете пожелать создать форму, которая будет включаться в себя поля из различных объектов. Например, регистрационная форма может содержать данные, принадлежащие как объекту User, так и многим объектам Address. К счастью, сделать это несложно с помощью компонентов формы.

Вставка единичного объекта

Предположим, каждый Task принадлежит простому объекту Category. Начнём, конечно же, с создания объекта Category:

// src/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Category
{
/**
* @Assert\NotBlank()
*/
public $name;
}

Далее, добавляем новое свойство category в класс Task:

// ...

class Task
{
// . more info here..

/**
* @Assert\Type(type="Acme\TaskBundle\Entity\Category")
*/
protected $category;

// ...

public function getCategory()
{
return $this->category;
}

public function setCategory(Category $category = null)
{
$this->category = $category;
}
}

Тепеерь, когда Ваше приложение обновлено для отображения новых требований, создаём класс формы, чтобы объект Category мог быть изменён пользователем:

// src/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class CategoryType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}

public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Category',
);
}

public function getName()
{
return 'category';
}
}

Конечная цель заключается в том, чтобы сделать объект Category объекта Task изменяемым непосредственно в форме задачи. Чтобы завершить начатое, добавьте поле category в объект TaskType, тип которого является экземпляром нового класса CategoryType:

public function buildForm(FormBuilder $builder, array $options)
{
// ...

$builder->add('category', new CategoryType());
}

Теперь поля из CategoryType могут быть визуализированы вместе с таковыми из класса TaskType. Визуализируем поля Category таким же образом, как и оригинальные поля Task:

Twig:

{# ... #}

<h3>Category</h3>
<div class="category">
{{ form_row(form.category.name) }}
</div>

{{ form_rest(form) }}
{# ... #}

PHP:

<!-- ... -->

<h3>Category</h3>
<div class="category">
<?php echo $view['form']->row($form['category']['name']) ?>
</div>

<?php echo $view['form']->rest($form) ?>
<!-- ... -->

Когда пользователь добавляет форму, добавленные данные для полей Category используются для создания экземпляра Category, который затем размещается в поле category экземпляра Task.

К экземпляру Category можно получить доступ с помощью $task->getCategory(). Его также можно сохранить в базе данных или использовать различным образом.

Вставка собрания форм

Вы также можете вставить собрание форм в одну форму. Это можно сделать, используя тип поля collection.

Темизация форм

Каждый этап визуализации формы можно модифицировать. Вы можете изменить способ визуализации каждой «строки» формы, изменить разметку, используемую для отображения ошибок, или даже модифицировать способ виртуализации тэга textarea. Можно без ограничений использовать различные модификации в различных частях приложения.

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

В Twig каждый «фрагмент» формы представлен Twig блоком. Для изменения какой-либо части процесса визуализации Вам необходимо просто подменить соответствующий блок.

В PHP каждый «фрагмент» формы визуализируется с помощью индивидуального темплейт-файла. Чтобы изменить какую-либо составляющую процесса визуализации формы, Вам необходимо подменить существующий темплейт новым.

Чтобы понять принцип действия, давайте модифицируем фрагмент form_row и добавим атрибут класса элементу div, окружающему каждый ряд. Чтобы это сделать, создадим новый темплейт-файл, который будет хранить новую разметку:

Twig:

{# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}

{% block field_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock field_row %}

PHP:

<!-- src/Acme/TaskBundle/Resources/views/Form/field_row.html.php -->

<div class="form_row">
<?php echo $view['form']->label($form, $label) ?>
<?php echo $view['form']->errors($form) ?>
<?php echo $view['form']->widget($form, $parameters) ?>
</div>

Фрагмент формы field_row используется при визуализации большинства полей с помощью функции form_row. Чтобы сообщить компоненту формы использование определённого выше фрагмента field_row, в верхней части темплейта, визуализирующего форму, добавляем следующее:

Twig:

{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}

{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}

<form ...>

PHP:

<!-- src/Acme/TaskBundle/Resources/views/Default/new.html.php -->

<?php $view['form']->setTheme($form, array('AcmeTaskBundle:Form')) ?>

<form ...>

Тэг form_theme (в Twig) «импортирует» фрагменты, определённые в данном темплейте и использует их при визуализации формы. Иными словами, функция «form_row» вызывается позже в темплейте; она будет использовать блок field_row из Вашей специальной темы (вместо используемого по умолчанию блока field_row от Symfony).

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

Присваивание имён формам

В Symfony каждая часть визуализируемой формы — HTML элементы формы, ошибки, ярдыки и т.д. — определяется в основной теме, представляющей собой собрание блоков в Twig, и собрание темплейт-файлов в PHP.

В Twig любой требуемый блок определяется в одном темплейт-файле (form_div_layout.html.twig) который ведётся внутри Twig Bridge. Внутри этого файла Вы можете увидеть каждый блок, который необходим для визуализации формы, а также каждый, установленный по умолчанию, тип поля.

В PHP фрагменты являются отдельными темплейт-файлами. По умолчанию, они расположены в директории Resources/views/Form пакета фреймворка (смотрите GitHub).

Название каждого фрагмента задаётся согласно основному образцу и разбивается на две части, разделённые единичным нижним подчёркиванием (_). Вот несколько примеров:

  • field_row — используется form_row для визуализации большинства полей;
  • textarea_widget — используется form_widget для визуализации типа поля textarea;
  • field_errors — используется form_errors для визуализации ошибок для поля.

Каждый фрагмент использует единый основной образец: type_part. Частьtype соответствует типу визуализируемого поля (например, textarea, checkbox, date и т.д.), тогда как часть part отображает то, что визуализируется (например, label, widget, errors и т.д.). По умолчанию, существуют 4 возможные части предполагаемой визуализируемой формы:

label (например, field_label) Визуализирует ярлык поля

widget (например, field_widget) Визуализирует HTML отображение поля

errors (например, field_errors) Визуализирует ошибки поля

row (например, field_row) Визуализирует целый ряд поля (ярлык, виджет и ошибки)

Фактически, существуют 3 другие части: rows, rest и enctype, но подменять их Вам, в случае надобности, доведётся редко.

Определив тип поля (например, textarea), а также часть, которую Вы хотите модифицировать (например, widget), можно составить имя фрагмента, который следует подменить (например, textarea_widget).

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

В некоторых случаях может оказаться, что фрагмент, который Вы хотите модифицировать, отсутствует. Например, существует фрагмент textarea_errors в темах, предоставляемых Symfony по умолчанию. Каким же образом визуализируются ошибки для поля textarea?

Ответ: посредством фрагмента field_errors. Когда Symfony визуализирует ошибки для типа textarea, сначала производится поиск фрагмента textarea_errors перед тем, как вернуться к фрагменту field_errors. Каждый тип поля имеет, в свою очередь, родительский тип (для textarea родительским является тип field) и Symfony использует фрагмент для родительского типа в случае, если основной фрагмент отсутствует.

Таким образом, чтобы подменить ошибки только для поля textarea, создайте копию фрагмента field_errors, переименуйте его в textarea_errors и измените. Чтобы подменить заданную по умолчанию визуализацию ошибок для всех полей, скопируйте и измените непосредственно фрагмент field_errors.

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

Глобальная темизация форм

В вышеприведённом примере Вы использовали хелпер form_theme (в Twig), чтобы «импортировать» специальные фрагменты формы именно в эту форму. Вы также можете сообщить Symfony импорт изменений формы на протяжении всего проекта.

Twig

Для автоматического внесения во все темплейты модифицированных блоков из созданного ранее темплейта fields.html.twig, измените конфигурационный файл Вашего приложения:

YAML:

# app/config/config.yml

twig:
form:
resources:
- 'AcmeTaskBundle:Form:fields.html.twig'
# ...

XML:

<!-- app/config/config.xml -->

<twig:config ...>
<twig:form>
<resource>AcmeTaskBundle:Form:fields.html.twig</resource>
</twig:form>
<!-- ... -->
</twig:config>

PHP:

// app/config/config.php

$container->loadFromExtension('twig', array(
'form' => array('resources' => array(
'AcmeTaskBundle:Form:fields.html.twig',
))
// ...
));

Теперь любой блок внутри темплейта fields.html.twig используется глобально для определения выхода формы.

Customizing Form Output all in a Single File with Twig

В Twig Вы также можете модифицировать блок формы непосредственно в темплейте, там, где необходимы изменения:

{% extends ‘::base.html.twig’ %}

{# import «_self» as the form theme #}
{% form_theme form _self %}

{# make the form fragment customization #}
{% block field_row %}
{# custom field row output #}
{% endblock field_row %}

{% block content %}
{# … #}

{{ form_row(form.task) }}
{% endblock %}

Тэг {% form_theme form _self %} изменять блоки формы непосредственно в темплейте, который будет использовать эти изменения. Используйте этот способ для быстрой модификации выхода формы, которая понадобится только для единичного темплейта.

PHP

Чтобы автоматически поместить модифицированные темплейты из созданной ранее директории Acme/TaskBundle/Resources/views/Form во все темплейты, измените конфигурационный файл Вашего приложения:

YAML:

# app/config/config.yml

framework:
templating:
form:
resources:
- 'AcmeTaskBundle:Form'
# ...

XML:

<!-- app/config/config.xml -->

<framework:config ...>
<framework:templating>
<framework:form>
<resource>AcmeTaskBundle:Form</resource>
</framework:form>
</framework:templating>
<!-- ... -->
</framework:config>

PHP:

// app/config/config.php

$container->loadFromExtension('framework', array(
'templating' => array('form' =>
array('resources' => array(
'AcmeTaskBundle:Form',
)))
// ...
));

Теперь любой фрагмент внутри директории Acme/TaskBundle/Resources/views/Form используется глобально для определения выхода формы.

Защита от CSRF

CSRF, или Подделка межсайтовых запросов (Cross-site request forgery) — это способ, с помощью которого вредоносный пользователь пытается принудить Ваших пользователей неосознанно добавлять данные, которые добавлять не следует. К счастью, CSRF атаки можно предотвратить, используя символ CSRF в Ваших формах.

Отрадно также, что, по умолчанию, Symfony автоматически вставляет и проверяет CSRF символы. Это значит, что Вы можете пользоваться защитой от CSRF, не прилагая усилий. Фактически, каждая форма в этой главе использует защиту от CSRF!

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

Поле _token является скрытым, которое буде визуализироваться автоматически при использовании в Вашем темплейте функции form_rest(), которая удостоверяет, что все невизуализированные поля являются выходными.

Символ CSRF можно модифицировать на межформенном уровне. Например:

class TaskType extends AbstractType
{
// ...

public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
'csrf_protection' => true,
'csrf_field_name' => '_token',
// a unique key to help generate the secret token
'intention' => 'task_item',
);
}

// ...
}

Чтобы отключить защиту от CSRF, установите значение false для опции csrf_protection. Изменения также можно сделать глобально для всего проекта. Для получения подробной информации смотрите раздел «справочник по настройке форм».

Опция intention является выборочной, однако, она значительно повышает безопасность путём создания различных символов для каждой формы.

Заключение

Теперь Вы знаете всё о создании блоков, необходимых для создания сложных и функциональных форм для Вашего приложения. Создавая формы, не забывайте о том, что основная цель формы заключается переводе данных из объекта (Task) в HTML форму, чтобы пользователь мог их изменять. Другая задача формы заключается в приёме добавленных пользователем данных и применение их для объекта.

Однако, много чего ещё можно узнать о мире форм. Например, как выполнять загрузку файлов с помощью Doctrine, или как создать форму, в которую можно добавить динамическое количество подформ (например, список задач, в который Вы, перед добавлением, можете добавлять дополнительные поля с помощью Javascript). Всё это описано в «книге рецептов». Также необходимо опираться на справочную документацию по типам полей, включающую примеры использования каждого типа поля и его опции.