Формы

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

Note

Компонент Symfony для работы с формами - это независимая библиотека, которая может быть использована вне проектов Symfony. Подробности ищите по ссылке Компонент по работе с формами на GitHub.

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

Предположим, что вы работаете над простым приложением - списком задач, которое будет отображать некоторые "задачи". Поскольку вашим пользователям будет необходимо создавать и редактировать задачи, вам потребуется создать форму. Но, прежде чем начать, сфокусируйтесь на базовом классе Task, который представляет и хранит данные для одной задачи:

 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
// src/AppBundle/Entity/Task.php
namespace AppBundle\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;
    }
}

Этот класс представляет собой обычный PHP-объект, так как на данный момент он не имеет ничего общего с Symfony или какой-либо другой библиотекой. Это обычный PHP-объект, который выполняет задачу непосредственно внутри вашего приложения (т.е. необходимость представления задачи в вашем приложении). Конечно же, к концу этой главы вы будете иметь возможность отправлять данные для экземпляра Task (посредством HTML-формы), валидировать её данные и сохранять их в базу данных.

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

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

 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
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use AppBundle\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class DefaultController extends Controller
{
    public function newAction(Request $request)
    {
        //  создайте задачу и задайте в ней фиктивные данные для этого примера
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, array('label' => 'Create Post'))
            ->getForm();

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

Tip

Этот пример показывает, как создать вашу форму непосредственно в коде вашего контроллера. Позднее, в секции ":ref:` Создание классов форм <form-creating-form-classes>`" вы узнаете, как создавать формы в отдельных классах, что является более предпочтительным вариантом, и сделает ваши формы доступными для повторного использования.

Создание формы требует относительно немного кода, так как объекты форм в Symfony создаются при помощи конструктора форм - "form builder". Цель конструктора форм – позволить вам писать простые «рецепты» форм, и брать всю сложную работу по построению форм на себя.

В этом примере вы добавили два поля в вашу форму - task и dueDate, соответствующие полям task и dueDate класса Task. Вы также указали каждому полю их «типы» (например TextType и DateType), представленные именем класса. Кроме всего, они определяют какой HTML тег(и) будет отображен для этого поля.

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

Symfony имеет много встроенных типов, которые будут вскоре описаны (см. Типы встроенных полей).

Отображение формы

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

  • Twig
    1
    2
    3
    4
    {# app/Resources/views/default/new.html.twig #}
    {{ form_start(form) }}
    {{ form_widget(form) }}
    {{ form_end(form) }}
    
  • PHP
    1
    2
    3
    4
    <!-- app/Resources/views/default/new.html.php -->
    <?php echo $view['form']->start($form) ?>
    <?php echo $view['form']->widget($form) ?>
    <?php echo $view['form']->end($form) ?>
    
_images/simple-form.png

Note

В этом примере предполагается, что вы отправляете форму на запрос «POST» и по тому же URL, по которому она была отображена. Позже вы узнаете, как изменить метод запроса и целевой URL формы.

Вот и все! Эти три строчки необходимы для отображения готовой формы:

form_start(form)
Отображает стартовый тег формы, включая правильный атрибут enctype во время использования файла загрузки.
form_widget(form)
Отображает все поля, которые включают в себя сам элемент поля, ярлык и любые сообщения ошибки валидации для поля.
form_end(form)
Отображает конечный тег формы и все поля, которые еще не были отображены, в случае, если вы отображали каждое поле самостоятельно. Это полезно для отображения скрытых полей и использования преимуществ автоматической защиты CSRF.
Как бы легко это не было, форма (пока) не гибкая. Обычно вам понадобится отображать каждое поле формы отдельно, чтобы вы могли контролировать внешний вид формы. Вы узнаете, как это делать в разделе "Отображение формы в шаблоне".

Прежде чем двигаться дальше, обратите внимание на то, как отображенное поле ввода task, имеет значение поля task объекта $task (например, "Write a blog post" – написать пост в блог). Это - первая задача формы: получить данные от объекта и перевести их в формат, подходящий для их последующего отображения в HTML форме.

Tip

Система форм достаточно умна, чтобы получить доступ к значению защищённого поля task через методы getTask() и setTask() класса Task. Если поле не публичное, оно должно иметь "геттер" и "сеттер" методы для того, чтобы компонент Form мог получать и передавать данные в свойство. Для булевых свойств вы также можете использовать метод "isser" или “hasser” (например, isPublished() или hasReminder()) вместо геттера (например,

getPublished() or getReminder()).

Эксплуатация отправки форм

По умолчанию, форма отправит запрос POST обратно к тому же самому контроллеру, который его отображает.

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

// ... use SymfonyComponentHttpFoundationRequest;

public function newAction(Request $request, EntityManagerInterface $em) {

// просто установите новый объект $task (удалите фиктивные данные) $task = new Task();

$form = $this->createFormBuilder($task)
->add('task', TextType::class) ->add('dueDate', DateType::class) ->add('save', SubmitType::class, array('label' => 'Create Task')) ->getForm();

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {

// $form->getData() holds the submitted values // но первоначальная переменная $task тоже была обновлена $task = $form->getData();

// ... . выполните действия, такие как сохранение задачи в базе данных // например, если Task является сущностью Doctrine, сохраните его! // $em->persist($task); // $em->flush();

return $this->redirectToRoute('task_success');

}

return $this->render('default/new.html.twig', array(
'form' => $form->createView(),

));

}

Caution

Помните, что метод createView() должен быть вызван после handleRequest(). Иначе изменения сделанные в *_SUBMIT, не будут применены к отображению (как ошибки валидации).

Этот контроллер следует типичному сценарию по обработке форм и имеет три возможных пути:

  1. При первичной загрузке страницы в браузере, форма просто создается и отображается.

    handleRequest() понимает, что форма не была отправлена и ничего не делает. isSubmitted() возвращает false, если форма не была отправлена;

  2. Когда пользователь отправляет форму, handleRequest() понимает это, и немедленно вписывает отправленные данные обратно в task и свойства dueDate объекта $task. Потом этот объект утверждается. Если он недействителен (валидация будет рассмотрена в следующей секции) isValid() возвращает false, и форма отображается со всеми ошибками валидации;

  3. Когда пользователь отправляет форму с действительными данными, отправленные данные, опять же, вписываются в форму, но в этот раз isValid() возвращает true. Теперь у вас есть возможность выполнить некоторые действия используя объект $task (например, отображение его в БД), перед тем, как перенаправлять пользователя на другую страницу (например, страницу «Спасибо» или «Успех»).

    Note

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

Если вам нужно больше контроля над тем, когда именно будет отправлена ваша форма, или какие данные передаются в нее, вы можете использовать submit(). Читайте больше об этом в Прямая отправка формы.

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

В предыдущей секции вы узнали, как форма может быть отправлена с валидными или не валидными данными. В Symfony валидация применяется к объекту, лежащему в основе формы (например, Task). Другими словами, вопрос не в том, валидна ли «форма», а валиден ли объект $task, после того как форма передала ему отправленные данные. Вызов метода $form->isValid() – это сокращение, которое спрашивает объект $task валидны ваши данные или нет.

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

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // src/AppBundle/Entity/Task.php
    namespace AppBundle\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class Task
    {
        /**
         * @Assert\NotBlank()
         */
        public $task;
    
        /**
         * @Assert\NotBlank()
         * @Assert\Type("\DateTime")
         */
        protected $dueDate;
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # src/AppBundle/Resources/config/validation.yml
    AppBundle\Entity\Task:
        properties:
            task:
                - NotBlank: ~
            dueDate:
                - NotBlank: ~
                - Type: \DateTime
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- src/AppBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
            http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="AppBundle\Entity\Task">
            <property name="task">
                <constraint name="NotBlank" />
            </property>
            <property name="dueDate">
                <constraint name="NotBlank" />
                <constraint name="Type">\DateTime</constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // src/AppBundle/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::class)
            );
        }
    }
    

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

