Компонент 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() выполняет:
- Проверяет HTTP-метод (POST/PUT/PATCH по умолчанию)
- Извлекает данные из
$request - Заполняет форму (и связанный объект) данными
- Запускает валидацию (если форма отправлена)
<?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()-- определяет поля черезFormBuilderInterfaceconfigureOptions()-- настройка формы,data_classсвязывает форму с объектомhandleRequest()->isSubmitted()->isValid()-- обязательный порядок- Без
data_classформа возвращает ассоциативный массив form_rest(form)иform_end(form)рендерят оставшиеся поля и CSRF-токен- CSRF-защита включена по умолчанию