Skip to content

Validation

Objektgraph-Validierung mit 21 eingebauten Validatoren, fluenter Feld-Konfiguration und automatischer Traversierung.

Einführung

Validierung in PHP läuft oft auf eines von zwei Extremen hinaus: entweder ein Framework-Monolith mit Annotations, Reflection-Magie und hunderten Klassen, oder handgeschriebene if-Ketten in jedem Use Case, die mit dem ersten Refactoring brechen.

jardissupport/validation geht einen dritten Weg. Das Package validiert ganze Objektgraphen automatisch (verschachtelte Objekte, Arrays von Entities, zirkuläre Referenzen) und braucht dafür weder Annotations noch Attribute:

  • Automatische Objektgraph-Traversierung — einen Validator für Order und einen für OrderLine registrieren, und die Validierung findet alle verschachtelten Objekte von selbst
  • 21 eingebaute Validatoren — E-Mail, UUID, IBAN, Kreditkarte, Phone, IP, URL, JSON, Regex und mehr
  • Fluente Feld-Konfiguration->field('email')->validates(Email::class, Email::strict()) statt kryptischer Options-Arrays
  • Break-Modus — Guard-Validierung: wenn eine Vorbedingung fehlschlägt, wird der Rest übersprungen
  • Partial Updates — Felder bei Create überspringen, bei Update validieren, ohne zwei separate Validatoren
  • Zirkuläre Referenzenspl_object_id()-basierte Erkennung verhindert Endlosschleifen
  • Statische Helper als Option-FactoriesEmail::strict(), Range::between(1, 100), Uuid::v4() statt Option-Key-Raten

Installation

bash
composer require jardissupport/validation

GitHub: jardisSupport/validation

Grundlegende Nutzung

Feld-Validierung mit fluenter API

php
use JardisSupport\Validation\CompositeFieldValidator;
use JardisSupport\Validation\Validator\Email;
use JardisSupport\Validation\Validator\Length;
use JardisSupport\Validation\Validator\NotBlank;
use JardisSupport\Validation\Validator\Range;
use JardisSupport\Validation\Validator\Uuid;

$validator = new CompositeFieldValidator();
$validator
    ->field('id')
        ->validates(Uuid::class, Uuid::v4())
    ->field('email')
        ->validates(Email::class, Email::strict())
    ->field('username')
        ->validates(NotBlank::class)
        ->validates(Length::class, Length::between(3, 20))
    ->field('age')
        ->validates(Range::class, Range::between(18, 120))
    ->end();

$result = $validator->validate($user);

if (!$result->isValid()) {
    $result->getErrors();              // ['email' => ['Invalid email'], 'age' => ['...'], ...]
    $result->getFieldErrors('email');   // ['Invalid email address']
    $result->getFirstError('email');    // 'Invalid email address'
    $result->getErrorCount();           // 2
}

Objektgraph-Validierung

php
use JardisSupport\Validation\ObjectValidator;
use JardisSupport\Validation\ValidatorRegistry;

$orderValidator = new CompositeFieldValidator();
$orderValidator
    ->field('customerId')->validates(Uuid::class, Uuid::v4())
    ->field('totalAmount')->validates(Positive::class);

$lineValidator = new CompositeFieldValidator();
$lineValidator
    ->field('productId')->validates(Uuid::class, Uuid::v4())
    ->field('quantity')->validates(Range::class, Range::min(1));

$registry = new ValidatorRegistry();
$registry
    ->register(Order::class, $orderValidator)
    ->register(OrderLine::class, $lineValidator);

$validator = new ObjectValidator($registry);
$result = $validator->validate($order);

// Fehler nach Kurzklassenname gruppiert:
// ['order' => ['totalAmount' => ['...']], 'orderLine' => ['quantity' => ['...']]]

Der ObjectValidator traversiert automatisch alle public und protected Properties des Objekts, findet verschachtelte Objekte und Arrays, und validiert alles, wofür ein Validator in der Registry registriert ist.

Feld-Wert-Auflösung

Der CompositeFieldValidator braucht kein bestimmtes Interface auf den Domain-Objekten. Er findet Werte über eine 5-stufige Auflösungskette:

PrioritätPatternBeispiel für Feld email
1get{Field}()$obj->getEmail()
2is{Field}()$obj->isEmail()
3has{Field}()$obj->hasEmail()
4{Field}()$obj->Email()
5Property-Reflection$obj->email (auch protected)

Break-Modus — Guard-Validierung

Manchmal soll die Validierung sofort abbrechen, wenn eine Vorbedingung nicht erfüllt ist. breaksOn() registriert einen Guard-Validator; schlägt er fehl, wird ein leeres (valides) Ergebnis zurückgegeben:

php
$validator = new CompositeFieldValidator();
$validator
    ->field('id')
        ->breaksOn(Uuid::class, Uuid::v4())   // Guard: ungültige ID → Abbruch
    ->field('email')
        ->validates(Email::class, Email::strict())
    ->field('amount')
        ->validates(Positive::class);

Anwendungsfall: In einem CQRS-Command-Handler: "Wenn die ID nicht valide ist, mach dir nicht die Mühe, die Business-Rules zu prüfen."

Break vs. Normal

breaksOn() bricht bei jedem Fehler ab und gibt ein valides Ergebnis zurück (keine Fehlermeldungen). validates() sammelt alle Fehler und gibt sie gesammelt zurück. Beides kann auf demselben Feld kombiniert werden.

Partial Updates

Ein häufiges Problem: Create erfordert alle Felder, Update nur die geänderten. Statt zwei Validatoren zu pflegen:

php
$validator = new CompositeFieldValidator();
$validator
    ->field('id')->validates(Uuid::class, Uuid::v4())
    ->field('email')->validates(Email::class, Email::strict())
    ->field('password')->validates(Length::class, Length::min(8))
    ->excludeFields(['password']);

// Create (id = null): password wird NICHT validiert
$validator->validate($newUser);

// Update (id = 'abc-123'): password wird validiert
$validator->validate($existingUser);

excludeFields() überspringt die genannten Felder nur, wenn das Identity-Feld (id per Default) null ist. Per withIdentityField('uuid') kann ein anderes Feld als Indikator verwendet werden.

Die 21 Validatoren

Alle Validatoren implementieren ValueValidatorInterface und geben null bei Erfolg oder einen Fehlertext bei Verstoß zurück. null-Werte passieren jeden Validator: für Pflichtfeld-Prüfung NotBlank oder NotEmpty verwenden.

Pflichtfeld-Validatoren

ValidatorPrüftStatische Helper
NotBlankWert ist nicht nullNotBlank::required()
NotEmptyWert ist nicht null, nicht leer, kein WhitespaceNotEmpty::trimmed(), NotEmpty::strict()

String-Validatoren

ValidatorPrüftStatische Helper
LengthZeichenlänge (min/max/exact)Length::between(3, 20), Length::min(8), Length::max(100), Length::exact(5)
FormatRegex-PatternFormat::pattern('/^[A-Z]{3}$/'), Format::slug(), Format::hexColor()
AlphanumericNur a-zA-Z0-9 (+ Optionen)Alphanumeric::withDashes(), withSpaces(), withUnderscores()
ContainWert ist in erlaubter ListeContain::oneOf(['active', 'inactive'])
EqualsGleichheit mit erwartetem WertEquals::strict($val), Equals::loose($val)

Format-Validatoren

ValidatorPrüftStatische Helper
EmailE-Mail-Adresse (optional DNS + Strict)Email::basic(), Email::withDnsCheck(), Email::strict()
UuidRFC 4122 UUID (optional Version)Uuid::any(), Uuid::v4(), Uuid::v1()
UrlURL (XSS-Schutz, Protokoll-Filter)Url::httpsOnly(), Url::noLocalhost(), Url::secure()
IpIPv4/IPv6 (optional Private/Reserved)Ip::v4(), Ip::v6(), Ip::noPrivate(), Ip::publicV4()
DateTimeDatum/Zeit-Format + RangeDateTime::iso8601(), DateTime::dateOnly(), DateTime::between(min, max, fmt)
JsonJSON-Syntax + TypJson::object(), Json::array(), Json::maxDepth(5)

Numerische Validatoren

ValidatorPrüftStatische Helper
RangeNumerischer BereichRange::between(1, 100), Range::min(0), Range::max(999)
PositivePositiver Wert (> 0 oder >= 0)Positive::strict(), Positive::allowZero()

