MidТеория6 min

Типы форм и опции

Типы полей: TextType, EntityType, ChoiceType, CollectionType, наследование типов, data_class и маппинг

Иерархия типов форм

Каждый тип формы наследует от родительского типа. Корневой тип -- 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_label Twig вызовет __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, требует class
  • CollectionType: allow_add, allow_delete, by_reference => false для коллекций
  • getParent() -- определяет наследование типа; getBlockPrefix() -- Twig-блок
  • mapped => false -- поле не привязано к объекту данных
  • required -- HTML-атрибут, НЕ серверная валидация

Проверь себя

Что означает опция `required: true` в типе поля формы?

В каком формате задаются значения в массиве `choices` ChoiceType?

Какой метод определяет родительский тип формы при создании кастомного типа?

Зачем нужна опция `by_reference => false` в CollectionType?

Какой HTML-элемент рендерится при `expanded: true` и `multiple: false` в ChoiceType?