Skip to content

Rule, RuleSet, and registry

Version 3 is designed around a small set of public construction APIs:

  • Rule creates standalone validators
  • Rule::on() creates class-scoped validators
  • RuleSet is passed into registry factories
  • ValidationRegistry stores the DTO-to-rules mapping

This keeps validation rules explicit, type-safe, and easy to reuse.

Rule is the main facade for constructing validators. Every factory accepts the DTO property name unless noted otherwise:

MethodValidator or purpose
Rule::string($field)StringValidator
Rule::boolean($field)BooleanValidator
Rule::number($field)NumberValidator
Rule::integer($field)IntegerValidator
Rule::decimal($field)DecimalValidator
Rule::email($field)EmailValidator
Rule::phoneNumber($field)PhoneNumberValidator
Rule::password($field)PasswordValidator
Rule::dateTime($field)DateTimeValidator
Rule::url($field)UrlValidator
Rule::uuid($field)UuidValidator
Rule::ipAddress($field)IpAddressValidator
Rule::regex($field, $pattern)RegexValidator
Rule::array($field)Alias for arrayOf()
Rule::arrayOf($field)ArrayValidator
Rule::enum($field, $enumClass)EnumValidator
Rule::file($field)FileValidator
Rule::image($field)ImageValidator
Rule::images($field)ImagesValidator
Rule::object($field, $targetClass)ObjectValidator
Rule::objectArray($field, $targetClass)ObjectArrayValidator
Rule::username($field)UsernameValidator
Rule::on($ownerClass)Create class-scoped rules
Rule::for($instance)Create instance-bound rules
Rule::labelFor($field)Generate a display label from a property name

Rule::on() expects a class name such as User::class. Use Rule::for($user) when the rules should read values from one object instance.

Existing v2-style definitions can use Rules::for() and RuleBuilder while moving to the v3 APIs:

use Fynix\Rules;
$rules = Rules::for(Order::class)
->string('shippingMethod')
->required()
->rules();
$errors = ValidationHandler::validate($order, rules: $rules);

The builder preserves the fluent registration style and supports v3 methods such as when(). Prefer RuleSet for new registry factories.

Use standalone rules when you are validating a value directly, not a registered object:

use Fynix\Rule;
$email = Rule::email('email')->max(255);
$age = Rule::integer('age')->min(18)->max(120);
$code = Rule::regex('code', '/^[A-Z]{3}-[0-9]{4}$/');

These are ideal for request payloads, form submission validation, and single field checks.

Scoped rules are checked against a declared class and property name:

use Fynix\Rule;
$rule = Rule::on(User::class)
->string('firstName')
->min(2)
->max(50);

This is the most common way to define v3 rules. If the owning class or property does not exist, Fynix throws a typed exception instead of silently creating incomplete rules.

Register reusable DTO rules once and then validate objects by type:

use Fynix\RuleSet;
use Fynix\ValidationRegistry;
ValidationRegistry::register(
User::class,
static fn (RuleSet $rules): array => [
$rules->string('firstName')->min(2)->max(50),
$rules->email('email')->max(255),
$rules->password('password')->min(8)->max(128),
]
);

A registry factory receives a RuleSet, not a DTO instance. Each returned definition must be a valid Validatable rule. The rules are then retrieved by ValidationRegistry::rulesFor() and applied by ValidationHandler.

The handler accepts an optional rules array and uses it before the global registry:

$errors = ValidationHandler::validate(
$user,
rules: [
Rule::on(User::class)->string('firstName')->min(2)->max(50),
Rule::on(User::class)->email('email'),
]
);

This is useful when you want one object validation to use a local or temporary rule set without registering globally. If you omit $rules, the registry lookup is used.

All fluent methods are immutable. A configuration method returns a new instance, so chaining is safe and explicit:

use Fynix\Rule;
$rule = Rule::string('name');
$rule = $rule->min(2);
$rule = $rule->max(80);

Keep rule construction on the v3 facade APIs so definitions remain immutable and class-aware.

