Symfony Validator: Eigene Constraint-Klassen schreiben
AI generated
SF
{ }
Symfony · Validator · PHP · Constraints
Symfony Validator: Eigene Constraint-Klassen
für komplexe Validierungsregeln schreiben

Die eingebauten Symfony Validator-Constraints decken Standardfälle ab – aber Geschäftslogik wie eindeutige Slugs, domänenspezifische Formate oder Abhängigkeiten zwischen Feldern erfordern eigene Constraint-Klassen. Wer sie richtig implementiert, hält Validierungslogik testbar, wiederverwendbar und sauber von Controllern getrennt.

16 Min. Lesezeit Constraint · ConstraintValidator · Class-Level · Service-Injection Symfony 7.x · PHP 8.3+

1. Warum eigene Constraints statt Validierungslogik im Controller

Der häufigste Fehler bei der Symfony Validator-Integration ist das Verlagern von Validierungslogik in Controller oder Services, statt sie in eigenständige Constraint-Klassen auszulagern. Ein Controller, der prüft ob ein Slug eindeutig ist, ob eine IBAN zu einem Bankkonto passt oder ob Startdatum vor Enddatum liegt, enthält damit implizit Geschäftsregeln, die an vielen Stellen im Code gelten sollen. Diese Logik lässt sich nicht ohne Duplizierung wiederverwenden – und sie ist schwerer zu testen als eine isolierte Validator-Klasse.

Der Symfony Validator bietet mit dem Constraint-System genau dafür die richtige Abstraktion. Eine eigene Constraint-Klasse kapselt die Fehlermeldungen und Optionen, der zugehörige ConstraintValidator enthält die Prüflogik. Beide zusammen sind über PHP-Attribute an jede Eigenschaft oder Klasse anheftbar – ohne dass der Controller auch nur eine Zeile Validierungscode enthält. Symfony erkennt eigene Constraints automatisch per Autowiring, wenn der Validator die richtige Namenskonvention oder das ConstraintValidatorInterface implementiert.

Die Trennung hat einen weiteren Vorteil: Fehlersammlungen bleiben konsistent. Der Symfony Validator aggregiert alle Verletzungen über einen Validierungsdurchlauf, egal ob der Fehler aus einem Built-in-Constraint wie NotBlank oder aus einem eigenen Constraint kommt. Das Ergebnis ist ein ConstraintViolationList-Objekt, das einheitlich in Formular-Fehler, API-Antworten oder Logeinträge umgewandelt werden kann.

2. Aufbau einer Constraint-Klasse in Symfony

Eine eigene Constraint-Klasse in Symfony erbt von Symfony\Component\Validator\Constraint. Die Klasse definiert zwei Dinge: die Fehlermeldung als öffentliche Konstante oder Eigenschaft und optionale Konfigurationsparameter, die beim Einsetzen des Constraints angegeben werden können. Seit PHP 8.0 kann dieselbe Klasse als PHP-Attribut verwendet werden – dafür muss sie das Attribut #[Attribute] tragen. In Symfony 7 ist das der bevorzugte Ansatz, weil er keine separate YAML- oder XML-Konfiguration benötigt.

Die Methode validatedBy() gibt den Service-Namen des zugehörigen Validators zurück. Symfony leitet ihn per Konvention automatisch ab: Für App\Validator\UniqueSlug sucht der Container nach einem Service mit dem Namen App\Validator\UniqueSlugValidator. Wer eigene Service-Namen verwenden will, überschreibt validatedBy() und gibt den vollqualifizierten Klassennamen zurück. Die Methode getTargets() gibt an, ob der Constraint auf Properties (PROPERTY_CONSTRAINT), auf Klassen (CLASS_CONSTRAINT) oder auf beides angewendet werden darf. Ein fehlerhafter Target führt beim Validierungsdurchlauf zu einer klaren Ausnahme, nicht zu stiller Ignoranz.

Constraint-Optionen werden als konstruktorlose öffentliche Eigenschaften definiert. Seit PHP 8.1 mit Constructor Property Promotion werden sie direkt im Konstruktor deklariert, mit Standardwerten versehen und sind damit sofort per Attribut-Parameter setzbar: #[UniqueSlug(field: 'slug', message: 'Dieser Slug ist bereits vergeben.')]. Die Basisklasse Constraint übernimmt das Mapping der benannten Argumente auf die Eigenschaften im Konstruktor.


<?php

