Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Custom Form Types in Symfony

Custom Form Types in Symfony

~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Beyond simple text fields, Symfony offers pre-built field types for recurring patterns – let's now model the task form with a status selector and a due date.

ChoiceType for fixed choice lists

src/Form/Data/TaskData.php
<?php

declare(strict_types=1);

namespace App\Form\Data;

use Symfony\Component\Validator\Constraints as Assert;

class TaskData
{
    #[Assert\NotBlank(message: 'Please enter a title.')]
    public string $title = '';

    #[Assert\Choice(choices: ['open', 'in_progress', 'done'])]
    public string $status = 'open';

    #[Assert\GreaterThanOrEqual('today', message: 'The due date cannot be in the past.')]
    public ?\DateTimeImmutable $dueAt = null;
}
src/Form/TaskType.php
<?php

declare(strict_types=1);

namespace App\Form;

use App\Form\Data\TaskData;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, ['label' => 'Title'])
            ->add('status', ChoiceType::class, [
                'label' => 'Status',
                'choices' => [
                    'Open' => 'open',
                    'In Progress' => 'in_progress',
                    'Done' => 'done',
                ],
            ])
            ->add('dueAt', DateType::class, [
                'label' => 'Due',
                'widget' => 'single_text',
                'required' => false,
            ])
        ;
    }

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

With ChoiceType, the array syntax ['Display Text' => 'stored_value'] is IMPORTANT: the KEY appears in the HTML as visible text, the VALUE is what actually gets stored – easy to mix up the first time.

'widget' => 'single_text' renders the date field as ONE single <input type="date"> – without this option, Symfony would default to THREE separate dropdown fields for day/month/year, unnecessary with modern browser date pickers.

Building your own, reusable form type

If SEVERAL forms need the same field combination (e.g. "pick a user from a dropdown list" for both project members and task assignment), a CUSTOM, reusable form type pays off:

src/Form/UserChoiceType.php
<?php

declare(strict_types=1);

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UserChoiceType extends AbstractType
{
    public function getParent(): string
    {
        return ChoiceType::class;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'choices' => [
                // Replaced in block 4 by a real database query of all users
                'Anna Schmidt' => 1,
                'Ben Meier' => 2,
            ],
        ]);
    }
}

getParent() declares that UserChoiceType BUILDS ON ChoiceType – it "inherits" its full behavior this way and only needs to define project-specific defaults. Used like any other form type: ->add('assignedTo', UserChoiceType::class).

Tipp: In block 4 (chapter 21), we'll replace the hardcoded choices list with EntityType – a special ChoiceType that loads options DIRECTLY from the database. The principle (a custom, reusable form type) stays identical.