Skip to content

RuleSet ​

RuleSet is the composable, immutable rule container that powers everything outside a form request: inline validation, shared rule libraries, conditional fields, errors-as-data flows. Reach for it whenever the rules are not bound to a single HTTP request. Form requests already wrap a RuleSet for you under the hood.

On this page: Building · Composing · Inspecting and exporting · Validating data · Raw Validator · Custom Validators · Compile pipeline · Method reference

Building a rule set ​

Three equivalent entry points. Pick whichever reads cleaner at the call site. The array form is compact for static rule lists; the make() builder form is friendlier when fields are added conditionally; define() hands you a FluentSchema so you can drop the FluentRule:: prefix.

php
use SanderMuller\FluentValidation\FluentSchema;
use SanderMuller\FluentValidation\RuleSet;

// From an array
$validated = RuleSet::from([
    'name'  => FluentRule::string('Full Name')->required()->min(2)->max(255),
    'items' => FluentRule::array()->required()->each([
        'name'  => FluentRule::string()->required()->min(2),
        'price' => FluentRule::numeric()->required()->min(0),
    ]),
    'role'        => FluentRule::string()->when($isAdmin, fn ($r) => $r->required()->in(['admin', 'editor'])),
    'permissions' => FluentRule::array()->when($isAdmin, fn ($r) => $r->required()),
])
    ->merge($sharedAddressRules)
    ->validate($request->all());

// Or fluently, with conditional fields and merging
$validated = RuleSet::make()
    ->field('name', FluentRule::string('Full Name')->required())
    ->field('items', FluentRule::array()->required()->each([
        'name'  => FluentRule::string()->required()->min(2),
        'price' => FluentRule::numeric()->required()->min(0),
    ]))
    ->when($isAdmin, fn (RuleSet $set) => $set
        ->field('role', FluentRule::string()->required()->in(['admin', 'editor']))
        ->field('permissions', FluentRule::array()->required())
    )
    ->merge($sharedAddressRules)
    ->validate($request->all());

// Or with a FluentSchema builder — no `FluentRule::` prefix on each line
$validated = RuleSet::define(fn (FluentSchema $rules) => [
    'name'  => $rules->string('Full Name')->required()->min(2)->max(255),
    'email' => $rules->email()->required(),
    'items' => $rules->array()->required()->each([
        'name' => $rules->string()->required(),
    ]),
])->validate($request->all());

Composing rule sets ​

Most non-trivial validation is assembled, not declared in one shot: shared address rules merged in, parent rules sliced down for a child request, a single field tweaked without rewriting the rest. RuleSet exposes three groups of composition tools.

Slice and combine: merge, only, except, put, get. merge() accepts a RuleSet or a plain array; later wins on key collision.

php
return UserRules::base()
    ->only(['email', 'password'])
    ->put('email_confirmation', FluentRule::email()->required()->same('email'));

Read-modify-write: modify, modifyEach, modifyChildren. All three clone the existing rule before handing it to your callback so parent rule sets aren't mutated. modify() is the primitive; modifyEach() and modifyChildren() are sugar for the common case of extending a keyed each([...]) (wildcard arrays) or children([...]) (fixed-key objects) map. The "Extending parent rules in child form requests" section walks through the parent/child inheritance flow with modify/modifyEach. The same shape works for modifyChildren on a fixed-key object:

php
// Parent
return RuleSet::from([
    'address' => FluentRule::field()->required()->children([
        'street' => FluentRule::string()->required(),
        'city'   => FluentRule::string()->required(),
    ]),
]);

// Child — later-wins merge into the children() map
return parent::rules()->modifyChildren('address', [
    'postal_code' => FluentRule::string()->required(),
]);

Conditionals: when() and unless() from Laravel's Conditionable trait. Use these to branch field inclusion based on a flag without breaking the chain (shown in the building example above).

Inspecting and exporting a rule set ​

Reads of the in-memory RuleSet: predicates for branching code, debugging dumps, and the user-facing toArray() export that hands rules off to a Validator. The lower-level static compile* family lives under Compile pipeline. That's the transform surface for tooling and codegen.

  • toArray() / all(): compiled flat output, ready for Validator::make(). all() is a Collection-style alias.
  • [...$ruleSet]: spread via IteratorAggregate; yields the toArray() shape, so [...$parent, 'extra' => $rule] works.
  • isEmpty(): true when no fields have been registered. Useful for "skip validation if empty" branches.
  • hasObjectRules(): true when at least one field uses each() or children(). Useful for tooling that needs to distinguish flat from nested rule sets.
  • flattenRules(): flattened dotted/wildcard form of the rules. Useful for codegen and debug logging:
php
RuleSet::from([
    'address' => FluentRule::field()->required()->children([
        'street' => FluentRule::string()->required(),
    ]),
    'items' => FluentRule::array()->each([
        'sku' => FluentRule::string()->required(),
    ]),
])->flattenRules();

// [
//     'address'        => FluentRule::field()->required()->children([...]),
//     'address.street' => FluentRule::string()->required(),
//     'items'          => FluentRule::array()->each([...]),
//     'items.*.sku'    => FluentRule::string()->required(),
// ]
  • dump(): returns ['rules' => ..., 'messages' => ..., 'attributes' => ...] for inspection; does not terminate.
  • dd(): dumps and terminates. Sugar for the same shape during development.

Validating ​

validate() and check(), the unknown-field flags, stopOnFirstFailure() and withBag() have their own page: Validating with a RuleSet.

Integrating with a raw Validator ​

For inspection (->failed(), ->errors(), ->valid()), check() already exposes the underlying Validator:

php
$validator = RuleSet::from($rules)
    ->check($request->all(), $customMessages)
    ->validator();

The validator has already run. For pre-run hooks like ->after() or ->sometimes(), prefer the equivalent RuleSet mechanics (custom Rule classes, Rule::when(), modify()). If you genuinely need an unvalidated Validator, prepare() returns the compiled rules, messages, and attributes for hand-rolled Validator::make(...) use.

Using with custom Validators ​

If your application extends Illuminate\Validation\Validator directly (for example, in import jobs), you may extend FluentValidator instead:

php
use SanderMuller\FluentValidation\FluentValidator;

class JsonImportValidator extends FluentValidator
{
    public function __construct(array $data, protected ?User $user = null)
    {
        parent::__construct($data, $this->buildRules());
    }

    private function buildRules(): array
    {
        return [
            '*.type' => FluentRule::string()->required()->in(InteractionType::cases()),
            '*.end_time' => FluentRule::numeric()
                ->requiredUnless('*.type', ...InteractionType::withoutDuration())
                ->greaterThanOrEqualTo('*.start_time'),
        ];
    }
}

FluentValidator resolves the translator and presence verifier from the container, calls prepare() on the rules, and sets implicit attributes. Cross-field wildcard references (requiredUnless('*.type', ...)) work automatically.

Migrating rules in a non-standard method? If your custom Validator holds its rules in a method that isn't named rules() (for example rulesWithoutPrefix() for a JSON-import pipeline), mark the method with #[SanderMuller\FluentValidation\FluentRules] so the migration Rector rules detect it. The attribute has no runtime effect; see the #[FluentRules] opt-in docs for the full semantics and guard interactions.

Compile pipeline ​

Escape hatches for tooling, codegen and framework interop

Application code does not reach for these; validate(), check(), prepare() and toArray() cover it. They are public so external Rector rules, PHPStan extensions and the Livewire bridge can hook the same pipeline RuleSet uses internally.

  • RuleSet::compile($rules): compile a fluent-rules array to native Laravel string|array rule format. The lowest-level transform.
  • RuleSet::compileToArrays($rules): compile to the array-of-rules shape Livewire's $this->validate() expects. Used by HasFluentValidation under the hood.
  • RuleSet::compileWithMetadata($rules): compile alongside extracted custom messages and attribute labels in one pass. For tooling that needs the metadata without re-walking the rule tree.
  • RuleSet::extractMetadata($rules): extract [messages, attributes] from labelled fluent rules without compiling. For tooling that only wants the metadata side.
  • $set->expandWildcards($data): pre-expand wildcard rules against a concrete payload without validating. Useful when generating per-row error keys ahead of time.

Method reference ​

Every public method, alphabetically
MethodReturnsDescription
->all()arrayCollection-style alias of ->toArray().
->check($data, $messages = [], $attributes = [])ValidatedValidate without throwing. $data accepts array or Illuminate\Http\Request. See Errors-as-data with check().
RuleSet::compile($rules)arrayCompile fluent rules to native Laravel format.
RuleSet::compileToArrays($rules)arrayCompile to array-of-rules shape for Livewire's $this->validate().
RuleSet::compileWithMetadata($rules)arrayCompile + return extracted messages and attributes in one pass.
->dd()neverDump the rule set and terminate.
RuleSet::define(fn ($rules))RuleSetCreate from a closure given a FluentSchema builder; drops the FluentRule:: prefix.
->dump()arrayReturn {rules, messages, attributes} for debugging.
->except(...$fields)RuleSetDrop the named fields (variadic strings or single array).
->expandWildcards($data)arrayPre-expand wildcards against $data without validating.
RuleSet::extractMetadata($rules)arrayExtract [messages, attributes] from labelled fluent rules.
->dropUnknownFields()RuleSetSilently strip unvalidated array sub-keys from validated() output. Lenient counterpart to failOnUnknownFields().
->failOnUnknownFields()RuleSetReject input keys not present in the rule set.
->field($name, $rule)RuleSetAdd a field via the fluent builder.
->flattenRules()arrayFlat dotted/wildcard form of the rules; each/children are unwrapped.
RuleSet::from([...])RuleSetCreate from a rules array.
->get($field, $default = null)mixedRead a single field's rule (uncompiled), or $default if absent.
->getIterator() / [...$ruleSet]TraversableSpread support; yields the toArray() shape.
->hasObjectRules()booltrue when at least one field uses each() or children().
->isEmpty()booltrue when no fields are registered.
RuleSet::make()RuleSetEmpty rule set; chain ->field(...).
->merge($ruleSet|$array)RuleSetMerge another RuleSet or array (later wins on collision).
->modify($field, fn ($rule))RuleSetRead-modify-write a single field; clones first; throws on missing key.
->modifyChildren($field, $rules)RuleSetSugar: later-wins merge into a FieldRule's children([...]) map.
->modifyEach($field, $rules)RuleSetSugar: later-wins merge into an ArrayRule's each([...]) map.
->only(...$fields)RuleSetKeep only the named fields (variadic strings or single array).
->prepare($data)PreparedRulesExpand, extract metadata, compile. For hand-rolled Validator::make.
->put($field, $rule)RuleSetAdd or replace a single field's rule.
->stopOnFirstFailure()RuleSetStop validating after the first field fails.
->toArray()arrayCompiled flat output; each() expanded to wildcards.
->validate($data, $messages = [], $attributes = [])arrayValidate with full optimization (see Performance). $data accepts array or Illuminate\Http\Request.
->when($cond, $cb) / ->unless(...)RuleSetConditionally add fields (Laravel's Conditionable trait).
->withBag($name)RuleSetSet the error bag name on the thrown ValidationException.