declare(strict_types=1);

namespace App\Validator;

use Symfony\Component\Validator\Constraint;

/**
 * Constraint that validates the uniqueness of a slug in the database.
 * Usage: #[UniqueSlug] on a string property or via YAML/XML config.
 */
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class UniqueSlug extends Constraint
{
    /**
     * Error message shown when the slug is already taken.
     * The { { value } } placeholder is replaced with the submitted value.
     */
    public string $message = 'Der Slug "{ { value } }" ist bereits vergeben.';

    /**
     * Entity class to check against — defaults to null (caller must set it).
     */
    public ?string $entityClass = null;

    /**
     * Field name to check for uniqueness.
     */
    public string $field = 'slug';

    /**
     * Optional: exclude a specific ID from the uniqueness check (for edit forms).
     */
    public ?int $excludeId = null;

    /**
     * @param string|null $message  Custom error message
     * @param string|null $entityClass  FQCN of the entity to query
     */
    public function __construct(
        public readonly array $groups = [],
        public readonly mixed $payload = null,
        ?string $message = null,
        ?string $entityClass = null,
        string $field = 'slug',
        ?int $excludeId = null,
        array $options = [],
    ) {
        parent::__construct($options, $groups, $payload);

        if ($message !== null) {
            $this->message = $message;
        }
        if ($entityClass !== null) {
            $this->entityClass = $entityClass;
        }
        $this->field     = $field;
        $this->excludeId = $excludeId;
    }
}

3. Den ConstraintValidator implementieren

Der ConstraintValidator ist die eigentliche Prüflogik. Er erbt von Symfony\Component\Validator\ConstraintValidator und implementiert die Methode validate(mixed $value, Constraint $constraint). Die erste Aufgabe in jeder validate()-Methode: Typprüfung des Constraints und Early Returns für Null- oder Leerstring-Werte, die der Symfony Validator mit separaten Constraints wie NotBlank behandeln soll. Das hält jeden Validator auf seinen Zuständigkeitsbereich fokussiert.

Wenn die Validierung fehlschlägt, erzeugt man über $this->context->buildViolation($constraint->message) eine Verletzung. Die Methoden der Builder-API erlauben es, Platzhalterwerte für die Fehlermeldung zu setzen (setParameter('{ { value } }', $value)), den Pfad der Verletzung innerhalb eines Objekts zu präzisieren (atPath('email')) und einen spezifischen Fehlercode zu vergeben (setCode(UniqueSlug::NOT_UNIQUE_ERROR)). Der abschließende Aufruf addViolation() trägt den Fehler in die ConstraintViolationList ein, ohne die Validierung abzubrechen – alle weiteren Constraints werden weiterhin geprüft.

Wichtig: Der ConstraintValidator darf keinen Zustand zwischen zwei Validierungsaufrufen halten. Er ist ein Service und wird als Singleton vom Container verwaltet. Alle zustandsbehafteten Daten gehören in lokale Variablen der validate()-Methode oder werden über Constructor Injection als stateless Services eingebracht. Diese Regel gilt auch für injizierte Repositories: Das Repository selbst ist zustandslos, nur der EntityManager hat Zustand – und der ist als Service scoped verwaltet.


<?php

declare(strict_types=1);

namespace App\Validator;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

/**
 * Validates that a slug value is unique within a given entity class.
 */
final class UniqueSlugValidator extends ConstraintValidator
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
    ) {}

    /**
     * Check that the given slug does not already exist in the database.
     *
     * @throws UnexpectedTypeException  if the constraint is not a UniqueSlug instance
     * @throws UnexpectedValueException if the value is not a string
     */
    public function validate(mixed $value, Constraint $constraint): void
    {
        // Type guard — ensures correct constraint class
        if (!$constraint instanceof UniqueSlug) {
            throw new UnexpectedTypeException($constraint, UniqueSlug::class);
        }

        // Skip validation for null and empty values — use NotBlank for those
        if (null === $value || '' === $value) {
            return;
        }

        if (!\is_string($value)) {
            throw new UnexpectedValueException($value, 'string');
        }

        if ($constraint->entityClass === null) {
            throw new \LogicException('UniqueSlug constraint requires entityClass to be set.');
        }

        // Build query dynamically based on entity class and field name
        $qb = $this->entityManager->createQueryBuilder()
            ->select('COUNT(e.id)')
            ->from($constraint->entityClass, 'e')
            ->where('e.' . $constraint->field . ' = :value')
            ->setParameter('value', $value);

        // Exclude current record when editing (prevents false positives)
        if ($constraint->excludeId !== null) {
            $qb->andWhere('e.id != :excludeId')
               ->setParameter('excludeId', $constraint->excludeId);
        }

        $count = (int) $qb->getQuery()->getSingleScalarResult();

        if ($count > 0) {
            // Build violation with placeholder replacement and specific error code
            $this->context->buildViolation($constraint->message)
                ->setParameter('{ { value } }', $value)
                ->setCode('UNIQUE_SLUG_NOT_UNIQUE')
                ->addViolation();
        }
    }
}

