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.