Иерархия типов форм
Каждый тип формы наследует от родительского типа. Корневой тип -- FormType. Это означает, что все общие опции (label, required, attr, data) доступны в каждом типе.
FormType (base)
├── TextType
│ ├── EmailType
│ ├── PasswordType
│ ├── SearchType
│ ├── UrlType
│ └── TelType
├── ChoiceType
│ ├── EntityType
│ ├── EnumType
│ └── CountryType, LanguageType, etc.
├── DateType
│ └── BirthdayType
└── ...
ChoiceType -- универсальный выбор
ChoiceType -- самый гибкий тип для создания select, radio buttons и checkboxes.
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
final class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// Simple select
->add('status', ChoiceType::class, [
'choices' => [
'Pending' => 'pending', // label => value
'Processing' => 'processing',
'Shipped' => 'shipped',
'Delivered' => 'delivered',
],
'placeholder' => 'Choose a status',
])
// Radio buttons
->add('priority', ChoiceType::class, [
'choices' => [
'Low' => 'low',
'Medium' => 'medium',
'High' => 'high',
],
'expanded' => true, // Render as radio buttons
'multiple' => false, // Single selection
])
// Checkboxes (multiple selection)
->add('tags', ChoiceType::class, [
'choices' => [
'Urgent' => 'urgent',
'Fragile' => 'fragile',
'Gift' => 'gift',
],
'expanded' => true, // Render as checkboxes
'multiple' => true, // Allow multiple selections
])
// Grouped choices
->add('category', ChoiceType::class, [
'choices' => [
'Electronics' => [
'Phones' => 'phones',
'Laptops' => 'laptops',
],
'Clothing' => [
'Shirts' => 'shirts',
'Pants' => 'pants',
],
],
])
;
}
}
Комбинации expanded и multiple
expanded |
multiple |
HTML-элемент |
|---|---|---|
false |
false |
<select> (dropdown) |
false |
true |
<select multiple> |
true |
false |
Radio buttons |
true |
true |
Checkboxes |
Подвох экзамена: В
ChoiceTypeмассивchoicesимеет формат'Label' => 'value'(ключ -- метка, значение -- данные). Это обратный порядок относительно интуитивного ожидания. На экзамене часто путают порядок.
EntityType -- выбор Doctrine Entity
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Category;
use App\Entity\Product;
use App\Repository\CategoryRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
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)
// Basic EntityType -- loads ALL categories
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name', // Property to display
'placeholder' => 'Select category',
])
// EntityType with custom query
->add('parentCategory', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'query_builder' => fn (CategoryRepository $repo) =>
$repo->createQueryBuilder('c')
->where('c.active = :active')
->setParameter('active', true)
->orderBy('c.name', 'ASC'),
])
// EntityType with callable choice_label
->add('relatedProducts', EntityType::class, [
'class' => Product::class,
'choice_label' => fn (Product $product): string =>
sprintf('%s (%s EUR)', $product->getName(), $product->getPrice()),
'multiple' => true,
'expanded' => false,
])
;
}
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'data_class' => Product::class,
]);
}
}
Подвох экзамена:
EntityTypeтребует опциюclass(FQCN entity).choice_labelможет быть строкой (имя свойства) или callable. Безchoice_labelTwig вызовет__toString()на entity.
EnumType -- PHP Enum
<?php
declare(strict_types=1);
namespace App\Enum;
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
<?php
declare(strict_types=1);
namespace App\Form;
use App\Enum\OrderStatus;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\FormBuilderInterface;
final class OrderFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('status', EnumType::class, [
'class' => OrderStatus::class,
// choice_label defaults to enum ->name or ->value
'choice_label' => fn (OrderStatus $status): string => match ($status) {
OrderStatus::Pending => 'Pending Review',
OrderStatus::Processing => 'In Progress',
OrderStatus::Shipped => 'Shipped',
OrderStatus::Delivered => 'Delivered',
OrderStatus::Cancelled => 'Cancelled',
},
])
;
}
}
CollectionType -- динамические коллекции
CollectionType позволяет управлять коллекцией вложенных форм (добавление/удаление элементов).
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class)
// Collection of simple fields
->add('emails', CollectionType::class, [
'entry_type' => EmailType::class,
'entry_options' => ['label' => false],
'allow_add' => true, // Allow adding new entries
'allow_delete' => true, // Allow removing entries
'by_reference' => false, // Call setter (addEmail/removeEmail)
'prototype' => true, // Enable JS prototype for new entries
'prototype_name' => '__email__',
])
// Collection of embedded forms
->add('addresses', CollectionType::class, [
'entry_type' => AddressType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
;
}
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'data_class' => User::class,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Address;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('street', TextType::class)
->add('city', TextType::class)
->add('zipCode', TextType::class)
->add('country', CountryType::class)
;
}
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'data_class' => Address::class,
]);
}
}
Подвох экзамена: Опция
by_reference => falseкритически важна для коллекций. Без неё Symfony не вызоветaddAddress()/removeAddress()на родительском объекте, а попытается модифицировать коллекцию напрямую. Если Entity используетadd*/remove*методы -- ВСЕГДА ставьтеby_reference => false.
Рендеринг CollectionType в Twig
{{ form_start(form) }}
{{ form_row(form.name) }}
<h3>Email Addresses</h3>
<div id="emails-container"
data-prototype="{{ form_widget(form.emails.vars.prototype)|e('html_attr') }}">
{% for email in form.emails %}
<div class="email-entry">
{{ form_widget(email) }}
<button type="button" class="btn-remove">Remove</button>
</div>
{% endfor %}
</div>
<button type="button" id="add-email">Add Email</button>
{{ form_rest(form) }}
<button type="submit">Save</button>
{{ form_end(form) }}
Наследование типов форм
Создание кастомного типа на основе существующего.
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
// Custom type that extends TextType
final class PhoneType extends AbstractType
{
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'attr' => [
'placeholder' => '+7 (XXX) XXX-XX-XX',
'pattern' => '\+7\s?\(\d{3}\)\s?\d{3}-\d{2}-\d{2}',
],
]);
}
// This type inherits from TextType
public function getParent(): string
{
return TextType::class;
}
// Unique block prefix for Twig theming
public function getBlockPrefix(): string
{
return 'phone';
}
}
<?php
declare(strict_types=1);
// Usage
$builder->add('phone', PhoneType::class, [
'label' => 'Phone Number',
]);
Подвох экзамена: Метод
getParent()определяет родительский тип. По умолчаниюgetParent()возвращаетFormType::class.getBlockPrefix()определяет имя блока для Twig-темизации.
Общие опции всех типов
Все типы наследуют опции от FormType:
| Опция | Тип | Описание |
|---|---|---|
label |
string|false |
Метка поля |
required |
bool |
HTML-атрибут required (не валидация!) |
attr |
array |
HTML-атрибуты для виджета |
label_attr |
array |
HTML-атрибуты для label |
data |
mixed |
Значение по умолчанию |
mapped |
bool |
Привязано ли поле к свойству объекта |
disabled |
bool |
Неактивное поле |
help |
string |
Текст подсказки |
empty_data |
mixed |
Значение, когда поле пустое |
constraints |
array |
Constraints валидации |
row_attr |
array |
HTML-атрибуты для обёртки строки |
Опция mapped
<?php
declare(strict_types=1);
$builder
// This field is NOT mapped to any property on data_class
->add('agreeTerms', CheckboxType::class, [
'mapped' => false, // Won't try to call setAgreeTerms()
'constraints' => [
new IsTrue(['message' => 'You must agree to terms.']),
],
])
// This field is also not mapped -- for display purposes only
->add('calculatedTotal', MoneyType::class, [
'mapped' => false,
'disabled' => true,
'data' => 99.99,
])
;
Подвох экзамена:
required => trueдобавляет HTML-атрибутrequired, но НЕ добавляет валидацию на стороне сервера. Для серверной валидации нужен#[Assert\NotBlank]. Браузерную валидацию легко обойти, поэтому серверная валидация обязательна.
Data Mapping и empty_data
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ProductType extends AbstractType
{
public function configureOptions(OptionsResolver $options): void
{
$options->setDefaults([
'data_class' => Product::class,
// empty_data: what to create if no object is passed to form
'empty_data' => fn (FormInterface $form): Product => new Product(
name: $form->get('name')->getData() ?? '',
category: $form->get('category')->getData(),
),
]);
}
}
Итоги
ChoiceType:expanded+multipleопределяют рендеринг (select, radio, checkbox)EntityTypeтребуетclass,choice_label;query_builderдля фильтрацииEnumTypeработает с PHP Backed Enum, требуетclassCollectionType:allow_add,allow_delete,by_reference => falseдля коллекцийgetParent()-- определяет наследование типа;getBlockPrefix()-- Twig-блокmapped => false-- поле не привязано к объекту данныхrequired-- HTML-атрибут, НЕ серверная валидация