4. Services in ConstraintValidatoren injizieren

Der Symfony Validator integriert sich vollständig mit dem Symfony-Service-Container. Ein ConstraintValidator ist ein normaler Service und unterstützt Constructor Injection mit Autowiring. Damit lassen sich Repository-Klassen, externe API-Clients, Caches oder andere Services direkt in den Validator injizieren – ohne manuelle Service-Definition in services.yaml, solange Autowiring aktiviert ist. Das ist der entscheidende Vorteil gegenüber Validierungslogik im Controller: Der Validator bekommt alle Abhängigkeiten vom Container, nicht vom Controller.

Wer mehrere ConstraintValidatoren mit derselben Abhängigkeit hat, kann einen abstrakten Basis-Validator einführen, der die gemeinsamen Services per Constructor Property Promotion erhält. Alle konkreten Validators erben dann davon. Alternativ – und sauberer in modernem Symfony – ist das Delegate-Pattern: Der Validator delegiert komplexe Datenbankabfragen an einen dedizierten ValidationQueryService, der die SQL-Logik kapselt. So bleibt der Validator dünn und der Query-Service unabhängig testbar.

Ein häufiger Fehler bei Service-Injection in ConstraintValidatoren: zirkuläre Abhängigkeiten, wenn ein Validator eine Entity-bezogene Service-Klasse injiziert, die ihrerseits den Validator nutzt. Die Lösung ist eine saubere Schichtentrennung: Der Validator kennt nur das Repository oder einen Query-Service, niemals Application-Services oder Command-Handler, die selbst die Validierung triggern könnten.

5. Class-Level-Constraints für Cross-Field-Validierung

Property-Level-Constraints validieren eine einzelne Eigenschaft isoliert. Für Validierungsregeln, die mehrere Felder eines Objekts gleichzeitig betreffen – etwa dass Enddatum nach Startdatum liegt oder dass mindestens eine von zwei optionalen E-Mail-Adressen angegeben ist – braucht man Class-Level-Constraints im Symfony Validator. Der Unterschied liegt in der Methode getTargets(): Sie gibt Constraint::CLASS_CONSTRAINT zurück, und der ConstraintValidator empfängt das gesamte Objekt statt eines einzelnen Feldwerts.

In der validate()-Methode eines Class-Level-Validators ist das übergebene $value das vollständige Objekt. Man greift auf alle Felder zu und kann beliebig komplexe Abhängigkeiten prüfen. Verletzungen werden mit atPath('feldname') dem spezifischen Feld zugewiesen, damit Formular-Renderer die Fehlermeldung an der richtigen Stelle anzeigen. Ohne atPath() erscheint der Fehler auf Objekt-Ebene, was in Formularen oft am falschen Ort dargestellt wird.

Class-Level-Constraints werden als Attribut auf der Klasse deklariert, nicht auf einer Eigenschaft. Das Attribut-Target muss dann \Attribute::TARGET_CLASS beinhalten. Der häufige Anwendungsfall in Symfony-Projekten sind DTO-Klassen für Formulare und API-Requests: Das DTO enthält alle eingehenden Daten, der Class-Level-Constraint validiert das Zusammenspiel der Felder bevor irgendein Service die Daten verarbeitet.


<?php

declare(strict_types=1);

namespace App\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class-level constraint: validates that the end date is after the start date.
 * Applied on the DTO class, not on individual properties.
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
final class DateRangeValid extends Constraint
{
    public string $message = 'Das Enddatum muss nach dem Startdatum liegen.';

    // Returns CLASS_CONSTRAINT so Symfony passes the whole object to validate()
    public function getTargets(): string|array
    {
        return self::CLASS_CONSTRAINT;
    }
}

/**
 * Validator for the DateRangeValid constraint.
 * Receives the full DTO object and checks start/end date relationship.
 */
final class DateRangeValidValidator extends ConstraintValidator
{
    /**
     * Validate that endDate > startDate on the submitted DTO.
     */
    public function validate(mixed $value, Constraint $constraint): void
    {
        if (!$constraint instanceof DateRangeValid) {
            throw new UnexpectedTypeException($constraint, DateRangeValid::class);
        }

        // $value is the whole object (DTO/Entity) when getTargets() = CLASS_CONSTRAINT
        if (!method_exists($value, 'getStartDate') || !method_exists($value, 'getEndDate')) {
            return;
        }

        $startDate = $value->getStartDate();
        $endDate   = $value->getEndDate();

        // Skip if either date is null — use NotNull for that separately
        if ($startDate === null || $endDate === null) {
            return;
        }

        if ($endDate <= $startDate) {
            // atPath() pins the violation to the endDate field in the form
            $this->context->buildViolation($constraint->message)
                ->atPath('endDate')
                ->addViolation();
        }
    }
}

// Usage on a DTO class:
// #[DateRangeValid]
// final class EventCreateDto
// {
//     public ?DateTimeImmutable $startDate = null;
//     public ?DateTimeImmutable $endDate   = null;
// }

6. Validation Groups: Regeln kontextabhängig aktivieren

Der Symfony Validator unterstützt Validation Groups, um denselben Datentyp in unterschiedlichen Kontexten mit unterschiedlichen Regeln zu validieren. Beim Erstellen eines Benutzers ist ein Passwort-Feld Pflicht, beim Bearbeiten optional. Mit Validation Groups wird derselbe DTO-Klasse für beide Szenarien genutzt, und die Constraints erhalten eine groups-Option. Die Gruppe Default gilt immer, sofern keine expliziten Gruppen übergeben werden.

In Symfony-Formularen setzt man Gruppen über die validation_groups-Option des Formular-Typs oder über einen GroupSequenceProvider. Letzterer ermöglicht dynamische Gruppenauswahl basierend auf dem Objektzustand: Ist das Objekt neu, gelten Erstellungsregeln; ist es bereits persistiert, gelten Aktualisierungsregeln. Eigene Constraint-Klassen akzeptieren das groups-Attribut automatisch, weil es in der Basisklasse Constraint definiert ist.

Für API-Plattform-Integrationen im Symfony-Projekt steuert man Validation Groups per Operations-Konfiguration: Die POST-Operation verwendet ['Default', 'Create'], die PUT-Operation ['Default', 'Update']. Die eigenen Constraint-Klassen tragen dann gezielt die passende Gruppe, ohne dass ein eigener Validator dupliziert werden muss. Das reduziert den Wartungsaufwand erheblich, wenn sich Validierungsregeln zwischen Erstellung und Aktualisierung unterscheiden.

7. Constraints als PHP-Attribute einsetzen

Seit PHP 8.0 können Constraint-Klassen als native PHP-Attribute eingesetzt werden. Im Symfony-Ökosystem ist das seit Symfony 5.2 offiziell unterstützt und seit Symfony 7 der bevorzugte Konfigurationsweg. Der Vorteil: Die Validierungsregeln stehen direkt bei den Eigenschaften, sind IDE-unterstützt (Autocomplete, Typchecks) und unterliegen dem PHP-Parsing statt YAML-Parsing. Für eigene Constraint-Klassen muss die Klasse das native PHP-Attribut #[\Attribute] tragen, und der Konstruktor muss die Parameter definieren, die als Attribut-Argumente übergeben werden.

Beim Einsatz als Attribut ist zu beachten, dass PHP-Attribute keine Vererbung von Argumenten zwischen Klassen kennen. Der Konstruktor der eigenen Constraint-Klasse muss alle gewünschten Parameter explizit definieren. Die Basisklasse Constraint erwartet als erstes Argument im Konstruktor entweder ein Options-Array oder einzelne benannte Argumente. Symfony 7 empfiehlt die benannte Argumente-Variante, weil sie lesbar und IDE-freundlich ist.

Ein praktischer Tipp für Projekte mit vielen eigenen Constraints: Alle Constraint-Klassen in einem einzigen Namespace App\Validator zu halten und die Validator-Klassen direkt daneben (App\Validator\UniqueSlugValidator). Das macht die Konvention ClassName + Validator für den Symfony Validator automatisch nutzbar, ohne manuelle validatedBy()-Überschreibungen. PhpStan- und Psalm-Level-8-kompatible eigene Constraints sind möglich, wenn Generics für den Constraint-Typ verwendet werden.

