Form Validation in Symfony
Form Validation in Symfony
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Right now, our project form accepts ANY value – even an empty name. Symfony's validator component solves this via declarative constraints, which we place EXACTLY where they logically belong: on the data class itself.
A dedicated data class instead of an array
So far, $form->getData() returned an array – for validation, we need a REAL PHP class with typed properties that constraints can attach to as attributes:
<?php
declare(strict_types=1);
namespace App\Form\Data;
use Symfony\Component\Validator\Constraints as Assert;
class ProjectData
{
#[Assert\NotBlank(message: 'Please enter a project name.')]
#[Assert\Length(
min: 3,
max: 100,
minMessage: 'The project name must be at least {{ limit }} characters long.',
maxMessage: 'The project name cannot be longer than {{ limit }} characters.',
)]
public string $name = '';
#[Assert\Length(max: 1000, maxMessage: 'The description cannot be longer than {{ limit }} characters.')]
public ?string $description = null;
}We deliberately place this class under src/Form/Data/ instead of src/Entity/ – in block 4, ProjectData gets replaced by the real Project entity, but the validation principle stays IDENTICAL.
Connecting the form type with data_class
use App\Form\Data\ProjectData;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['label' => 'Project Name'])
->add('description', TextareaType::class, ['label' => 'Description', 'required' => false])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => ProjectData::class]);
}
}data_class tells the form WHICH class to populate – $form->getData() now returns a ProjectData OBJECT instead of an array, AND isValid() automatically checks ALL constraints defined on that class.
Common constraints at a glance
| Constraint | Checks |
|---|---|
| #[Assert\NotBlank] | Must not be empty. |
| #[Assert\Length(min: ..., max: ...)] | Minimum/maximum length for strings. |
| #[Assert\Email] | Must be a valid email address (chapter 28 uses this for registration). |
| #[Assert\Range(min: ..., max: ...)] | Numeric value within a range. |
| #[Assert\Choice(choices: [...])] | Value must come from a fixed list (chapter 17 uses this for task status). |
| #[Assert\GreaterThan('today')] | For date values – useful for our task entity's due date. |
Validation errors get displayed automatically
Since {{ form(form) }} (chapter 15) already renders errors automatically, we don't need to change anything in the template – if validation fails, the configured message AUTOMATICALLY appears under the affected field.
Custom validation logic: the callback constraint
For rules that don't fit a built-in constraint (e.g. "due date can't fall on a weekend"), there's #[Assert\Callback]:
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[Assert\Callback]
public function validate(ExecutionContextInterface $context): void
{
if (str_contains($this->name, 'TODO')) {
$context->buildViolation('The project name must not contain "TODO".')
->atPath('name')
->addViolation()
;
}
}Tipp: Rule of thumb: ALWAYS prefer built-in constraints (NotBlank, Length, ...) when they cover the rule – reach for #[Assert\Callback] only for TRULY project-specific logic that can't be isolated to a single field.