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
Orderund einen fürOrderLineregistrieren, 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 Referenzen —
spl_object_id()-basierte Erkennung verhindert Endlosschleifen - Statische Helper als Option-Factories —
Email::strict(),Range::between(1, 100),Uuid::v4()statt Option-Key-Raten
Installation
composer require jardissupport/validationGitHub: jardisSupport/validation
Grundlegende Nutzung
Feld-Validierung mit fluenter API
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
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ät | Pattern | Beispiel für Feld email |
|---|---|---|
| 1 | get{Field}() | $obj->getEmail() |
| 2 | is{Field}() | $obj->isEmail() |
| 3 | has{Field}() | $obj->hasEmail() |
| 4 | {Field}() | $obj->Email() |
| 5 | Property-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:
$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:
$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
| Validator | Prüft | Statische Helper |
|---|---|---|
NotBlank | Wert ist nicht null | NotBlank::required() |
NotEmpty | Wert ist nicht null, nicht leer, kein Whitespace | NotEmpty::trimmed(), NotEmpty::strict() |
String-Validatoren
| Validator | Prüft | Statische Helper |
|---|---|---|
Length | Zeichenlänge (min/max/exact) | Length::between(3, 20), Length::min(8), Length::max(100), Length::exact(5) |
Format | Regex-Pattern | Format::pattern('/^[A-Z]{3}$/'), Format::slug(), Format::hexColor() |
Alphanumeric | Nur a-zA-Z0-9 (+ Optionen) | Alphanumeric::withDashes(), withSpaces(), withUnderscores() |
Contain | Wert ist in erlaubter Liste | Contain::oneOf(['active', 'inactive']) |
Equals | Gleichheit mit erwartetem Wert | Equals::strict($val), Equals::loose($val) |
Format-Validatoren
| Validator | Prüft | Statische Helper |
|---|---|---|
Email | E-Mail-Adresse (optional DNS + Strict) | Email::basic(), Email::withDnsCheck(), Email::strict() |
Uuid | RFC 4122 UUID (optional Version) | Uuid::any(), Uuid::v4(), Uuid::v1() |
Url | URL (XSS-Schutz, Protokoll-Filter) | Url::httpsOnly(), Url::noLocalhost(), Url::secure() |
Ip | IPv4/IPv6 (optional Private/Reserved) | Ip::v4(), Ip::v6(), Ip::noPrivate(), Ip::publicV4() |
DateTime | Datum/Zeit-Format + Range | DateTime::iso8601(), DateTime::dateOnly(), DateTime::between(min, max, fmt) |
Json | JSON-Syntax + Typ | Json::object(), Json::array(), Json::maxDepth(5) |
Numerische Validatoren
| Validator | Prüft | Statische Helper |
|---|---|---|
Range | Numerischer Bereich | Range::between(1, 100), Range::min(0), Range::max(999) |
Positive | Positiver Wert (> 0 oder >= 0) | Positive::strict(), Positive::allowZero() |
Collection-Validatoren
| Validator | Prüft | Statische Helper |
|---|---|---|
Count | Array/Countable-Länge | Count::between(1, 10), Count::min(1), Count::exact(3) |
UniqueItems | Keine Duplikate im Array | UniqueItems::strict(), UniqueItems::loose() |
Spezial-Validatoren
| Validator | Prüft | Statische Helper |
|---|---|---|
CreditCard | Luhn-Algorithmus + Kartentyp | CreditCard::visa(), mastercard(), amex(), discover() |
Iban | Format + Mod-97-Checksum (70+ Länder) | Iban::sepa(), Iban::forCountry('DE') |
PhoneNumber | Format + Länderspezifisch (10 Länder) | PhoneNumber::german(), us(), international() |
Callback | Eigene Logik per Closure | Direkte Instanziierung: new Callback(fn($v) => ...) |
Callback-Validator
Für projektspezifische Validierungslogik, die keinen eigenen Validator rechtfertigt:
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:
$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 FehlernZirkulä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:
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 ÜberschreitungArchitektur
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ählerVerzeichnisstruktur
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.phpAPI-Referenz
ObjectValidator
| Methode | Signatur | Beschreibung |
|---|---|---|
__construct | __construct(ValidatorRegistry $registry, ?ValidationContext $context = null) | Registry und optionaler Context |
validate | validate(object $object): ValidationResult | Objektgraph validieren |
CompositeFieldValidator
| Methode | Signatur | Beschreibung |
|---|---|---|
field | field(string $name): FieldBuilder | Fluente Feld-Konfiguration starten |
excludeFields | excludeFields(array $fields): self | Felder bei Create überspringen |
withIdentityField | withIdentityField(string $name): self | Identity-Feld ändern (Standard: id) |
validate | validate(object $data): ValidationResult | Objekt validieren |
FieldBuilder
| Methode | Signatur | Beschreibung |
|---|---|---|
validates | validates(string $validatorClass, array $options = []): self | Normal-Validator hinzufügen |
breaksOn | breaksOn(string $validatorClass, array $options = []): self | Guard-Validator hinzufügen |
field | field(string $name): FieldBuilder | Nächstes Feld konfigurieren |
end | end(): CompositeFieldValidator | Builder abschließen |
ValidatorRegistry
| Methode | Signatur | Beschreibung |
|---|---|---|
register | register(string $className, ValidatorInterface $validator): self | Validator registrieren |
getValidator | getValidator(object $object): ?ValidatorInterface | Validator für Objekt finden |
Vollständiges Beispiel
Ein E-Commerce-Szenario mit verschachtelter Validierung, Guard-Modus und Partial Updates:
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";
}
}