MethodPurpose
label(string $label)Override the generated field label.
required() / optional()Control missing-value behavior.
in(array $values) / notIn(array $values)Allow or reject exact values.
`sameAs(stringClosure $fieldOrCondition)/differentFrom(string
`requiredIf(stringClosure $fieldOrCondition, mixed $value = null)`
`requiredUnless(stringClosure $fieldOrCondition, mixed $value = null)`
`prohibitedIf(stringClosure $fieldOrCondition, mixed $value = null)`
`prohibitedUnless(stringClosure $fieldOrCondition, mixed $value = null)`
when(Closure $condition)Run the validator only when the owning object satisfies a condition.
genericValidation(bool $enabled = true) / withoutGenericValidation()Enable or disable shared requiredness, normalization, HTML, and length checks.
validate(mixed $value, ?object $data = null)Return the first applicable validation error.
validateAll(mixed $value, ?object $data = null)Return all applicable validation errors.

Validators also expose name(), propertyName(), requiredState(), minLength(), and maxLength() for inspecting configured metadata and constraints.

Every fluent method returns a new rule. The original rule remains unchanged, so you can safely keep a base rule and derive more specific rules from it.

Use label() to control the human-readable name in error messages. Use required() for values that must be present and optional() when null or an empty string is allowed.

$displayName = Rule::string('displayName')
->label('Display name')
->required();
$nickname = Rule::string('nickname')->optional();

required(false) is equivalent to optional(). Presence is checked before type-specific validation.

Use min() and max() for one-sided limits. Use length() when both limits belong to the same rule. The meaning follows the validator: strings use character count, numbers use their numeric value, and arrays or collections use item count.

$username = Rule::string('username')->min(3)->max(30);
$age = Rule::integer('age')->min(18)->max(120);
$tags = Rule::array('tags')->min(1)->max(10);
$password = Rule::password('password')->length(12, 128);

Use in() to allow only an explicit list. Use notIn() to reject values from a list. Comparisons are strict, so 1 and '1' are different values.

$accountType = Rule::string('accountType')
->in(['personal', 'business']);
$status = Rule::string('status')
->notIn(['deleted', 'suspended']);

sameAs() and differentFrom() compare the current field with another property on the same object. They require object context, so use them with Rule::on(), Rule::for(), a RuleSet, or ValidationHandler.

$rules = [
Rule::on(User::class)
->string('passwordConfirmation')
->sameAs('password'),
Rule::on(User::class)
->string('newEmail')
->differentFrom('email'),
];

Use requiredIf() when a field becomes required after another field matches a value. Use requiredUnless() when it becomes required after the other field differs from that value.

$rules = [
Rule::on(Registration::class)
->string('companyName')
->optional()
->requiredIf('accountType', 'business'),
Rule::on(Registration::class)
->string('taxId')
->optional()
->requiredUnless('accountType', 'personal'),
];

The comparison is strict. Conditional presence rules are evaluated with the current DTO, not with a separately supplied scalar value.

Use prohibitedIf() to reject a field when another field matches a value. Use prohibitedUnless() to reject it when the other field does not match.

$rules = [
Rule::on(Registration::class)
->string('companyName')
->optional()
->prohibitedIf('accountType', 'personal'),
Rule::on(Registration::class)
->string('personalTaxId')
->optional()
->prohibitedUnless('accountType', 'personal'),
];

When a value is prohibited, the pipeline returns a prohibition error before running the field’s type-specific checks.

Use genericValidation(false) or withoutGenericValidation() when a specialized validator must skip the shared requiredness, normalization, HTML, and length layer. This is an advanced option; most rules should keep the default pipeline enabled.

$rawValue = Rule::string('rawValue')
->withoutGenericValidation();

Use validate() for the first applicable error and validateAll() when the caller needs every applicable error. For a standalone rule, pass the value. For an object-bound rule, omit the value and let Fynix read the property.

$error = Rule::email('email')->validate('not-an-email');
$errors = Rule::password('password')
->length(12, 128)
->validateAll('abc');
$boundError = Rule::for($user)
->string('firstName')
->min(2)
->validate();

When validating a complete object, pass the rules to ValidationHandler::validate(). The handler runs each rule internally, so do not call validate() inside the rules array.

For combined rules, see composable rules.