Форми (частина 1)

12/04/2016 0 symfony, шаблон, шаблон Twig

Мати справу з  HTML формами - одне з найтиповіших і найскладніших завдань для розробників. Symfony інтегрує компонент Form, що набагато полегшує цей процес. У цьому розділі, ми побудуємо складну форму з нуля, і разом покажемо найважливіші властивості бібліотеки форм.  

Компонент Symfony Form є автономною бібліотекою, яка доступна для користування поза проектами Symfony. Детальніше читайте тут.

Створюємо просту форму

Припустимо, ви створюєте додаток з простим списком завдань, яким має відображати  ті завдання. Оскільки, користувачам потрібно буде редагувати і створювати завдання, слід створити форму. Перш ніж почати, зверніть увагу на загальний клас Task, який репрезентує і зберігає дані для одного завдання:

// 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, це робиться шляхом створення об’єкта і задання його у шаблоні. На даний час, це можна зробити з середини контролера:  

// 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)
    {
        // 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', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, array('label' => 'Create Task'))
            ->getForm();

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

Цей приклад ілюструє як побудувати форму напряму з контролера. Пізніше, ви дізнаєтеся як побудувати форму у автономному класі, який рекомендовано створити, адже ваша форма багато раз використовується.  

Для сворення форми не потрібно багато коду, бо об’єкти форм у Symfony побудовані з  "будівником форм" (form builder). Мета будівника форм полягає у тому, що він дозволяє писати прості рецепти форм, і робити усі важкі завдання під час створення форми.  

У цьому прикладі ви додали два поля до вашої форми, а саме,  task та dueDate - відповідно до властивостей task та dueDate класу Task. Ви також додали їх тип  (наприклад, TextType та DateType), представивши повним ім’ям класу. Також, це визначає які теги HTML форм задаються цьому полю.  

Щоб зазначити тип форми, слід використовувати повне ім’я класу - наприклад  TextType::class у PHP 5.5+ чи Symfony\Component\Form\Extension\Core\Type\TextType.

Перед версією 2.8, ви могли користуватися псевдонімом для кожного типу, текстового чи дати. Старий синтакс працюватиме у всіх версіях Symfony до 3.0. Детальніше, читайте тут.

Нарешті, ми додали кнопку відправлення з міткою для відправки форми на сервер.

Кнопки відправлення появилися у Symfony 2.3. Перед тим, потрібно було додавати кнопки до HTML форм вручну.

Symfony має велику кількість вбудованих типів, які будуть обговорюватися пієніше у даному розділі.  

Задаємо форму

Тепер, коли ми створили форму, можемо відобразити її. Це можна зробити за допомогою спеціальго об’єкта форми "view" у вашому шаблоні (перегляньте  $form->createView() в контролері зверху), і за допомогою набору допоміжних функцій:

TWIG

{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

PHP

<!-- 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) ?>

Цей приклад передбачає, що ви відправляєте форму у тому самому запиті "POST"  і тій самій URL-адресі, у якій вона відображалася. Пізніше ви дізнаєтеся як змінити метод запиту і потрібну URL-адресу форми.

Так просто! Всього трішки коду, щоб відобразити завершену форму:  

form_start(form)

Відображає початковий тег форми, у тому числі правильний атрибут ENCTYPE при використанні завантаження файлів.

form_widget(form)

Відображає всі поля, які включають сам елемент поля, написи і повідомлення про помилки валідації для поля.

form_end(form)

Відображає кінцевий тег форми і будь-які поля, які ще не були відображені у випадку, якщо ви не задавали кожне поле самостійно. Це є досить корисним для того, щоб задати приховані поля, і користування автоматичним захистом CSRF.

Поки що це не є дуже зручно. Зазвичай, ви будете задавати форми кожному полю, щоб контролювати вигляд форми. У  підрозділі “Задаємо форми у шаблоні” ми розповімо про це детальніше.

Перед тим як ми продовжимо, зверніть увагу на те, як відображене поле введення task містить значення властивості task об’єкта $task (тобто "Написати блог" ). Найперше завдання форми: взяти дані з об’єкта і перетворити їх у зручний для форми HTML формат.

Система форм має доступ до значень захищеної властивості task через методи getTask() та setTask() у класі Task. Якщо ця властивість не є загальнодоступною вона повинна містити геттери і сеттери, щоб компонет форми міг діставати і надсилати дані з властивості. Для властивості логічного типу ви можете використати методи ‘isser’, або ‘hasser’ (наприклад, isPublished() чи hasReminder())замість геттерів (наприклад, getPublished() чи getReminder()).

Обробка форм

Наступне завдання форми це перетворити дані, відправлені користувачем назад до властивостей об’єкта. Для цього, відправлені користувачем дані мають бути записані в об’єкті форми. Додайте наступні функції до вашого контролера:

// ...
use Symfony\Component\HttpFoundation\Request;

public function newAction(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $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()) {
        // ... perform some action, such as saving the task to the database

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

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

Слід знати, що метод createView() повинен використовуватися після handleRequest. В іншому випадку, зміни, зроблені в *_SUBMIT не будуть застосовуватися (як помилки валідації).

Метод handleRequest() появився у Symfony 2.3. Перед цим змінна $request передавалася методу submit. Ця стратегія вже не використовується у Symfony 3.0.  

Контролер виконує все за звичайним зразком для обробки форм, і має три способи виконання:  

  1. Коли у браузері завантажується сторінка, створюється і надається форма. Метод handleRequest() розпізнає, що не було подано запиту на форму і не виконує ніяких дій. Метод  isValid() повертає false якщо не було запиту на форму.  

  2. Коли користувач відправляє форму, handleRequest() розпізнає її, і відразу повертає дані назад до властивостей task і dueDate об’єкта $task. Після цього об’єкт перевіряється. Якщо він непройшов валідацію, isValid() знову повертає false, щоб форма надалася разом з повідомленням про помилки під час валідації.

Ви також можете використати метод  isSubmitted() щоб перевірити чи форма була відправлена, незважаючи чи дані вірні.  

Коли користувач відправляє форму з правильними даними, вони записуються у форму, і у цьому випадку isValid() повертає true. Тепер ви можете виконувати деякі дії, використовуючи об’єкт $task (наприклад, збереження даних в базу даних ), перед перенаправленням користувача до іншої сторінки (наприклад, до сторінки “дякую”, або “все пройшло успішно”).

Перенаправлення користувача після успішного заповнення і відправки форми, користувачу не потрібно натискати кнопку для оновлення у браузері і знову вводити дані.

Якщо ви хочете більше конролювати процес відправки форми, або які саме дані відсилаються, використовуйте submit(). Дивіться більше у довіднику

Відправка форми з кількома кнопками

Підтримка кнопок для форм була представлена у Symfony 2.3.
Коли форма має більше ніж одну кнопку відправлення даних, вам потрібно буде перевірити, які з кнопок були натиснуті, щоб адаптувати програмний потік в контролері. Щоб це зробити, додайте кнопку з написом “Save and add” ("Зберегти і додати")

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

У вашому контролері, використовуйте метод isClicked() для запиту, щоб перевірити чи  кнопка “Save and add” ("Зберегти і добавити") була натиснена.  

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

    $nextAction = $form->get('saveAndAdd')->isClicked()
        ? 'task_new'
        : 'task_success';

    return $this->redirectToRoute($nextAction);
}

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

Ви вже знаєте як можна відправити форму з вірними або не вірними даними. У Symfony, валідація використовується до базового об’єкта (наприклад, Task). Іншими словами, питання стоїть не в тому чи правильна форма, а в тому, чи після обробки даних формою правильний об’єкт $task є дійсним.

Валідація виконується процесом додавання набору правил (обмежень) до класу. Щоб побачити це на практиці, додайте такі обмеження для перевірки: поля task і dueDate повинні бути заповненими, а об’єкт DateTime має бути дійсним.  

Анотації

// 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

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

XML

<!-- 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

// 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')
        );
    }
}

