con un clic
fluent-validation
// FluentRule builders for Laravel validation. Activates when: writing/modifying rules in FormRequest, Livewire component, or Validator; user mentions FluentRule, HasFluentRules, HasFluentValidation, fluent validation.
// FluentRule builders for Laravel validation. Activates when: writing/modifying rules in FormRequest, Livewire component, or Validator; user mentions FluentRule, HasFluentRules, HasFluentValidation, fluent validation.
Pre-push / pre-release checklist. Runs Rector, Pint, full test suite, PHPStan, audits README + `.ai/` docs for staleness, and runs both benchmark harnesses (benchmark.php + --group=benchmark). Activate before: pushing to remote, tagging a release, writing release notes, or when user mentions: pre-release, pre-push, release checklist, ship, cut release, release notes.
FluentRule in Livewire components via HasFluentValidation trait. Activates when: writing Livewire validate(), rules() in Component, assertHasErrors, or Livewire validation.
Migrate FormRequest `messages(): array` to inline `message:` on fluent chains. Dry-run, then apply. Activates when: user mentions migrate messages, messages array, inline message, remove messages().
Scan validation for fluent upgrades: missing HasFluentRules, convertible string rules, label() and each() opportunities. Activates when: optimizing/migrating validation, converting to fluent rules, validation performance.
Implements a specification file phase-by-phase with progress tracking. Activates when: implementing a spec, building from a spec, starting a spec phase, or when user mentions: implement spec, spec file, implement phase, build spec, start phase.
Writes implementation-ready specification files with progress-trackable phases. Activates when: writing a spec, creating a spec file, documenting a feature plan, or when user mentions: write spec, create spec, spec format, spec template.
| name | fluent-validation |
| description | FluentRule builders for Laravel validation. Activates when: writing/modifying rules in FormRequest, Livewire component, or Validator; user mentions FluentRule, HasFluentRules, HasFluentValidation, fluent validation. |
Do not prompt the user when this skill is loaded. Apply these rules automatically when writing or modifying validation code. This skill provides context, not an interactive command.
When sandermuller/laravel-fluent-validation is installed, use FluentRule for type-safe, fluent validation rule building with IDE autocompletion.
For deeper guidance, read the relevant reference file before implementing:
references/rule-types.md — complete method reference for all rule types (string, numeric, date, boolean, array, file, image, email, password, field)references/field-modifiers.md — shared modifiers: presence, prohibition, exclusion, labels, messages, conditionals, escape hatchreferences/migration-patterns.md — common before/after patterns when converting from string rules. Read this to avoid unnecessary ->rule() escape hatchesreferences/performance.md — wildcard optimization, RuleSet API, benchmarks, custom Validator integrationuse SanderMuller\FluentValidation\FluentRule;
use SanderMuller\FluentValidation\RuleSet;
use SanderMuller\FluentValidation\Contracts\FluentRuleContract; // for rules() return types
Every rule class in Rules/* implements FluentRuleContract (which itself extends Laravel's ValidationRule). Use it as the single return type for rules() instead of enumerating concrete classes:
/** @return array<string, FluentRuleContract> */
public function rules(): array
{
return [
'name' => FluentRule::string()->required()->min(2),
'age' => FluentRule::numeric()->nullable()->integer(),
'email' => FluentRule::email()->required(),
];
}
| Factory Method | Returns | Base Laravel Rule |
|---|---|---|
FluentRule::string('Label?') | StringRule | 'string' |
FluentRule::numeric('Label?') | NumericRule | 'numeric' |
FluentRule::integer('Label?') | NumericRule | 'numeric|integer' (shorthand) |
FluentRule::date('Label?') | DateRule | 'date' |
FluentRule::dateTime('Label?') | DateRule | 'date_format:Y-m-d H:i:s' |
FluentRule::boolean('Label?') | BooleanRule | 'boolean' |
FluentRule::accepted('Label?') | AcceptedRule | 'accepted' (permissive; no boolean base) |
FluentRule::array(keys?, label:?) | ArrayRule | 'array' |
FluentRule::email('Label?') | EmailRule | 'string' + 'email' |
FluentRule::password(min?, label:?) | PasswordRule | 'string' + Password |
FluentRule::file('Label?') | FileRule | 'file' |
FluentRule::image('Label?') | ImageRule | 'image' |
FluentRule::field('Label?') | FieldRule | (no type constraint) |
FluentRule::anyOf([...]) | AnyOf | OR combinator |
All factory methods accept an optional label that replaces :attribute in error messages.
FluentRule::field() is the untyped builder — use it when no base type constraint applies. It supports modifiers (required, nullable, present, conditional presence), children(), same/different/confirmed, embedded-rule factories (exists, unique, enum, in, notIn), and the ->rule(...) escape hatch. Do not chain type-specific methods (min, max, regex, email, digits, mimes, before/after, contains, etc.) on field() — those live on the typed builders (string(), numeric(), array(), date(), file(), boolean()). Calling one on field() throws UnknownFluentRuleMethod at runtime with a hint pointing at the correct typed builder. When generating code, pick the typed builder matching the field's type for any rule that constrains the value itself.
Same reasoning applies to the string-escape-hatch form: do not emit FluentRule::field()->rule('min:1'), ->rule('max:...'), ->rule('regex:...'), or any other ->rule('type_rule:...') on field(). It works at runtime but signals the field has a base type that should be encoded with a typed builder — emit FluentRule::numeric()->min(1) / FluentRule::string()->regex(...) / etc. instead.
public function rules(): array
{
return [
'name' => FluentRule::string('Full Name')->required()->min(2)->max(255),
'email' => FluentRule::email('Email')->required()->unique('users'),
'age' => FluentRule::numeric('Age')->nullable()->integer()->min(0),
'role' => FluentRule::string()->required()->in(RoleEnum::class),
'tags' => FluentRule::array()->required()->each(FluentRule::string()->max(50)),
'items' => FluentRule::array()->required()->each([
'name' => FluentRule::string('Item Name')->required(),
'qty' => FluentRule::numeric()->required()->integer()->min(1),
]),
'search' => FluentRule::array()->children([
'value' => FluentRule::string()->nullable(),
'regex' => FluentRule::string()->nullable()->in(['true', 'false']),
]),
'avatar' => FluentRule::image()->nullable()->max('2mb'),
'password' => FluentRule::password()->required()->mixedCase()->numbers(),
];
}
Labels — replace :attribute in all error messages:
FluentRule::string('Full Name')->required() // "The Full Name field is required."
Per-rule messages — preferred form is inline message::
FluentRule::string()->required(message: 'We need this!')->min(2, message: 'Too short.')
Also available: ->method(...)->message('...') (chained shorthand), ->messageFor('rule', '...') (escape hatch for variadic methods, custom rule objects, and targeting non-last sub-rule on composite methods like digits).
Wildcard children (each) — produces items.*.name:
FluentRule::array()->each([
'name' => FluentRule::string()->required(),
])
Fixed-key children (children) — produces search.value:
FluentRule::array()->children([
'value' => FluentRule::string()->nullable(),
])
Build-time conditions — evaluated when building rules:
FluentRule::string()->when($isAdmin, fn ($r) => $r->min(12))
Validation-time conditions — evaluated with input data:
FluentRule::string()->whenInput(
fn ($input) => $input->role === 'admin',
fn ($r) => $r->required()->min(12),
)
Enum values in in() — accepts enum class directly:
FluentRule::string()->in(StatusEnum::class)
Fixed-key children (children) on field — for untyped parents with known sub-keys:
FluentRule::field()->required()->children([
'value' => FluentRule::string()->nullable(),
'regex' => FluentRule::string()->nullable()->in(['true', 'false']),
])
Escape hatch — any Laravel rule (string, object, array tuple):
FluentRule::string()->rule('email:rfc,dns')
FluentRule::file()->rule(['mimetypes', ...$types])
FluentRule::string()->rule(new MyCustomRule())
Extend a parent's each() / children() shape — for subclass FormRequests that add one sub-rule to the parent's map. Preserves parent base constraints (nullable, max, required, etc.):
// Sugar form — later-wins merge
return parent::rules()->modifyEach('answers', [
'id' => FluentRule::numeric()->nullable(),
]);
// Primitive form — throws on existing-key collision
return parent::rules()->modify('answers', fn (ArrayRule $rule) =>
$rule->addEachRule('id', FluentRule::numeric()->nullable())
);
RuleSet::modifyEach / modifyChildren wrap mergeEachRules / mergeChildRules (later-wins on collision). For strict add-only, use the primitive modify(..., fn ($r) => $r->addEachRule(...)) — addEachRule / addChildRule throw on existing-key collision. addEachRule / mergeEachRules throw CannotExtendListShapedEach when the parent's each() is list-shaped (each(FluentRule::string()) without keys) — convert to keyed form first.
Macros — reusable rule chains registered in a service provider:
NumericRule::macro('percentage', fn () => $this->integer()->min(0)->max(100));
// Then: FluentRule::numeric()->percentage()
All rule types support macros via Macroable.
HasFluentRules automatically applies O(n) wildcard expansion, per-attribute fast-checks (25 rules supported), and batched DB validation (exists/unique rules batched into a single whereIn query). Up to 97x faster for simple rules, 10x for mixed rule sets, and N→1 queries for database rules. Use RuleSet::validate() for inline validation outside FormRequests. See references/performance.md for details.
See the fluent-validation-livewire skill for full Livewire guidance. Key point: add use HasFluentValidation to Livewire components. Both flat wildcard keys (items.*) and each()/children() work — the trait handles the expansion.
For unit tests of FluentRule chains, RuleSets, FluentFormRequest subclasses, and FluentValidator subclasses, use FluentRulesTester instead of going through postJson() or hand-rolling a validateRules() helper:
use SanderMuller\FluentValidation\Testing\FluentRulesTester;
FluentRulesTester::for($rules)->with($data)->passes();
FluentRulesTester::for(StorePostRequest::class)->with($payload)->failsWith('email', 'email');
FluentRulesTester::for(JsonImportValidator::class, $user)->with($payload)->passes();
// Bind route params + authenticated user when authorize()/rules() need them
FluentRulesTester::for(UpdateVideoRequest::class)
->withRoute(['video' => $video])
->actingAs($user)
->with($payload)
->passes();
// Livewire components — auto-detected. set/call/mount + andCall for multi-action chains.
FluentRulesTester::for(AppealPage::class)
->set('type', 'refund')
->set('reason', 'Long enough reason.')
->call('openModal')
->andCall('submit')
->passes();
Target-shape cheatsheet:
| Target | Trigger | Notes |
|---|---|---|
array<string, mixed> of rules | with($data) | wraps via RuleSet::from($rules)->check($data) |
RuleSet instance | with($data) | direct ->check($data) |
Single ValidationRule | with(['value' => $x]) | wrapped under 'value' key |
FormRequest class-string | with($data) | full validateResolved() pipeline; pair with withRoute() / actingAs() |
FluentValidator class-string | with($data) + variadic ctor args via for(class, ...$args) | |
Livewire Component class-string | set()/with() then call() (+ andCall()) | multi-action queue dispatches against one Livewire::test() instance; each new chain resets state |
More assertions: failsWithAny($prefix) for subtree failures (exact + dotted descendants), failsOnly($field, $rule = null) for surgical single-field failures, doesNotFailOn(...$fields) for negative assertions, failsWithMessage($field, $key, $replacements = []) for rendered-translation matches, assertUnauthorized() for FormRequest authorize() gate failures. RuleSet has only/except/put/get/modify/all() + IteratorAggregate spread support.
with(array $data) is required before any assertion (Livewire targets require call() instead — validation only runs on action dispatch). Variadic args after the target are forwarded to FluentValidator subclass constructors after $data. withRoute() and actingAs() are re-callable and only meaningful for FormRequest class-string targets. For unauthorized FormRequests, the AuthorizationException is recorded — assert via ->fails() or ->assertUnauthorized(). FluentRulesTester is the only stable test surface; everything else under Testing\ is @internal.
Optional Pest expectations live in src/Testing/PestExpectations.php — require_once from tests/Pest.php to opt in to expect($rules)->toPassWith($data), ->toFailOn($data, $field, $rule), ->toBeFluentRuleOf($class).
Extend FluentValidator instead of Validator. Handles the full pipeline automatically:
use SanderMuller\FluentValidation\FluentValidator;
class MyValidator extends FluentValidator
{
public function __construct(array $data) {
parent::__construct($data, $this->buildRules());
}
}
If the class holds its rules in a method that isn't called rules() (e.g. rulesWithoutPrefix() in JSON-import pipelines), mark the method with #[FluentRules] so the migration Rector rules detect it. The attribute is migration-only and has no runtime effect:
use SanderMuller\FluentValidation\FluentRules;
class JsonImportValidator extends FluentValidator
{
#[FluentRules]
public function rulesWithoutPrefix(): array { /* ... */ }
}