Collection-Validatoren

ValidatorPrüftStatische Helper
CountArray/Countable-LängeCount::between(1, 10), Count::min(1), Count::exact(3)
UniqueItemsKeine Duplikate im ArrayUniqueItems::strict(), UniqueItems::loose()

Spezial-Validatoren

ValidatorPrüftStatische Helper
CreditCardLuhn-Algorithmus + KartentypCreditCard::visa(), mastercard(), amex(), discover()
IbanFormat + Mod-97-Checksum (70+ Länder)Iban::sepa(), Iban::forCountry('DE')
PhoneNumberFormat + Länderspezifisch (10 Länder)PhoneNumber::german(), us(), international()
CallbackEigene Logik per ClosureDirekte Instanziierung: new Callback(fn($v) => ...)

Callback-Validator

Für projektspezifische Validierungslogik, die keinen eigenen Validator rechtfertigt:

php
use JardisSupport\Validation\Validator\Callback;

$validator = new Callback(function (mixed $value): ?string {
    if (!is_array($value) || count($value) < 2) {
        return 'Mindestens 2 Elemente erforderlich';
    }
    return null;  // null = valide
});

$validator->validateValue([1, 2]);  // null (valide)
$validator->validateValue([1]);     // 'Mindestens 2 Elemente erforderlich'

Callback im CompositeFieldValidator

Callback wird als Instanz übergeben, nicht als Class-String. Für die Verwendung im CompositeFieldValidator den Handler direkt registrieren.

ValidationResult

Das Ergebnis jeder Validierung ist ein immutables ValidationResult:

php
$result = $validator->validate($object);

$result->isValid();                     // bool
$result->getErrors();                   // ['field' => ['error1', 'error2'], ...]
$result->getFieldErrors('email');       // ['Invalid email address']
$result->getFirstError('email');        // 'Invalid email address'
$result->hasFieldError('email');        // true
$result->getAllFieldsWithErrors();      // ['email', 'age']
$result->getErrorCount();              // Anzahl Felder mit Fehlern

Zirkuläre Referenzen und Tiefenlimit

Der ObjectValidator erkennt zirkuläre Referenzen via spl_object_id() und überspringt bereits besuchte Objekte. Zusätzlich gibt es ein konfigurierbares Tiefenlimit:

php
use JardisSupport\Validation\Internal\ValidationContext;

// Standard: maxDepth = 100
$validator = new ObjectValidator($registry);

// Eigenes Limit setzen
$context = new ValidationContext(maxDepth: 10);
$validator = new ObjectValidator($registry, $context);
// Wirft \RuntimeException bei Überschreitung

Architektur

Unter der Haube folgt das Package dem Closure-Orchestrator-Pattern in einer Drei-Schichten-Aufteilung:

ObjectValidator                        ← Orchestrator (Graph-Traversierung)
├── ValidatorRegistry                  ← Klasse → Validator Mapping
│   └── CompositeFieldValidator        ← Feld-Validierung (implements ValidatorInterface)
│       ├── FieldBuilder               ← Fluente API
│       └── Validator/                 ← 21 Value Validators
│           ├── Email
│           ├── Uuid
│           ├── Range
│           └── ...
└── ValidationContext                  ← Besuchte Objekte + Tiefenzähler

Verzeichnisstruktur

src/
├── ObjectValidator.php              ← Orchestrator (Graph-Traversierung)
├── CompositeFieldValidator.php      ← Feld-Regel-Komposition
├── ValidatorRegistry.php            ← Klasse → Validator Mapping
├── Internal/
│   ├── FieldBuilder.php             ← Fluenter Builder
│   └── ValidationContext.php        ← Traversierungs-State
└── Validator/
    ├── Alphanumeric.php
    ├── Callback.php
    ├── Contain.php
    ├── Count.php
    ├── CreditCard.php
    ├── DateTime.php
    ├── Email.php
    ├── Equals.php
    ├── Format.php
    ├── Iban.php
    ├── Ip.php
    ├── Json.php
    ├── Length.php
    ├── NotBlank.php
    ├── NotEmpty.php
    ├── PhoneNumber.php
    ├── Positive.php
    ├── Range.php
    ├── UniqueItems.php
    ├── Url.php
    └── Uuid.php