Ось так! Якщо ви перевідправите форму з невірними даними, на екрані, разом з формою, ви побачите відповідні помилки.

HTML5 валідація

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

Згенеровані форми мають з цього користь, додаючи властивості HTML, які і дають поштовх процесу валідації. Однак, перевірка на стороні клієнта, може бути відключена шляхом додавання атрибуту  novalidate до тегів form або formnovalidate до тегу відправлення. Це стане у нагоді, коли ви захочете протестувати обмеження на стороні сервера, але не зможете через те, що форма у вашому браузері відправляє форми з незаповненими полями.

TWIG

{# app/Resources/views/default/new.html.twig #}
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}

PHP

<!-- app/Resources/views/default/new.html.php -->
<?php echo $view['form']->form($form, array(
   'attr' => array('novalidate' => 'novalidate'),
)) ?>

Валідація - це потужна властивість Symfony. Детальніше про цей процес читайте тут. (лінк на попередній розділ).

Групи перевірки

Якщо ваш об’єкт використовує групи перевірки, вам слід уточнити, яку групу повинна використовувати форма.

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

Метод configureOptions() появився у Symfony 2.7. Раніше, замість нього використовувався метод setDefaultOptions().

Якщо ви створюватимете класи форм (це дуже хороша практика), тоді вам потрібно буде додати до методу configureOptions() наступне:

use Symfony\Component\OptionsResolver\OptionsResolver;

public function configureOptions(OptionsResolver $resolver)
{
   $resolver->setDefaults(array(
       'validation_groups' => array('registration'),
   ));
}

В обох випадках, для валідації базового об’єкта буде використовуватися лише група перевірки registration.

Відключення перевірки

Можливість задавати значення “false” до validation_groups стало можливим у Symfony 2.3.

Іноді корисно, повністю відключити перевірку форми. Для цього задайте validation_groups опцію false:

use Symfony\Component\OptionsResolver\OptionsResolver;

public function configureOptions(OptionsResolver $resolver)
{
   $resolver->setDefaults(array(
       'validation_groups' => false,
   ));
}

Зверніть увагу, що коли ви зробите це, форма все одно буде робити основну перевірку. Перевірятиме, наприклад, чи завантажений файл занадто великий або були заповнені неіснуючі поля. Якщо ви хочете відключити перевірку, ви можете використати POST_SUBMIT event.

Групи, засновані на відправлених даних

Якщо вам важко визначити групи перевірки (базуючись, наприклад, на відправлених даних), ви можете встановити опцію validation_groups на масив зворотнього виклику.

use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
   $resolver->setDefaults(array(
       'validation_groups' => array(
           'AppBundle\Entity\Client',
           'determineValidationGroups',
       ),
   ));
}

Це дасть запит на статичний метод  determineValidationGroups() у класі Client, після відправлення форми, проте перед виконанням перевірки. Об’єкт Form є аргументом для цього методу. А ще ви можете використати Closure:

use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
   $resolver->setDefaults(array(
       'validation_groups' => function (FormInterface $form) {
           $data = $form->getData();

           if (Client::TYPE_PERSON == $data->getType()) {
               return array('person');
           }

           return array('company');
       },
   ));
} 

Використання опції validation_groups перевершує групу перевірки по замовчуванню, яка раніше використовувалася. Якщо ви будете перевіряти обмеження класу по замовчуванню, вам потрібно буде налаштувати опцію наступним чином:  

use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
   $resolver->setDefaults(array(
       'validation_groups' => function (FormInterface $form) {
           $data = $form->getData();

           if (Client::TYPE_PERSON == $data->getType()) {
               return array('Default', 'person');
           }

           return array('Default', 'company');
       },
   ));
}

Ви можете знайти більше інформації про групи перевірки і роботу обмежень за замовчуванням у розділі “Валідація” (лінк на попередній чаптер)  

Групи, що базуються на натисненні кнопки

Використання кнопок у формах з’явилося у Symfony 2.3.

Коли ваша форма містить багато кнопок відправлення, ви можете змінити групу перевірки залежно від того яка кнопка буде призначена для відправки самої форми.  

Розглянемо, наприклад, форму в експертний системі (майстрі), яка дозволяє перейти до наступного кроку або повернутися до попереднього. Також, запам’ятайте, що коли ви будете поветатися до попереднього етапу, дані повинні бути збережені, проте не перевірені.

Для початку, додамо дві кнопки до форми:

$form = $this->createFormBuilder($task)
   // ...
   ->add('nextStep', SubmitType::class)
   ->add('previousStep', SubmitType::class)
   ->getForm();

Далі, ми налаштуємо кнопку для того, щоб повернутися на попередній етап і ввімкнути певні групи обмежень. У даному прикладі, ми хочемо вимкнути валідацію, тому ми налаштовуємо опції validation_groups на “false”:

$form = $this->createFormBuilder($task)
   // ...
   ->add('previousStep', SubmitType::class, array(
       'validation_groups' => false,
   ))
   ->getForm();

