Object and DTO validation
Use object validation when several fields belong to one request or domain object. Define class-scoped rules and let ValidationHandler run them together.
Define a DTO
Section titled “Define a DTO”final class User{ public string $firstName = ''; public string $email = ''; public string $password = '';}Define reusable class rules
Section titled “Define reusable class rules”use Fynix\Rule;
final class UserRules{ public static function rules(): array { return [ Rule::on(User::class)->string('firstName')->min(2)->max(50), Rule::on(User::class)->email('email')->max(180), Rule::on(User::class)->password('password')->length(12, 128), ]; }}Rule::on(User::class) validates the declared class and property while constructing the rule.
Validate the DTO
Section titled “Validate the DTO”use Fynix\ValidationHandler;
$user = new User();$user->firstName = 'A';$user->email = 'not-an-email';$user->password = 'short';
$errors = ValidationHandler::validate( $user, rules: UserRules::rules(),);The handler runs every rule internally. Do not call ->validate() inside the rules array.
Put rules on the class
Section titled “Put rules on the class”use Fynix\ValidationError;
final class Customer{ public string $firstName = ''; public string $email = '';
public static function rules(): array { return [ Rule::on(self::class)->email('email')->required(), ]; }
public function validationErrors(): array { return ValidationHandler::validate($this, rules: self::rules()); }
public function firstNameError(): ?ValidationError { return Rule::for($this) ->string('firstName') ->min(2) ->validate(); }}Rule::on(self::class) defines reusable class-scoped rules. Rule::for($this) binds a one-off rule to the current instance. Neither form is Rule::on($this): on() expects a class name, while for() expects an object.
Validate a nested object
Section titled “Validate a nested object”final class Address{ public string $city = '';}
final class Customer{ public ?Address $address = null;}
ValidationRegistry::register( Address::class, static fn (RuleSet $rules): array => [ $rules->string('city')->min(2)->max(80), ],);
ValidationRegistry::register( Customer::class, static fn (RuleSet $rules): array => [ $rules->object('address', Address::class), ],);
$errors = ValidationHandler::validate($customer);Nested rules are resolved by type and returned in the nested object shape.
Compare fields
Section titled “Compare fields”ValidationRegistry::register( Registration::class, static fn (RuleSet $rules): array => [ $rules->string('passwordConfirmation')->sameAs('password'), $rules->string('newEmail')->differentFrom('email'), ],);sameAs() and differentFrom() read related properties from the same object.
Conditional fields
Section titled “Conditional fields”ValidationRegistry::register( Registration::class, static fn (RuleSet $rules): array => [ $rules->string('companyName') ->optional() ->requiredIf('accountType', 'business'), $rules->string('businessLicense') ->optional() ->prohibitedUnless('accountType', 'business'), ],);Use requiredUnless(), prohibitedIf(), and prohibitedUnless() for the inverse cases.
Structured nested errors
Section titled “Structured nested errors”$errors = ValidationHandler::validate( $customer, flattenErrorToString: false,);
foreach ($errors as $field => $issues) { // $issues may contain nested objects or ValidationError instances.}Keep structured errors for APIs and use the default string output for simple form rendering.