API-Referenz

ObjectValidator

MethodeSignaturBeschreibung
__construct__construct(ValidatorRegistry $registry, ?ValidationContext $context = null)Registry und optionaler Context
validatevalidate(object $object): ValidationResultObjektgraph validieren

CompositeFieldValidator

MethodeSignaturBeschreibung
fieldfield(string $name): FieldBuilderFluente Feld-Konfiguration starten
excludeFieldsexcludeFields(array $fields): selfFelder bei Create überspringen
withIdentityFieldwithIdentityField(string $name): selfIdentity-Feld ändern (Standard: id)
validatevalidate(object $data): ValidationResultObjekt validieren

FieldBuilder

MethodeSignaturBeschreibung
validatesvalidates(string $validatorClass, array $options = []): selfNormal-Validator hinzufügen
breaksOnbreaksOn(string $validatorClass, array $options = []): selfGuard-Validator hinzufügen
fieldfield(string $name): FieldBuilderNächstes Feld konfigurieren
endend(): CompositeFieldValidatorBuilder abschließen

ValidatorRegistry

MethodeSignaturBeschreibung
registerregister(string $className, ValidatorInterface $validator): selfValidator registrieren
getValidatorgetValidator(object $object): ?ValidatorInterfaceValidator für Objekt finden

Vollständiges Beispiel

Ein E-Commerce-Szenario mit verschachtelter Validierung, Guard-Modus und Partial Updates:

php
use JardisSupport\Validation\CompositeFieldValidator;
use JardisSupport\Validation\ObjectValidator;
use JardisSupport\Validation\ValidatorRegistry;
use JardisSupport\Validation\Validator\Count;
use JardisSupport\Validation\Validator\Email;
use JardisSupport\Validation\Validator\Iban;
use JardisSupport\Validation\Validator\Length;
use JardisSupport\Validation\Validator\NotBlank;
use JardisSupport\Validation\Validator\Positive;
use JardisSupport\Validation\Validator\Range;
use JardisSupport\Validation\Validator\Uuid;

// Order-Validator mit Guard und Partial Update
$orderValidator = new CompositeFieldValidator();
$orderValidator
    ->field('id')
        ->breaksOn(Uuid::class, Uuid::v4())       // Guard: ungültige ID → Abbruch
    ->field('customerId')
        ->validates(NotBlank::class)
        ->validates(Uuid::class, Uuid::v4())
    ->field('customerEmail')
        ->validates(Email::class, Email::strict())
    ->field('shippingAddress')
        ->validates(NotBlank::class)
        ->validates(Length::class, Length::between(10, 500))
    ->field('totalAmount')
        ->validates(Positive::class, Positive::strict())
    ->field('lines')
        ->validates(Count::class, Count::min(1))
    ->excludeFields(['shippingAddress']);           // Bei Create optional

// OrderLine-Validator
$lineValidator = new CompositeFieldValidator();
$lineValidator
    ->field('productId')
        ->validates(Uuid::class, Uuid::v4())
    ->field('productName')
        ->validates(NotBlank::class)
        ->validates(Length::class, Length::max(200))
    ->field('quantity')
        ->validates(Range::class, Range::between(1, 9999))
    ->field('unitPrice')
        ->validates(Positive::class);

// Payment-Validator
$paymentValidator = new CompositeFieldValidator();
$paymentValidator
    ->field('iban')
        ->validates(Iban::class, Iban::sepa())
    ->field('amount')
        ->validates(Positive::class, Positive::strict());

// Registry: Klasse → Validator
$registry = new ValidatorRegistry();
$registry
    ->register(Order::class, $orderValidator)
    ->register(OrderLine::class, $lineValidator)
    ->register(PaymentInfo::class, $paymentValidator);

// Validierung — traversiert automatisch Order → OrderLines → PaymentInfo
$validator = new ObjectValidator($registry);
$result = $validator->validate($order);

if (!$result->isValid()) {
    foreach ($result->getAllFieldsWithErrors() as $field) {
        echo "{$field}: " . $result->getFirstError($field) . "\n";
    }
}