MidТеория5 min

Основы форм

Компонент Form: создание формы, handleRequest, валидация, FormBuilderInterface, типы полей

Компонент Form

Form component в Symfony -- мощная система для создания, рендеринга и обработки HTML-форм. Компонент связывает HTML-форму с PHP-объектом (data class), автоматически заполняя объект данными из запроса.

Browser Form → Request → handleRequest() → Form → Data Object
                                              ↓
                                         Validation
                                              ↓
                                        isValid()

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

<?php

declare(strict_types=1);

namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => 'Product Name',
                'required' => true,
                'attr' => ['maxlength' => 255, 'placeholder' => 'Enter product name'],
            ])
            ->add('description', TextareaType::class, [
                'label' => 'Description',
                'required' => false,
                'attr' => ['rows' => 5],
            ])
            ->add('price', MoneyType::class, [
                'label' => 'Price',
                'currency' => 'EUR',
                'divisor' => 100, // Store in cents
            ])
        ;
    }

    public function configureOptions(OptionsResolver $options): void
    {
        $options->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}

Подвох экзамена: data_class связывает форму с конкретным PHP-классом. Если не указать data_class, форма вернёт ассоциативный массив вместо объекта. На экзамене часто проверяют, что происходит без data_class.

Обработка формы в контроллере

Стандартный паттерн

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class ProductController extends AbstractController
{
    #[Route('/product/new', name: 'product_new', methods: ['GET', 'POST'])]
    public function new(Request $request, EntityManagerInterface $em): Response
    {
        $product = new Product();

        // Create form bound to the Product object
        $form = $this->createForm(ProductType::class, $product);

        // Handle the request: populates form and $product with submitted data
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // $product is already populated with form data
            $em->persist($product);
            $em->flush();

            $this->addFlash('success', 'Product created!');

            return $this->redirectToRoute('product_show', ['id' => $product->getId()]);
        }

        return $this->render('product/new.html.twig', [
            'form' => $form,
        ]);
    }
}

Порядок вызовов: handleRequest, isSubmitted, isValid

<?php

declare(strict_types=1);

// CORRECT order:
$form->handleRequest($request);       // 1. Process request data
if ($form->isSubmitted()) {            // 2. Was the form submitted?
    if ($form->isValid()) {            // 3. Is validation passed?
        // Process data
    }
}

// COMMON shortcut:
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
    // Process data
}

// WRONG: calling isValid() before handleRequest()
// isValid() will always return false if form is not submitted

Подвох экзамена: isValid() сначала проверяет isSubmitted(). Если форма не была отправлена, isValid() вернёт false. Поэтому $form->isSubmitted() && $form->isValid() -- стандартный паттерн, хотя технически isValid() достаточно.

Что делает handleRequest()

handleRequest() выполняет:

  1. Проверяет HTTP-метод (POST/PUT/PATCH по умолчанию)
  2. Извлекает данные из $request
  3. Заполняет форму (и связанный объект) данными
  4. Запускает валидацию (если форма отправлена)
<?php

declare(strict_types=1);

// handleRequest checks the form name in request data
// For form named "product", it looks for $_POST['product']

// Manual alternative to handleRequest:
$form->submit($request->getPayload()->all());

// Submit with specific data
$form->submit([
    'name' => 'New Product',
    'price' => 1999,
]);

FormBuilderInterface

FormBuilderInterface -- основной инструмент для определения полей формы.

Основные методы

<?php

declare(strict_types=1);

namespace App\Form;

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

final class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // add(name, type, options)
            ->add('email', EmailType::class, [
                'label' => 'Email Address',
            ])
            ->add('password', RepeatedType::class, [
                'type' => PasswordType::class,
                'first_options' => ['label' => 'Password'],
                'second_options' => ['label' => 'Confirm Password'],
            ])

            // Remove a field
            ->remove('temporaryField')

            // Check if field exists
            // ->has('email')  // returns true

            // Get a specific field builder
            // ->get('email')

            // Set action and method
            ->setAction('/register')
            ->setMethod('POST')
        ;
    }
}

Передача опций в форму

<?php

declare(strict_types=1);

namespace App\Form;

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

final class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class)
            ->add('content', TextareaType::class)
        ;

        // Conditional fields based on custom options
        if ($options['include_published_at']) {
            $builder->add('publishedAt', DateTimeType::class);
        }
    }

    public function configureOptions(OptionsResolver $options): void
    {
        $options->setDefaults([
            'data_class' => Article::class,
            'include_published_at' => false,  // Custom option
        ]);

        // Type-safe option declaration
        $options->setAllowedTypes('include_published_at', 'bool');
    }
}
<?php