8. Eigene Constraints unit-testen

Das Testen eigener ConstraintValidator-Klassen ist mit dem Symfony Validator-TestCase-Framework einfach. Die abstrakte Klasse ConstraintValidatorTestCase aus dem Paket symfony/validator stellt den Validierungskontext bereit und enthält Hilfsmethoden zum Prüfen von Verletzungen. Man erbt von ihr, gibt den vollqualifizierten Klassennamen des Validators zurück und kann in den Testmethoden direkt $this->validator->validate($value, new MyConstraint()) aufrufen. Nach der Validierung prüft man mit $this->assertNoViolation() oder $this->buildViolation($message)->assertRaised().

Für ConstraintValidatoren mit Service-Abhängigkeiten verwendet man PHPUnit-Mocks. Das Repository wird als Mock übergeben, der für bekannte Eingaben definierte Rückgabewerte liefert. So ist der Unit-Test vollständig von der Datenbank isoliert: Man testet ausschließlich die Entscheidungslogik des Validators, nicht die SQL-Korrektheit des Repositorys. Letztere testet man in separaten Repository-Integrationstests, die eine echte Datenbankverbindung nutzen.

Integrationstests mit dem vollständigen Symfony Validator-Service prüfen, ob die Constraint-Konfiguration im Container korrekt ist – also ob Autowiring greift, der Service gefunden wird und alle Dependencies gelöst werden. Dafür bootet man den Symfony-Kernel im Test und ruft den Validator aus dem Container. Diese Tests sind langsamer, aber notwendig, um Konfigurationsfehler zu finden, die im Unit-Test nicht sichtbar sind.

9. Vergleich: Built-in vs. Custom Constraints

Der Symfony Validator kommt mit über 70 eingebauten Constraints. Bevor man eine eigene Constraint-Klasse schreibt, lohnt ein Blick in die Dokumentation: Compound-Constraints kombinieren mehrere Built-in-Constraints zu einem, Callback-Constraints ermöglichen Inline-Validierung direkt in der Entity-Klasse, und Expression-Constraints nutzen die Symfony ExpressionLanguage für einfache Regel-Ausdrücke. Eigene Klassen sind nur dann wirklich nötig, wenn die Prüflogik Service-Abhängigkeiten, eigene Fehlertypen oder Class-Level-Kontext benötigt.

Anforderung Built-in Constraint Eigene Constraint-Klasse Empfehlung
Datenbankprüfung (Eindeutigkeit) UniqueEntity (Doctrine) Custom UniqueSlug mit Repository Custom für komplexe Queries
Cross-Field-Validierung Nicht direkt möglich Class-Level Constraint Immer eigene Klasse
Einfache Regelkombination Compound-Constraint Unnötig komplex Compound bevorzugen
Externe API-Validierung Nicht vorhanden Custom + Service-Injection Eigene Klasse mit HTTP-Client
Inline-Logik ohne Wiederverwendung Callback-Constraint Overkill Callback ausreicht

Die Entscheidung für eine eigene Constraint-Klasse fällt immer dann, wenn die Prüflogik wiederverwendet wird, externe Services benötigt werden oder die Verletzung mit einem spezifischen Fehlercode gemeldet werden soll. Callback-Constraints haben keinen Service-Zugriff und sind schwerer zu testen. Expression-Constraints sind gut für einfache Bedingungsausdrücke, die keine PHP-Logik brauchen.

Mironsoft

Symfony-Entwicklung, Validierungsarchitektur und PHP-Backend

Validierungslogik sauber in Symfony strukturieren?

Wir entwickeln eigene Symfony Validator Constraints, Class-Level-Validierungen und testbare Validierungsarchitekturen für komplexe PHP-Backends – vom DTO über den ConstraintValidator bis zur API-Integration.

Constraint-Design

Eigene Constraint-Klassen und Validator-Services für domänenspezifische Validierungsregeln

Cross-Field-Validation

Class-Level-Constraints für Abhängigkeiten zwischen Feldern und Kontext-sensitive Gruppen

Test-Abdeckung

Unit- und Integrationstests für alle Validators mit ConstraintValidatorTestCase und Mocks

10. Zusammenfassung

