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

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:

src/Form/Data/ProjectData.php
<?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

src/Form/ProjectType.php
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

ConstraintChecks
#[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.