Валидация - это очень мощная функция Symfony, о которой есть целая статья

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

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

  • Twig
    1
    2
    {# app/Resources/views/default/new.html.twig #}
    {{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
    
  • PHP
    1
    2
    3
    4
    <!-- app/Resources/views/default/new.html.php -->
    <?php echo $view['form']->form($form, array(
        'attr' => array('novalidate' => 'novalidate'),
    )) ?>
    

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

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

Hidden Fields

Base Fields

Вы также можете создать свои пользовательские типы полей. Эта тема раскрывается в статье Как создавать пользовательские типы поля.

Опции типов полей

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

1
->add('dueDate', DateType::class, array('widget' => 'single_text'))
_images/simple-form-2.png

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

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

1
2
3
4
->add('dueDate', DateType::class, array(
    'widget' => 'single_text',
    'required' => false
))

Отметим также, что установка опции required как true не приведет к запуску серверной валидации. Другими словами, если пользователь отправляет пустое значение для поля (при помощи старого браузера или веб-сервиса, к примеру) оно будет считаться валидным значением , разве что вы используете ограничения валидации Symfony NotBlank или NotNull.

Другими словами, опция required «хорошая», но настощая серверная валидация должна использоваться всегда.

Ярлык для поля формы может быть установлен с помощью опции label, которую можно применить к любому полю:

1
2
3
4
->add('dueDate', DateType::class, array(
    'widget' => 'single_text',
    'label'  => 'Due Date',
))

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

Определение типов полей

Теперь, когда вы добавили метаданные для валидации в класс Task, Symfony уже много знает о ваших полях. Если вы позволите, Symfony может предполагать ("угадывать") тип вашего поля и устанавливать его за вас. В этом примере, Symfony может предположить по правилам валидации, что поле task является нормальным полем TextType , а поле dueDate - полем типа DateType:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function newAction()
{
    $task = new Task();

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

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

Caution

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

Определение опций типов полей

В дополнение к определению "типа" поля, Symfony также может попытаться определить правильные значения ряда опций поля.

Tip

Когда эти опции будут установлены, поле будет отображено с использованием особых HTML атрибутов, которые позволяют выполнять клиентскую HTML5 валидацию (например, Assert\Length). И, несмотря на то, что вам нужно будет вручную добавлять правила серверной валидации, эти опции типов полей могут быть угаданы исходя из этой информации.

required
Опция required может быть определена исходя из правил валидации (т.е. если поле NotBlank или NotNull) или же на основании метаданных Doctrine (т.е. если поле nullable). Это очень полезно, так как правила клиентской валидации автоматически будут соответствовать правилам серверной валидации.
max_length
Если поле текстовое, тогда опция max_length может быть угадана по ограничениям валидации (если используется Length или Range) или по метаданным Doctrine (с помощью длины поля).

Caution

Эти опции полей предполагаются только если вы используете Symfony для угадывания типа поля (т.е. опускаете или передаете null в качестве второго аргумента add()).

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

1
->add('task', null, array('attr' => array('maxlength' => 4)))

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

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

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

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/AppBundle/Controller/DefaultController.php
use AppBundle\Form\TaskType;

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

    // ...
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use AppBundle\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => Task::class,
    ));
}

Tip

Во время отображения форм в объектах, отображаются все поля. Любые поля формы, которые не существуют на отображенном объекте, будут создавать исключение.

В случаях, когда вам понадобятся дополнительные поля в форме (например, чекбокс «согласны ли вы с этими условиями»), которые не будут отображены в базовом объекте, вам необходимо будет установить опцию mapped как false:

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

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('task')
        ->add('dueDate')
        ->add('agreeTerms', CheckboxType::class, array('mapped' => false))
        ->add('save', SubmitType::class)
    ;
}

Кроме того, если в форме есть какие-либо поля, не включенные в отправленные данные, эти поля будут явно установлены как null.

Доступ к данным полям можно получить в контроллере с помощью:

1
$form->get('agreeTerms')->getData();

Кроме того, данные неотображенного поля также могут быть изменены напрямую:

1
$form->get('agreeTerms')->setData(true);

Заключение

При построении форм, помните, что первичной целью формы является перевод данных из объекта (Task) в HTML-форму, чтобы пользователь мог изменять эти данные. Вторичная цель формы - брать данные, отправленные пользователем, и повторно применять их к объекту.

Можно узнать еще много информации и много мощных фишек в системе форм.

Узнать больше

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