Eigene Symfony Validator Constraint-Klassen sind der richtige Weg, sobald Validierungslogik wiederverwendet werden soll, Service-Abhängigkeiten wie Repositories oder externe APIs benötigt werden oder mehrere Felder gemeinsam geprüft werden müssen. Die Trennung zwischen der Constraint-Klasse (Metadaten, Fehlermeldung, Optionen) und dem ConstraintValidator (Prüflogik) hält beide Teile klein, testbar und verständlich. Class-Level-Constraints lösen Cross-Field-Validierungen elegant ohne Duplizierung.

Die Integration mit dem Symfony-Service-Container über Autowiring macht ConstraintValidatoren zu vollwertigen Services, die beliebige Dependencies injiziert bekommen. Validation Groups erlauben kontextabhängige Aktivierung von Regeln, ohne separate DTOs für Erstellung und Aktualisierung schreiben zu müssen. Wer eigene Constraints konsequent einsetzt, hält Controller dünn, macht Validierungslogik unabhängig von der Transport-Schicht testbar und sorgt für konsistente Fehlermeldungen über die gesamte Anwendung.

Symfony Validator: Eigene Constraints — Das Wichtigste auf einen Blick

Constraint-Klasse

Erbt von Constraint, trägt #[\Attribute], definiert Fehlermeldung und Optionen. getTargets() bestimmt ob Property- oder Class-Level.

ConstraintValidator

Erbt von ConstraintValidator, implementiert validate(). Typprüfung zuerst, Early Return für null/leer, Verletzung mit buildViolation()->addViolation().

Service-Injection

Validator ist normaler Symfony-Service. Constructor Injection mit Autowiring – Repository, HTTP-Client oder Cache direkt injizierbar ohne manuelle Konfiguration.

Testen

ConstraintValidatorTestCase für Unit-Tests mit Mock-Dependencies. assertNoViolation() und buildViolation()->assertRaised() für klare Assertions.

11. FAQ: Symfony Validator und eigene Constraint-Klassen

1Wann brauche ich eine eigene Constraint-Klasse?
Bei Wiederverwendung, Service-Abhängigkeiten (Repository, HTTP-Client) oder Cross-Field-Prüfungen. Für einfache Inline-Logik reicht der Callback-Constraint.
2Constraint vs. ConstraintValidator?
Constraint = Metadaten-Objekt mit Fehlermeldung und Optionen. ConstraintValidator = Klasse mit der eigentlichen validate()-Logik. Beide zusammen bilden einen vollständigen Validator.
3Repository in Validator injizieren?
Per Constructor Injection. Der Validator ist normaler Symfony-Service mit Autowiring. EntityManager oder Repository direkt als readonly-Parameter deklarieren.
4Was sind Class-Level-Constraints?
Constraints auf Klassenebene – validate() empfängt das gesamte Objekt. Nötig für Abhängigkeiten zwischen Feldern. getTargets() gibt CLASS_CONSTRAINT zurück.
5Verletzung einem Feld zuweisen?
Mit atPath('feldname') im Builder. Ohne atPath erscheint die Verletzung auf Objekt-Ebene – in Formularen oft am falschen Ort.
6Wie teste ich einen eigenen Validator?
ConstraintValidatorTestCase als Basis. assertNoViolation() und buildViolation()->assertRaised(). Dependencies als PHPUnit-Mocks übergeben – kein echter Datenbankzugriff.
7Constraints als PHP-Attribut?
Die Constraint-Klasse muss #[\Attribute] tragen. Konstruktorparameter werden direkt als Attribut-Argumente übergeben. In Symfony 7 der bevorzugte Konfigurationsweg.
8Was sind Validation Groups?
Gruppen aktivieren Constraints kontextabhängig. Default gilt immer. Eigene Gruppen wie Create oder Update aktiviert man beim Validierungsaufruf oder in der Formular-Konfiguration.
9Wann Compound-Constraint statt eigener Klasse?
Compound kombiniert mehrere Built-in-Constraints ohne eigene Validator-Logik. Gut für Wiederverwendung ohne Service-Abhängigkeiten. Wenn Datenbankprüfung oder externe Logik nötig, eigene Klasse verwenden.
10Darf ein ConstraintValidator Zustand halten?
Nein. Der Validator ist ein Singleton-Service. Alle zustandsbehafteten Daten gehören in lokale Variablen der validate()-Methode, nie als Instanzvariable.