declare(strict_types=1);

// Usage in controller with custom option
$form = $this->createForm(ArticleType::class, $article, [
    'include_published_at' => true,
]);

Основные типы полей

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

Тип Назначение
TextType Однострочный текст <input type="text">
TextareaType Многострочный текст <textarea>
EmailType Email <input type="email">
PasswordType Пароль <input type="password">
SearchType Поиск <input type="search">
UrlType URL <input type="url">
TelType Телефон <input type="tel">

Числовые поля

Тип Назначение
IntegerType Целое число
NumberType Число с плавающей точкой
MoneyType Денежная сумма (с валютой)
PercentType Процент
RangeType Ползунок <input type="range">

Выбор

Тип Назначение
ChoiceType Select, radio, checkbox
EntityType Выбор Doctrine entity
EnumType PHP 8.1+ Enum (Symfony 6.1+)

Дата и время

Тип Назначение
DateType Дата
TimeType Время
DateTimeType Дата и время
BirthdayType Дата рождения

Прочие

Тип Назначение
CheckboxType Одиночный чекбокс
FileType Загрузка файла
HiddenType Скрытое поле
RepeatedType Два одинаковых поля (подтверждение пароля)
SubmitType Кнопка отправки

Рендеринг формы в Twig

{# Full form rendering #}
{{ form_start(form) }}
    {{ form_widget(form) }}
    <button type="submit" class="btn btn-primary">Save</button>
{{ form_end(form) }}

{# Field-by-field rendering (more control) #}
{{ form_start(form) }}
    <div class="mb-3">
        {{ form_label(form.name) }}
        {{ form_widget(form.name, {attr: {class: 'form-control'}}) }}
        {{ form_help(form.name) }}
        {{ form_errors(form.name) }}
    </div>

    <div class="mb-3">
        {{ form_row(form.description) }}   {# label + widget + errors in one #}
    </div>

    {{ form_rest(form) }}  {# Renders remaining fields (including CSRF) #}
{{ form_end(form) }}

Функции рендеринга

Функция Описание
form_start(form) Открывающий тег <form>
form_end(form) Закрывающий </form> + нерендеренные поля
form_widget(form.field) HTML-виджет поля
form_label(form.field) Метка <label>
form_errors(form.field) Ошибки валидации поля
form_help(form.field) Текст подсказки
form_row(form.field) label + widget + help + errors
form_rest(form) Все нерендеренные поля (включая CSRF-токен)

Подвох экзамена: form_end(form) автоматически рендерит все нерендеренные поля, включая CSRF-токен и скрытые поля. Если нужно отключить это поведение: {{ form_end(form, {render_rest: false}) }}. Но тогда CSRF-токен нужно рендерить вручную!

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

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class ContactController extends AbstractController
{
    public function contact(Request $request): Response
    {
        // Form without FormType class -- built directly in controller
        $form = $this->createFormBuilder()
            ->add('name', TextType::class)
            ->add('email', EmailType::class)
            ->add('message', TextareaType::class)
            ->getForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // getData() returns an associative array (no data_class)
            $data = $form->getData();
            // $data = ['name' => '...', 'email' => '...', 'message' => '...']
        }

        return $this->render('contact/form.html.twig', [
            'form' => $form,
        ]);
    }
}

CSRF-защита

Symfony автоматически добавляет CSRF-токен к формам.

<?php

declare(strict_types=1);

namespace App\Form;

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

final class DeleteType extends AbstractType
{
    public function configureOptions(OptionsResolver $options): void
    {
        $options->setDefaults([
            // CSRF protection (enabled by default)
            'csrf_protection' => true,
            'csrf_field_name' => '_token',
            'csrf_token_id' => 'delete_item',
        ]);
    }
}

Итоги

  • AbstractType::buildForm() -- определяет поля через FormBuilderInterface
  • configureOptions() -- настройка формы, data_class связывает форму с объектом
  • handleRequest() -> isSubmitted() -> isValid() -- обязательный порядок
  • Без data_class форма возвращает ассоциативный массив
  • form_rest(form) и form_end(form) рендерят оставшиеся поля и CSRF-токен
  • CSRF-защита включена по умолчанию

Проверь себя

Какой HTTP-метод по умолчанию ожидает `handleRequest()` для обработки формы?

Что произойдёт, если вызвать `$form->isValid()` ДО `$form->handleRequest($request)`?

Для чего нужна опция `divisor` в `MoneyType`?

Что делает `{{ form_end(form) }}` в Twig помимо закрытия тега `</form>`?

Что вернёт `$form->getData()`, если в `configureOptions` НЕ указан `data_class`?