Тепер, створена вами форма омине обмеження. Вона перевірятиме основні, цілісні обмеження: перевірятиме чи  завантажений файл не завеликий, або чи не був введений текст у поле де мали б бути числа.

Типи вбудованих полів

Як і інші додатки, Symfony містить велику кількість типів полів, до яких входять усі типові поля форм і типи даних, з якими ви будете зіштовхуватися. 

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

Поля вибору

Поля дати і часу

Інші поля

Групи полів

Приховані поля

Кнопки

Основні поля

Ви маєте можливість створити свої, користувацькі типи полів. Детальніше дізнавайтесь у довіднику

Опції типів полів

Кожне поле можна налаштувати, користуючись даними опціями. Наприклад, dueDate відображається у формі трьох полів з вибором (select boxes). Проте тип даних можна налаштувати, таким чином, щоб він відображався як одне текстове поле (де користувач вводитиме текстові дані (string)):  

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

Кожен тип поля містить різноманітні опції, які виконуються. Багато з них належать до певного типу поля, і у документації The Symfony Book можна знайти ці деталі.  

Опція required

Найтиповішою опцією є required, яка може використовуватися з будь-яким полем. За замовчуванням опція required має значення true, це означає, що браузери які підтримують HTML5, робитимуть перевірку на стороні клієнта, якщо поле порожнє. Якщо ви не хочете щоб ця опція виконувалася, вимкніть валідацію HTML5, або налаштуйте значення опції required на false:

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

Також, візьміть до уваги те, що налаштування опції required на true не призведе до перевірки на стороні сервера. Іншими словами, якщо користувач відправляє порожнє значення у полі (або зі старого браузера чи сервера), це прийматиметься за дійсні дані, якщо ви не будете використовувати обмеження NotBlank чи NotNull. Опція required - непоганий варіант, проте перевірка на стороні сервера повинна використовуватися завжди.

Опція “label” (мітка)

Мітка для форми поля можна налаштувати опцією label, яка може використовуватися у будь-якому полі:

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

Мітка для поля може бути встановлена в шаблоні, що відображає форму, але про це ми роповімо пізніше. Якщо вам не потрібна пов'язана з введенням мітка, ви можете вимкнути її, встановивши значення false.

Тип поля “guessing” (“процес вгадування”)

Тепер, коли ми вже додали метадані валідації до класу Task, Symfony матиме деякі уявлення про наші поля. Якщо ви забажаєте, Symfony може “вгадати” тип поля і налаштувати його замість вас. У цьому прикладі, Symfony може “здогадатися” з обмежень, що поле task є звичайним полем TextType, а поле dueDate є полем DateType:

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 у попередньому прикладі), то усі ці опції виконуватимуться у полі, яке вгадується.  

Якщо ваша форма використовує певну групу перевірки, тип поля “guesser” буде брати до уваги усі обмеження при “відгадуванні”  вашого типу поля (включаючи обмеження, які не є частиною використаних груп обмежень).

Вгадування опцій типів полів

Крім того що фреймворк може “вгадати” тип поля, Symfony може також “відгадати” правильні значення кількох опцій полів.  

Коли ці опції будуть встановлені, поле буде містити спеціальні атрибути HTML, що забезпечують перевірку на стороні клієнта. Проте, вони не згенерують еквівалентні обмеження на стороні сервера (наприклад, Assert\Length). І хоча вам буде потрібно   вручну додати перевірку на стороні сервера, Symfony може “вгадати” ці опції типів полів з інформації, яка буде дана.  

required

Опцію required Symfony може “вгадати”, базуючись на правилах перевірки (тобто, це будуть поля NotBlank або NotNull), або метадані Doctrine (тобто, поле nullable). Це є надвичайно зручно, так як перевірка на стороні клєнта автоматично підходитиме правилам перевірки.

max_length

Якщо поле є текстовим,  опцію max_length можна вгадати з обмежень (якщо ви використовували Length or Range), або з метаданих Doctrine (через довжину поля).

Ці опції полів можна “вгадати” лише у тому випадку, якщо ви використали Symfony, щоб “вгадати” тип поля (тобто, опустити або використати null як другий аргумент до add()).

Якщо ви бажаєте змінити одне з “відгаданих” значень, ви можете перевизначити його шляхом передачі опції в масив поля опцій: 

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

Поділитися