Validation recipes
These recipes show the public Fynix API in small, focused scenarios. Start with Rule for one value, use Rule::for() for an object instance, and use ValidationHandler for a complete DTO.
1. Validate a required string
Section titled “1. Validate a required string”use Fynix\Rule;
$error = Rule::string('firstName') ->required() ->validate('');Use validate() when the first applicable error is enough.
2. Validate an optional field
Section titled “2. Validate an optional field”$nickname = Rule::string('nickname') ->optional() ->max(40);
$error = $nickname->validate(null); // nulloptional() allows null and an empty string to pass presence validation.
3. Use a human-readable label
Section titled “3. Use a human-readable label”$error = Rule::string('postalCode') ->label('Postal code') ->length(5, 10) ->validate('12');The label changes the message, while the structured field name remains postalCode.
4. Apply minimum and maximum bounds
Section titled “4. Apply minimum and maximum bounds”$username = Rule::string('username') ->min(3) ->max(30);
$error = $username->validate('ab');For strings, the bounds are character counts. For numbers they are numeric limits, and for arrays they are item counts.
5. Apply one length range
Section titled “5. Apply one length range”$password = Rule::password('password') ->length(12, 128);
$errors = $password->validateAll('abc');length() is useful when the lower and upper bounds describe one policy.
6. Restrict values with an allow-list
Section titled “6. Restrict values with an allow-list”$accountType = Rule::string('accountType') ->in(['personal', 'business']);
$error = $accountType->validate('unknown');in() uses strict comparison and returns value.not_allowed for an unlisted value.
7. Reject values with a deny-list
Section titled “7. Reject values with a deny-list”$status = Rule::string('status') ->notIn(['deleted', 'suspended']);
$error = $status->validate('deleted');notIn() returns value.disallowed for a listed value.
8. Compare confirmation fields
Section titled “8. Compare confirmation fields”$user = new User();$user->password = 'secret';$user->passwordConfirmation = 'different';
$error = Rule::for($user) ->string('passwordConfirmation') ->sameAs('password') ->validate();Cross-field methods need object context so Fynix can read the related property.
9. Require a field conditionally
Section titled “9. Require a field conditionally”$registration = new Registration();$registration->accountType = 'business';$registration->companyName = '';
$error = Rule::for($registration) ->string('companyName') ->optional() ->requiredIf('accountType', 'business') ->validate();Use requiredUnless() when the field should be required for every value except one condition.
10. Prohibit a field conditionally
Section titled “10. Prohibit a field conditionally”$registration = new Registration();$registration->accountType = 'personal';$registration->companyName = 'Acme';
$error = Rule::for($registration) ->string('companyName') ->optional() ->prohibitedIf('accountType', 'personal') ->validate();Use prohibitedUnless() when a value is allowed only for one context.
11. Validate one property on an object
Section titled “11. Validate one property on an object”$user = new User();$user->firstName = 'A';
$error = Rule::for($user) ->string('firstName') ->min(2) ->max(50) ->validate();Because the rule is bound to $user, do not pass $user->firstName to validate().
12. Return every applicable error
Section titled “12. Return every applicable error”$errors = Rule::password('password') ->length(12, 128) ->validateAll('abc');
$payload = array_map( static fn ($error): array => $error->toArray(), $errors,);Use validateAll() for password policies, imports, and forms that should show every issue at once.
13. Validate an object with explicit rules
Section titled “13. Validate an object with explicit rules”$user = new User();$user->firstName = '';$user->email = 'not-an-email';
$errors = ValidationHandler::validate( $user, rules: [ Rule::on(User::class)->string('firstName')->min(2), Rule::on(User::class)->email('email')->max(180), ],);The handler runs each rule internally. Do not add ->validate() inside the rules array.
14. Keep reusable rules in a class method
Section titled “14. Keep reusable rules in a class method”final class User{ public string $firstName = ''; public string $email = '';
public static function rules(): array { return [ Rule::on(self::class)->string('firstName')->min(2)->max(50), Rule::on(self::class)->email('email')->max(180), ]; }
public function validationErrors(): array { return ValidationHandler::validate($this, rules: self::rules()); }}This keeps the class-scoped rule definition separate from the method that validates the current instance.
15. Register rules for DTO validation
Section titled “15. Register rules for DTO validation”ValidationRegistry::register( User::class, static fn (RuleSet $rules): array => [ $rules->string('firstName')->min(2)->max(50), $rules->email('email')->max(180), ],);
$errors = ValidationHandler::validate($user);When no explicit rules: argument is supplied, the handler loads the registered rules for the object’s class.
16. Validate arrays and every item
Section titled “16. Validate arrays and every item”$tags = Rule::arrayOf('tags') ->min(1) ->max(10) ->each(Rule::string('tag')->length(2, 30));
$errors = $tags->validateAll(['php', '']);Use arrayOf() when the collection itself and each item need validation.
17. Validate nested DTOs
Section titled “17. Validate nested DTOs”ValidationRegistry::register( Address::class, static fn (RuleSet $rules): array => [ $rules->string('city')->min(2)->max(80), ],);
ValidationRegistry::register( User::class, static fn (RuleSet $rules): array => [ $rules->object('address', Address::class), ],);
$errors = ValidationHandler::validate($user);The handler resolves the nested object’s registered rules and preserves the nested error shape.
18. Compose rules for reusable policies
Section titled “18. Compose rules for reusable policies”use Fynix\Rule;use Fynix\Rules\AllOf;use Fynix\Rules\AnyOf;use Fynix\Rules\Not;
$name = new AllOf([ Rule::string('name')->min(2), Rule::string('name')->max(80),]);
$contact = new AnyOf([ Rule::email('contact'), Rule::phoneNumber('contact'),]);
$blocked = new Not( Rule::string('status')->in(['blocked']), 'This status is not allowed.',);Use composition when a policy combines alternatives, multiple constraints, or an explicit negation.
19. Customize a domain validator
Section titled “19. Customize a domain validator”final class EvenNumberValidator extends ValidatorBase{ protected function validateValue(mixed $fieldValue): ?ValidationError { if (!is_int($fieldValue) || $fieldValue % 2 !== 0) { return new ValidationError($this, "$this->name must be even.", 'number.even'); }
return null; }}Pass $this to ValidationError to preserve validator metadata and use $this->name for the configured display label. This is an advanced extension point.