SymfonyMiddleTechnical

Что такое Symfony forms и как создавать и валидировать форму?

Форма Symfony описывается классом FormType на основе AbstractType, создаётся в контроллере через createForm(), обрабатывается handleRequest() и валидируется через isSubmitted() && isValid(). Данные автоматически маппятся на объект-модель.

Symfony Forms — назначение и архитектура

Компонент symfony/form предоставляет декларативный способ описания HTML-форм, их привязки к объектам (Data Objects), валидации и рендеринга. Форма описывается в отдельном классе FormType, который создаётся через AbstractType.

Создание FormType

<?php
// src/Form/RegistrationFormType.php
namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('email', EmailType::class, [
                'label' => 'Email address',
            ])
            ->add('plainPassword', PasswordType::class, [
                'mapped' => false,  // не маппится на свойство сущности
                'constraints' => [
                    new NotBlank(['message' => 'Please enter a password']),
                    new Length(['min' => 6, 'max' => 4096]),
                ],
            ])
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'constraints' => [
                    new IsTrue(['message' => 'You should agree to our terms.']),
                ],
            ]);
    }

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

Обработка в контроллере

<?php
// src/Controller/RegistrationController.php
namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;

class RegistrationController extends AbstractController
{
    #[Route('/register', name: 'app_register')]
    public function register(
        Request $request,
        UserPasswordHasherInterface $hasher,
        EntityManagerInterface $em
    ): Response {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $plainPassword = $form->get('plainPassword')->getData();
            $user->setPassword($hasher->hashPassword($user, $plainPassword));
            $em->persist($user);
            $em->flush();

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

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

Шаблон Twig

{{ form_start(registrationForm) }}
    {{ form_row(registrationForm.email) }}
    {{ form_row(registrationForm.plainPassword) }}
    {{ form_row(registrationForm.agreeTerms) }}
    <button type="submit">Register</button>
{{ form_end(registrationForm) }}

Валидация через аннотации/атрибуты на сущности

<?php
use Symfony\Component\Validator\Constraints as Assert;

class User
{
    #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
    #[Assert\NotBlank]
    private string $email;

    #[Assert\Length(min: 2, max: 50)]
    private string $name;
}

Подводные камни

  • isSubmitted() && isValid() — оба условия обязательны. Вызов isValid() без isSubmitted() всегда возвращает false и не бросает исключение — баг молчит.
  • Поле с mapped: false не попадёт в объект автоматически — нужно вручную получать данные через $form->get('fieldName')->getData().
  • CSRF-защита включена по умолчанию. При отправке формы через AJAX без CSRF-токена получите ошибку валидации без внятного сообщения.
  • Тип поля EntityType делает отдельный запрос к БД для каждой формы на странице — при списке из 100 форм это 100 лишних SELECT.
  • handleRequest() использует имя формы как префикс полей. Если имя FormType изменится, старые POST-запросы перестанут обрабатываться без ошибки.
  • Валидация атрибутами на сущности и constraints в полях формы работают независимо. Конфликтующие правила могут давать противоречивые сообщения.
  • При вложенных формах (CollectionType) добавление/удаление элементов через JS требует prototype и data-index — без этого новые элементы коллекции не пройдут маппинг.
  • form_end() автоматически рендерит незадекларированные поля (hidden, _token). Если опустить его и рендерить поля вручную, CSRF-токен не попадёт в форму.

Common mistakes

  • Сводить forms validation к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет forms validation через конкретную точку lifecycle в Symfony.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics