sandermuller/laravel-fluent-validation
Type-safe, IDE-autocomplete Laravel validation rule builders. Create rules fluently without memorizing strings; each rule exposes only valid methods. Define nested array validation with each()/children(). Optional HasFluentRules trait speeds wildcard validation dramatically (up to 160x).
Turns a silent validation bypass into a loud, actionable error. A wildcard rule key written incorrectly used to apply no validation and let invalid data through unnoticed; it now fails fast with a corrective hint.
A wildcard rule key must contain a .* segment (e.g. items.*.name, or
items.* for a scalar list). A key with a * outside a .* segment — the typo
items* (missing the dot), or a root-level * / *.foo — previously computed an
empty parent internally and was dropped before validation ran. The rule applied
nothing and invalid data passed silently.
Such keys now throw InvalidArgumentException with a corrective hint:
Malformed wildcard rule key [items*]: a wildcard segment must be written as '.*'
(e.g. 'items.*.name'). Did you mean 'items.*'?
This matches the package's existing fail-fast on malformed array-rule keys.
Impact: correctly-formed rules (items.*.name, items.*, addresses.*.postcode,
…) are unaffected — verdicts are identical. Only a previously-malformed key, which
was silently doing nothing, now surfaces as an error. If you hit this on upgrade,
it has been masking a rule that never ran: fix the key to use .*. Root-level
wildcards (*, *.foo) are not supported — nest rules under a named key
(field.*).
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.28.1...1.29.0
A performance patch for conditional wildcard validation. No runtime behaviour or public API changed — validation verdicts are identical; the conditional path just does less redundant work per item.
When a wildcard array carries sibling-dependent conditionals — required_if,
required_with, and the presence/value conditionals — each item can reduce to a
different effective rule set, so that path could not use the existing dispatch
cache and rebuilt its compiled fast-check closures for every single item. In the
common case where many items reduce to the same rule set (e.g. most rows take
the same conditional branch), that work was repeated needlessly.
These compiled fast-checks are now memoized per distinct reduced rule set, keyed by the same content-sensitive key the per-item validator cache already uses. The reduction itself still runs per item (it depends on each item's data); only the fast-check assembly is reused. Larger arrays with repeated conditional shapes do proportionally less compilation work.
The change is internal to the validation engine. Verdicts are unchanged, covered by the existing conditional/parity suites plus dedicated cache-correctness tests.
UPGRADING.md upgrade guide at the repository root (documents the 1.28
Laravel 11 support drop). It is a GitHub-facing document and is not part of the
distributed package.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.28.0...1.28.1
Drops Laravel 11 from the supported matrix and moves the package's AI-authoring dev tooling onto the boost 1.x line. No runtime code or public API changed — the only consumer-facing effect is the narrowed framework constraint.
Every Laravel 11 release (v11.0.0 through v11.54.0) is now flagged by Packagist security advisories, and there is no advisory-free patch in the 11.x line. Composer's advisory policy refuses to install any of them, so the package could no longer be resolved or tested against Laravel 11.
The supported framework constraint narrows accordingly:
illuminate/*: ^11.0||^12.0||^13.0 -> ^12.0||^13.0
The CI matrix drops its Laravel 11 legs (and the orchestra/testbench ^9.0 requirement that only existed to test them); Laravel 12 and 13 remain, across PHP 8.2 / 8.3 / 8.4 on Ubuntu and Windows.
The package's require-dev boost stack moved to the 1.0 line — sandermuller/package-boost-laravel ^1.0 (pulling boost-core 1.x + package-boost-php 1.x) and sandermuller/boost-skills ^2.5. The .config/boost.php config was updated to the array-argument builder API that boost-core 0.20+ requires. These are developer-only dependencies; they are not installed by consumers and do not affect the package at runtime.
No runtime code touched. No public-API change. No new runtime dependencies.
Drop-in for anyone already on Laravel 12 or 13: composer update sandermuller/laravel-fluent-validation.
Projects still on Laravel 11 must upgrade to Laravel 12+ to take this release. Laravel 11 is past security support and its releases are advisory-blocked; staying on it is not a secure option regardless of this package.
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.27.3...1.28.0
Patch fix: the always-on FluentRule guideline never reached consumers' AI tooling because the wrong file extension made boost-core skip it.
core.blade.php → core.mdThe package ships an always-on guideline (FluentRule Validation) that boost-core renders into a consumer's CLAUDE.md / AGENTS.md when they allowlist this package. It carried the standing guidance — FormRequests must use HasFluentRules, the FluentRule:: type list, "don't wrap conditional modifiers in Rule::".
The file was plain markdown — zero Blade directives, zero render-time tokens — but its .blade.php extension made boost-core route it through a renderer that no normal consumer registers. Every consumer's boost sync skipped it:
⚠ guideline `core.blade.php` skipped — no renderer registered for its extension.
So the standing guidance never landed in CLAUDE.md / AGENTS.md; contributors only got it on-demand via the fluent-validation* skills, not as always-on context.
Fix: renamed the file to core.md. boost-core renders .md guidelines natively, so the body now reaches every allowlisting consumer with no withSkillRenderers() registration required. No content change.
Surfaced during a downstream consumer audit of synced AI assets.
Drop-in. composer update sandermuller/laravel-fluent-validation, then re-run vendor/bin/boost sync; the guideline now renders without the skip warning.
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.27.2...1.27.3
Patch fix: conditional-required and presence modifiers combined with nullable() were silently dropped when a FluentRule self-validates.
nullable() no longer drops conditional-required / presence modifiers in self-validationWhen a FluentRule is used as a standalone ValidationRule — inline $request->validate([...]) or Validator::make() without the HasFluentRules trait — it self-validates through SelfValidates. isNullable() skipped validation for a null or absent value whenever nullable was present and the literal required string was absent from the rule's constraints.
Conditional-required and presence modifiers never emit that literal required string — they compile to required_* constraint strings, or to RequiredIf / RequiredUnless objects when built from a closure or bool. So the guard silently dropped the requirement on a null or missing value:
// $enabled = true; the field is omitted or null
FluentRule::email()->requiredIf($enabled)->nullable()
// before: passed — requirement dropped ❌
// after: fails, matching native Laravel ✅
This diverged from both native Laravel (['nullable', Rule::requiredIf(true)]) and the compiled HasFluentRules path, which were already correct — so the gap only surfaced when a FluentRule validated in isolation.
Fix: isNullable() now short-circuits only when the chain carries no presence-forcing modifier. The presence-forcing set is:
required, the string conditionals (requiredIf / requiredUnless / requiredWith / requiredWithout and their _all / _if_accepted / _if_declined variants), and the RequiredIf / RequiredUnless objects.present / presentIf / presentUnless / presentWith / presentWithAllmissing / missingIf / missingUnless / missingWith / missingWithAllDetection matches exact rule names, so required_array_keys (an array-content rule, not a presence requirement) is correctly excluded. prohibited* and exclude* remain short-circuitable — they are satisfied by a null/empty value, which already matched native Laravel.
A second, related gap is closed in the same path: a nullable array no longer short-circuits when it carries nested each() / children() rules. A required child under a null parent is now enforced like native Laravel (and the compiled path) instead of skipped. Wildcard each() still passes on a null parent — it expands to nothing — while fixed children() enforce their sub-rules.
Pinned by tests/RequiredConditionalNullableTest.php: a parity matrix that asserts every conditional-required and presence family — with nullable on and off, across absent / null / empty-string / valid values, plus nested parent-null shapes — behaves identically on the self-validation path, the compiled HasFluentRules path, and native Laravel.
No public-API change. No new dependencies. Same compatibility matrix as 1.27.1.
Reported via #15.
Drop-in. composer update sandermuller/laravel-fluent-validation.
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.27.1...1.27.2
Patch fix for a silent gap in the parent-max:N short-circuit on batched-database validation.
BatchLimitRemap now populates Validator::failed() with the tripped ruleWhen each() carries a batched exists/unique rule and the parent array declares max:N, the package short-circuits before any DB query fires (HasFluentRules::assertParentArraysWithinMax). The short-circuit throws BatchLimitExceededException, and BatchLimitRemap::toValidationException() remaps that to Illuminate\Validation\ValidationException so callers see the package's standard exception type.
The remap built a synthetic Validator::make([], []), pushed the error message into the bag, but never populated failedRules. Net effect:
$caught->validator->errors()->keys(); // ['actions'] ✅
$caught->validator->errors()->first(); // human-readable message ✅
$caught->validator->failed(); // [] ❌
Validator::failed() returning [] meant FluentRulesTester::failsWith('actions', 'max') — and any other assertion path that inspects the rule-bag rather than the message bag — couldn't see which rule actually tripped.
The load-bearing detail: the parent-max guard fires before the validator is built, so this path never goes through Laravel's normal Max rule. Reproducers that drive Validator::make directly against the same FluentRule chain hit Laravel's own Max, which populates failedRules correctly — masking the gap for narrow tests.
Fix: the remap now reflects failedRules onto the synthetic validator:
REASON_PARENT_MAX → [$attribute => ['Max' => [(string) $limit]]]REASON_HARD_CAP → [$attribute => ['BatchLimit' => []]]Pinned by:
tests/BatchValidationGuardsTest.php — asserts the post-remap failed() shape directlytests/Testing/FluentRulesTesterClassTargetsTest.php + tests/Fixtures/BailMaxEachFluentFormRequest.php — guards the FluentRulesTester::failsWith(..., 'max') consumer surfaceNo public-API change. No new dependencies. Same compatibility matrix as 1.27.0.
Same matrix as 1.27.0:
Drop-in. composer update sandermuller/laravel-fluent-validation.
dropUnknownFields() — lenient counterpart to failOnUnknownFields()RuleSet::failOnUnknownFields() rejects unknown input keys with a validation error. dropUnknownFields() does the opposite: it strips them silently.
$validated = RuleSet::from([
'name' => FluentRule::string()->required(),
'meta' => FluentRule::array()->required()->children([
'type' => FluentRule::string()->required(),
]),
])->dropUnknownFields()->validate($request);
// Input: ['name' => 'John', 'meta' => ['type' => 'admin', 'secret' => 'leak']]
// Output: ['name' => 'John', 'meta' => ['type' => 'admin']]
Top-level keys outside the rule set are already excluded from validated(); this flag extends the same behavior to nested array shapes declared via children(), each(), or dotted rule keys. Maps to Laravel's Validator::$excludeUnvalidatedArrayKeys, but gives per-RuleSet control instead of relying on whatever the host factory's flag happens to be set to — useful when an application has called Factory::includeUnvalidatedArrayKeys() globally and a specific call site needs the strict default back.
If both dropUnknownFields() and failOnUnknownFields() are set on the same rule set, failOnUnknownFields() wins — unknown keys trigger a validation error before the drop ever applies. Order is deterministic and pinned by tests/RuleSetTest.php.
Wildcard rule sets (anything built with each()) take a different path internally: the per-item validators don't see siblings, so they can't strip cross-item unknown keys. When dropUnknownFields() is combined with each(), the rule set falls back to a single fully-expanded validator so the flag applies uniformly. The fallback is correct but bypasses the batched-database verifier and per-item fast-check optimizations — fine for typical FormRequest payloads, worth knowing if you're stripping unknowns on five-figure-row imports.
validate(Request) / check(Request)RuleSet::validate() and RuleSet::check() now accept array|Illuminate\Http\Request:
// Before
$validated = RuleSet::from([...])->validate($request->all());
// 1.27
$validated = RuleSet::from([...])->validate($request);
When a Request is passed, the package calls $request->all() once inside a private normalizer at the library boundary, then runs the rest of the pipeline against the array. Functionally identical to the explicit $request->all() form — but the unsafe-input read happens inside the package, not in your controller.
The motivation is downstream static-analysis rules that forbid $request->all() / $request->input() outside FormRequest classes — they trip on the explicit form even when the very next call validates the data. With the overload, ad-hoc controller validations stay clean for those rules without forcing a FormRequest extraction. For real form requests (auth-gated endpoints, reusable rule sets, anything non-trivial), keep using HasFluentRules — the overload is for one-shot ad-hoc validations only.
check() accepts the same overload for the errors-as-data path.
stopOnFirstFailure now propagates through validateStandard()Pre-existing gap. RuleSet::validateStandard() — the fully-expanded fallback path — built its validator without applying ->stopOnFirstFailure($this->stopOnFirstFailure). The non-wildcard branch and the wildcard top-validator both did; only the fallback didn't.
The bug surfaces any time validateStandard() is reached: rules with distinct, and — new in 1.27 — rules with dropUnknownFields() combined with each(). Fix is a single chain call; pinned by tests/RuleSetTest.php.
illuminate/http now declared in requireThe package references Illuminate\Http\UploadedFile from PresenceConditionalReducer and (new in 1.27) Illuminate\Http\Request from RuleSet. Previously the composer.json require block only listed illuminate/contracts, illuminate/support, and illuminate/validation — none of which transitively guarantee illuminate/http. In practice every install ended up with it via laravel/framework, but a downstream consumer using only the standalone illuminate components would have hit an undeclared-dependency runtime fail.
illuminate/http is now in require with the same ^11.0||^12.0||^13.0 constraint as the other illuminate packages. No version change for anyone whose lockfile already pulled it in transitively.
If your application has called $factory->includeUnvalidatedArrayKeys() globally and you previously worked around it with explicit $validator->excludeUnvalidatedArrayKeys = true; after Validator::make, you can now express that directly on the rule set with ->dropUnknownFields().
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.26.0...1.27.0
FastCheckCompiler::compile()Repeated FormRequest validations recompile the same pipe-delimited rule strings into the same closures. 1.26 memoizes the result by rule string — first call builds the closure, subsequent calls hit a process-local map.
Login form benchmark: ~7x → ~12x speedup vs native Laravel. Wildcard-heavy scenarios already routed through RuleCacheKey upstream and don't move on the bench, but the flat-rule FormRequest path (login forms, settings forms, simple CRUD) doubles its relative speedup.
The cache is bounded and Octane-aware:
in: lists, generated regexes) can't grow the cache without bound on long-lived workers. Above the cap the cache resets; correctness is preserved, only the warm hit-rate momentarily drops.CoreValueCompiler::parseDateLiteral calls strtotime() at compile time and bakes the timestamp into the closure. Caching after:today / before:now / after_or_equal:tomorrow / before_or_equal:+1 week / date_equals:today would freeze the relative timestamp for the lifetime of the Octane worker and silently drift validation after midnight. These rules recompile per-call by design — cheap and correct.Pinned by tests/FastCheckCompilerCacheTest.php (7 cases) covering both the stable-rule reuse contract and the no-reuse contract for the five date-comparison opcodes.
OptimizedValidator::passes()Phase 2 used to call evaluateConditionals() on every attribute in the rules map — scanning each rule list for exclude_unless / exclude_if tuples even when the attribute had none. For FormRequests with sparse conditionals over many attributes that scan was the dominant per-call cost.
1.26 replaces it with a single up-front pass (indexConditionalAttrs()) that builds a flat [attribute => list<{action, field, values}>] map, then iterates only attributes that actually have conditionals. The post-eval fast-check branch now runs against pre-parsed tuples instead of re-scanning the rule list.
FormRequest with conditional rules (100 items × 10 fields, 6 conditional): median 13–16ms → ~10ms (~30% faster). The benchmark harness scenarios use RuleSet::validate() (which routes through ItemValidator, not OptimizedValidator) so they don't reflect the win — but FormRequest paths in real apps see it directly.
passes() itself was decomposed into four helpers (runFastCheckPhase, runConditionalPhase, tryFastCheckRemaining, finalizeWithoutParent) to keep cognitive complexity under threshold during the rewrite.
Three small cleanups in the per-item validation loop. None move the benchmark needle individually (variance dominates), but they match existing intent and avoid pointless work on every call:
CoreValueCompiler: replaced in_array($value, [null, '', []], true) with an explicit === chain. The adjacent comment already advertised this — the code just hadn't been updated.ItemValidator: dropped array_values() before passing the fast-check map to passesAllFastChecks. The collector iterates values via foreach, so the rekey was free busywork on every item.ItemErrorCollector: widened the param type to iterable to match.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.25.0...1.26.0
FluentRule::integer(strict: true) — compiles to numeric|integer:strict. Strict-mode rejection only honored on Laravel 12.23+; runtime no-op on older versions.FluentRule::date(message:) and FluentRule::dateTime(message:). Pinned message migrates from date → date_format automatically when ->format(...) is called (explicit messageFor('date_format', ...) wins).integer:strict. CoreValueCompiler and ValueTypePredicates::predicateFor() now branch on the integer.strict flag and emit is_int($v) instead of filter_var(..., FILTER_VALIDATE_INT).HasFluentRules, expanded comparison table to all 25 Rule:: static methods, rule reference rewritten one-concept-per-line, labels CAUTION callout, troubleshooting tightened, Rector/PHPStan companion sections trimmed.migration-patterns.md adds integer:strict row.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.24.0...1.25.0
RuleSet::modifyEach(string $field, array $rules) — sugar over ArrayRule::mergeEachRules().RuleSet::modifyChildren(string $field, array $rules) — sugar over FieldRule::mergeChildRules() (FieldRule-only).ArrayRule::getEachKeyedRules(): ?array<string, ValidationRule>.ArrayRule::getEachListRule(): ?ValidationRule.ArrayRule::getEachRules() — use the two narrow getters. Return type narrows to ?array<string, ValidationRule> in 1.25.0.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.23.0...1.24.0
required_if, required_unless, prohibited_if, prohibited_unless via new ValueConditionalReducer. Wildcard items rewrite to bare required/prohibited (fast-checkable) or drop. Custom {field}.{rule} messages and validation.custom.* overrides preserved.ArrayRule::addEachRule($key, $rule) and mergeEachRules($rules).FieldRule::addChildRule($key, $rule) and mergeChildRules($rules).CannotExtendListShapedEach exception when calling these on list-form each(VR).RuleSet::isEmpty() and RuleSet::hasObjectRules().add*Rule collisions throw LogicException; empty keys throw InvalidArgumentException. Use merge*Rules for later-wins replacement.ArrayRule storage refactored — eachRules always ?array<string, ValidationRule>; separate ?ValidationRule $eachListRule slot. getEachRules() return shape unchanged for BC.array_merge inside loops replaced with collect-then-merge in BatchDatabaseChecker::queryValues(), ArrayRule::buildEachNestedRules/buildChildNestedRules, FieldRule::buildNestedRules.RuleCacheKey::for() (string content verbatim, stable fingerprint for objects/scalars/arrays).compileFluentRules() honors an explicitly empty $rules = [] instead of falling back to rules().Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.22.0...1.23.0
Three batched-DB validation guards (filter → dedup → cap → query):
integer/numeric/uuid/ulid/string rules drop values pre-query.max:N short-circuit — over-limit input fails on parent attribute, zero queries.(table, column, rule-type) group via BatchDatabaseChecker::$maxValuesPerGroup (default 10_000).BatchLimitExceededException (REASON_PARENT_MAX, REASON_HARD_CAP).
Static helper BatchDatabaseChecker::filterValuesByType(array $values, array|string $itemRules): array.
exists + unique conflation when the same (table, column) carried both — registerLookups now refuses to batch and falls back to the default DatabasePresenceVerifier.HasFluentRules, RuleSet::validate(), RuleSet::check()) still throw ValidationException. Direct $ruleSet->prepare() consumers may observe raw BatchLimitExceededException.max:N inspected on the parent (not size/between/outer-ancestors). Numerically-indexed wildcards only. failedValidation() does not fire on parent-max / hard-cap paths.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.21.0...1.22.0
ArrayRule::contains() / doesntContain() route through Rules\Contains / Rules\DoesntContain on Laravel 12+, with CSV-quoting and enum resolution.Arrayable, BackedEnum (uses value), UnitEnum (uses name), plus existing scalar varargs.->message() / messageFor() now bind to 'contains' / 'doesnt_contain' keys.str_getcsv parser. L11 now uses CSV-escaped pipe-string fallback; L12+ uses object form.InvalidArgumentException on multi-array varargs (->contains(['a'], ['b'])).RuntimeException on doesntContain() under Laravel 11.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.20.0...1.21.0
message: named arg on every non-variadic rule method and every factory with a stable error-lookup key.$lastConstraint so ->message() works without messageFor: string, numeric, boolean, accepted, declined, file, image, array, email.message: support: string, numeric, integer, boolean, array, file, image, accepted, declined, email, url, uuid, ulid, ip, ipv4, ipv6, macAddress, json, timezone, hexColor, activeUrl, regex, list.migrate-messages-array Boost skill — rewrites messages(): array overrides into inline message: form, with three rewrite tiers (portable / via-messageFor / unportable).digits, digitsBetween, exactly, date between, image width/minWidth/ratio) bind message: to the last sub-rule; target earlier ones via messageFor.message:: variadic-trailing methods, mode modifiers (rfcCompliant/strict/Password mode toggles), FluentRule::date()/dateTime()/password() factories, FluentRule::field()/anyOf().?string $message = null. ->message() and ->messageFor() remain first-class; neither deprecated.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.19.0...1.20.0
FluentRule::ipv4(), ipv6(), macAddress(), json(), timezone(), hexColor(), activeUrl(), regex(pattern), list(), enum(class). Each accepts an optional ?string $label.FluentRule::declined() — symmetric sibling of accepted(). ->declinedIf(...) replaces base declined.NumericRule sign helpers: positive() (gt:0), negative() (lt:0), nonNegative() (gte:0), nonPositive() (lte:0).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.18.0...1.19.0
FieldRule::__call/__callStatic override — type-specific methods on FluentRule::field() now throw UnknownFluentRuleMethod (extends BadMethodCallException) naming the correct typed builder. Hint table reflection-derived from typed builder public methods.BansFieldRuleTypeMethods arch helper at Testing/Arch/ for Pest/PHPUnit. Walks paths with nikic/php-parser (^5.0, suggest-only).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.17.1...1.18.0
FluentRulesTester promoted to [@api](https://github.com/api) — public methods locked under semver.FastCheckCompiler split into per-family compilers under src/FastCheck/ (CoreValueCompiler, ItemContextCompiler, PresenceConditionalCompiler, ProhibitedCompiler, plus utilities). Public API unchanged.RuleSet into src/Internal/ (ItemRuleCompiler, ItemErrorCollector, ItemValidator).prohibited|sometimes and prohibited|bail (and orderings) now fast-check.laravel/framework:dev-master + orchestra/testbench:dev-master on PHP 8.4.FastCheckCompiler::sizePair, BatchDatabaseChecker::uniqueStringValues, PrecomputedPresenceVerifier::flip — silent skip on malformed input rather than TypeError.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.17.0...1.17.1
SanderMuller\FluentValidation\Contracts\FluentRuleContract (extends Illuminate\Contracts\Validation\ValidationRule) implemented by all 11 rule classes (AcceptedRule, ArrayRule, BooleanRule, DateRule, EmailRule, FieldRule, FileRule, ImageRule, NumericRule, PasswordRule, StringRule). Carries the universally-shared modifier/conditional/metadata/SelfValidates/Conditionable surface. Type-specific methods stay on concrete classes.validateWithBag" updated to the 1.16.0 RuleSet::from(...)->withBag(...)->validate(...) chain.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.16.0...1.17.0
FluentRule::field()->prohibited() is fast-checkable when alone (optionally with nullable/sometimes). Combinations stay on slow path.RuleSet::withBag(string $name) — mirrors Validator::validateWithBag. Sets the thrown ValidationException::$errorBag.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.15.1...1.16.0
PresenceConditionalReducer extracted from RuleSet ([@internal](https://github.com/internal), final, static-only).FastCheckCompiler::sizePair — defensive type guards before count/mb_strlen/numeric ops.BatchDatabaseChecker::uniqueStringValues — coerce unknown shapes to empty string instead of strval TypeError.PrecomputedPresenceVerifier::flip — scalar/Stringable guard; skip unknown shapes.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.15.0...1.15.1
required_with, required_without, required_with_all, required_without_all inside wildcard groups. Reducer rewrites active to required, drops inactive, preserves rules with custom messages on the original rule name.{field}.{rule} map keys (bare/wildcard-prefixed/parent-prefixed) and validation.custom.* translator entries (including Str::is-matched wildcard keys).str_getcsv to match Laravel's ValidationRuleParser::parseParameters exactly.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.14.0...1.15.0
FluentRule::accepted() factory — permissive opt-in (true/1/'1'/'yes'/'on'/'true') without conflicting boolean base. ->acceptedIf(...) replaces the unconditional base.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.13.2...1.14.0
FluentRulesTester::actingAs() was a no-op on Livewire class-string targets. Lifted user-binding into shared applyActingAs() helper.app.key set in Testbench env — Livewire test renders need it.set/call) vs "rules-only shape" (rules() array + with).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.13.1...1.13.2
composer.json shipped with a broken repositories entry.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.13.0...1.13.1
FluentRulesTester::for(Component::class) routes through Livewire::test(). Supports set($key, $value) / set([...]), call($action, ...$args), andCall(...) (queued append-order). Captures both validate() and addError(...) errors. Per-dispatch state consumption.FluentRulesTester::failsWithAny($prefix) — inclusive prefix match (exact OR $prefix.*).FluentRulesTester::failsOnly($field, $rule = null) — exactly-one matching error key.FluentRulesTester::doesNotFailOn(...$fields).RuleSet::modify($field, fn ($rule)) — clones stored rule before callback. Throws LogicException on missing key.livewire/livewire is a soft dev dep; class_exists guard avoids hard fatal in PHPUnit-only suites.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.12.3...1.13.0
RuleSet implements IteratorAggregate — spread ([...$ruleSet]) works without ->toArray().RuleSet::all() — alias of toArray().HasFluentRules and HasFluentValidation auto-unwrap RuleSet from rules().FluentRule::rule() docblock clarifies it mutates the receiver.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.12.2...1.12.3
FluentRulesTester::withRoute(array $params) — bind route parameters for $this->route(name) lookups in authorize()/rules().FluentRulesTester::actingAs($user, $guard = null).RuleSet::only() / except() accept array form in addition to variadic.failsWith() docblock notes FluentRule::integer() compiles to numeric|integer and fails as Numeric on non-numeric input.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.12.1...1.12.2
FluentRulesTester — testing surface for FluentRule chains, RuleSets, FormRequest class-strings, and FluentValidator class-strings. with(array) required before assertions.
passes(), fails(), failsWith($field [, $rule]), failsWithMessage($field, $key, $replacements = []), assertUnauthorized(), errors(), validated().createFrom() + validateResolved(); records ValidationException / AuthorizationException instead of rethrowing.Optional Pest expectations at src/Testing/PestExpectations.php: toPassWith, toFailOn, toBeFluentRuleOf. class_exists-guarded on Pest\Expectation.
RuleSet::only(...$fields), except(...$fields), put($field, $rule), get($field, $default = null).
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.11.0...1.12.0
after:FIELD, after_or_equal:FIELD, before:FIELD, before_or_equal:FIELD, date_equals:FIELD resolved in closure at call time.FastCheckCompiler::compileWithItemContext(string $ruleString): ?\Closure.after:/before:/date_equals: to avoid noise on unrelated rules.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.9.1...1.9.2
required_with, required_without, required_with_all, required_without_all (multi-param). Composes with item-aware date / same / different / confirmed / gt/gte/lt/lte rules from 1.10.0.FastCheckCompiler::compileWithPresenceConditionals(string $ruleString): ?\Closure. Picked up automatically by RuleSet::buildFastChecks after compile() and compileWithItemContext().isLaravelEmpty() helper centralizing presence semantics (null / trim() === '' / empty array / empty Countable).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.10.0...1.11.0
after/before/after_or_equal/before_or_equal/date_equals), same/different (single-param), confirmed/confirmed:custom, sized comparisons with type flag (numeric|gt:, string|gt:, array|gte:, integer|lte:, etc.).FastCheckCompiler::compileWithItemContext(string $ruleString, ?string $attributeName = null): ?\Closure. Used as fallback by RuleSet::buildFastChecks.gt/gte/lt/lte, date_format:X + date field-ref combos, multi-param different:a,b,c, custom Rule objects/closures, distinct, exists/unique with closure callbacks.RepeatedOrEqualToInArrayRector skipped on src/FastCheckCompiler.php.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.9.2...1.10.0
RuleSet::check() returns immutable Validated result. Methods: passes(), fails(), errors(), firstError($field), validated(), safe(), validator().nullable only bypasses null when no implicit rule fires; '' + non-implicit passes (presentOrRuleIsImplicit); required fails on empty arrays; array|min/max use count(); alpha/alpha_dash/alpha_num accept int/float, reject bool/null/array; regex/not_regex require is_string || is_numeric; in/not_in reject non-scalars; dotted rule keys fall through to Laravel in non-wildcard path.filled and sometimes route through Laravel (presence tracking unavailable in closure).min/max without type flag (string/array/numeric/integer) are non-fast-checkable.nullable to rules that should accept null.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.8.2...1.9.0
[@return](https://github.com/return) array<string, array<mixed>> PHPDoc to RuleSet::compileToArrays().Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.8.1...1.8.2
HasFluentValidationForFilament now uses standard validate()/validateOnly() method names. Consumers add an insteadof block for validate, validateOnly, getRules, getValidationAttributes.getRules() merges FluentRule-compiled rules with Filament's form-schema rules (previously dropped schema rules). getValidationAttributes() merges labels too.validate()/validateOnly() preserve Filament's form-validation-error event dispatch and onValidationError() hook.InteractsWithForms), v5 (target InteractsWithSchemas).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.8.0...1.8.1
HasFluentValidationForFilament trait — for Livewire components using Filament's InteractsWithForms/InteractsWithSchemas. Exposes validateFluent() (compiles FluentRule, extracts labels/messages, delegates to Filament's validate()).RuleSet::compileWithMetadata() — returns [rules, messages, attributes] matching validate() parameter order.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.7.1...1.8.0
HasFluentValidation — overrides getRules(), getMessages(), getValidationAttributes() in addition to validate()/validateOnly().each() and children() work in Livewire — flattened to wildcard/fixed-path keys before Livewire reads them.->label()) and messages (->message()) auto-extracted for Livewire validation.$rules property support (in addition to rules() method).excludeIf, excludeUnless, requiredIf, requiredUnless, prohibitedIf, prohibitedUnless, presentIf, presentUnless, missingIf, missingUnless — auto-serialized to backing value.RuleSet::flattenRules() public method for wildcard-preserving expansion.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.6.0...1.7.0
date, date_format, after, before, after_or_equal, before_or_equal, date_equals with literal dates fast-checked via strtotime(). Field references fall through.array and filled.options.*.label expanded within per-item closure.FluentRules marker attribute — for migration tooling detection. No runtime effect.OptimizedValidator pre-groups attributes by wildcard pattern; uses Arr::dot() for O(1) flat lookups.BatchDatabaseChecker — uniqueStringValues() uses SORT_STRING.PrecomputedPresenceVerifier — string-cast flip maps + isset() for O(1) lookups; fixes int/string type mismatch.RuleSet — $flatRules threaded through prepare/expand/separateRules.sandermuller/laravel-fluent-validation-rector — 6 Rector rules to migrate native Laravel validation to FluentRule.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.5.0...1.6.0
(Same content as 1.6.0 — see above.)
Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.4.1...1.5.0
private const int) from BatchDatabaseChecker (PHP 8.3+ syntax).src/Rector from PHPStan; removed stale baseline entries.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.4.0...1.4.1
exists/unique on wildcard fields run a single whereIn instead of one query per item. Works in RuleSet::validate() and HasFluentRules. Scalar where() clauses batched too; closure callbacks fall through.BatchDatabaseChecker, PrecomputedPresenceVerifier.Exists/Unique rule objects retained, so custom messages, attributes, ignore(), validated() work unchanged.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.3.0...1.4.0
RuleSet::failOnUnknownFields() — unknown keys emit a prohibited error. Mirrors Laravel 13.4's FormRequest::failOnUnknownFields.RuleSet::stopOnFirstFailure() — works across top-level fields, wildcard groups, and per-item.messageFor() promoted to primary recommendation. README labels note links all four extraction-supporting paths.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.2.0...1.3.0
FluentRule::macro() — register custom factory methods.RuleSet is now Macroable.HasFluentValidation — explicit mixed types for PHP 8.5; private narrowing helpers (toNullableArray, toStringMap); compileFluentRules() is protected.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.1.0...1.2.0
presentIf, presentUnless, presentWith, presentWithAll, requiredIfAccepted, requiredIfDeclined, prohibitedIfAccepted, prohibitedIfDeclined.contains(...$values), doesntContain(...$values).encoding($encoding).FluentRule::url(), uuid(), ulid(), ip().->toArray(), ->dump(), ->dd() on rules; RuleSet::from(...)->dump()/->dd().presentIf/presentUnless/presentWith/presentWithAll now correctly trigger validation for absent fields in self-validation.FluentRule::field()->toArray() returns [] instead of [''].Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.0.1...1.1.0
$stopOnFirstFailure = true on a FormRequest using HasFluentRules is now honored.HasFluentRules filters rules to submitted fields via filterPrecognitiveRules().Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.0.0...1.0.1
Stable release. API locked under semver.
string, integer, numeric, email, password, date, dateTime, boolean, array, file, image, field, plus anyOf (Laravel 13+).HasFluentRules (FormRequest), HasFluentValidation (Livewire), FluentValidator (custom).each() / children(), ->label(), ->message(), ->messageFor(), Email::default() / Password::default() integration, RuleSet, RuleSet::compileToArrays(), whenInput(), macros, Octane-safe.compileToArrays() return type.minimum-stability: dev.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.5.3...1.0.0
sandermuller/package-boost for AI skills/guidelines management..ai/ directory with code-review, backend-quality, bug-fixing, evaluate, write-spec, implement-spec, pr-review-feedback, autoresearch skills..mcp.json — Laravel Boost MCP config.CONTRIBUTING.md.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.5.2...0.5.3
RuleSet::compileToArrays() — returns array<string, array<mixed>>. For Livewire's $this->validate() in Filament components where HasFluentValidation collides with InteractsWithSchemas.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.5.1...0.5.2
RuleSet::compileToArrays() (see 0.5.2).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.5.0...0.5.1
FluentRule::email() uses Email::default() when configured via Email::defaults(). Opt out with FluentRule::email(defaults: false).defaults: false parameter on FluentRule::email() and FluentRule::password().Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.5...0.5.0
Email::default() auto-application from 0.4.4. FluentRule::email() returns to basic 'email'. Use ->rule(Email::default()) explicitly.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.4...0.4.5
FluentRule::integer() — shorthand for numeric()->integer().FluentRule::email() uses Email::default() when configured. (Reverted in 0.4.5.)->messageFor('rule', 'msg') — position-independent message attachment.->notIn() accepts scalars.same(), different(), confirmed() on FieldRule.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.3...0.4.4
Required, Min, Max, ...) exposed in $validator->failed() from self-validation. Fixes Livewire assertHasErrors(['field' => 'rule']) without the HasFluentValidation trait.fluent-validation-livewire Boost skill.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.2...0.4.3
confirmed() on PasswordRule.min() on PasswordRule (chain method, in addition to constructor password(min: ...)).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.1...0.4.2
required, nullable, bail) come before type constraint. FluentRule::string()->required() compiles to required|string.OptimizedValidator pre-evaluates exclude_unless / exclude_if before validation loop.RuleSet::validate() pre-computes reduced rule sets per unique condition value, caches validators by signature.FluentRule::password() uses Password::default() when configured. Override via password(min: 12).Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.4.0...0.4.1
HasFluentValidation trait for Livewire — overrides validate()/validateOnly(), compiles FluentRule, extracts labels/messages, expands wildcards. Uses Livewire's getDataForValidation() / unwrapDataForValidation(). Note: use flat wildcard keys for Livewire array fields, not each().in() and notIn() accept Arrayable.Exists, Unique, Dimensions stay as objects (only In/NotIn stringified) to preserve closure-based where() constraints.toKilobytes() uses 1000 (decimal) matching Laravel's File rule. '5mb' → 5000 KB.OptimizedValidator — factory resolver restored via try/finally.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.6...0.4.0
(int) $v === $v with filter_var($v, FILTER_VALIDATE_INT) to match validateInteger.date and filled from fast-check (strtotime diverges from Laravel's date_parse; filled needs key-presence context).HasFluentRules factory resolver swap wrapped in try/finally.WildcardExpander depth limit of 50 levels.distinct() on ArrayRule.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.5...0.3.6
Carbon::parse() (throws on invalid; getTimestamp() never returns false) with strtotime(). Added DateFuncCallToCarbonRector to skip list.benchmark.php updated for buildFastChecks() tuple return.In, NotIn, Exists, Unique, Dimensions during compilation, enabling fast-check.email, url, ip, uuid, ulid, alpha, alpha_dash, alpha_num, accepted, declined, filled, not_in, regex, not_regex, digits, digits_between. (Note: some reverted in later releases — see 0.3.6, 1.9.0.)Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.3...0.3.4
HasFluentRules conditionally creates OptimizedValidator when fast-checkable wildcard rules are detected.FastCheckCompiler — shared rule-string-to-closure compiler used by RuleSet and OptimizedValidator.FluentFormRequest reduced to class FluentFormRequest extends FormRequest { use HasFluentRules; }.RuleSet::validate() split into separateRules, validateWildcardGroups, validateItems, passesAllFastChecks, throwValidationErrors.SelfValidates::validate() split into buildRulesForAttribute, buildMessages, buildAttributes, forwardErrors.buildCompiledRules() ordering — presence modifiers, then strings, then other object rules.accepted/declined correctly bail to Laravel instead of being silently ignored.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.2...0.3.3
ArrayRule::compiledRules() now includes array type — FluentRule::array()->nullable() compiles to array|nullable (was nullable).FluentRule after compiledRules() no longer inherits stale cache. Enables (clone $rules[self::TYPE])->rule($extraClosure).OptimizedValidator::validateAttribute() signature matches parent.HasFluentRules conditionally uses OptimizedValidator when fast-checkable wildcard rules present.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.1...0.3.2
ArrayRule::compiledRules() includes array type (see 0.3.2).OptimizedValidator::validateAttribute() signature.exists() and unique() — optional ?Closure $callback 3rd arg for ->where()/->whereNull()/->ignore().FluentFormRequest base class.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.3.0...0.3.1
FluentFormRequest — FormRequest + HasFluentRules + per-attribute fast-check via OptimizedValidator.OptimizedValidator — Validator subclass overriding validateAttribute() with per-attribute fast-check cache.exists() and unique() (optional ?Closure $callback 3rd arg).compiledRules() delegates to buildValidationRules(); only joins to pipe-string when all rules are strings.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/0.2.4...0.3.0
ExcludeIf, RequiredIf, ProhibitedIf) come before string constraints in compiled output.FluentValidator and HasFluentRules reverted to safe prepare() path for cross-field wildcard compatibility.OptimizedValidator (per-item optimization remains in RuleSet::validate()).HasFluentRules trait for FormRequest integration.FluentValidator base class.RuleSet::validate().RuleSet::prepare() — single-call pipeline returning PreparedRules DTO.optimize-validation Boost skill.false no longer becomes empty string).FluentRule factory: string, numeric, date, dateTime, boolean, array, email, file, image, password, field, anyOf.RuleSet builder: from(), field(), merge(), when()/unless(), expandWildcards(), validate().WildcardExpander (O(n) tree traversal).FluentRule::string('Full Name')).message() and fieldMessage() for per-rule custom error messages.each() (wildcard child rules) and children() (fixed-key child rules) on ArrayRule.whenInput().requiredIf, requiredUnless, excludeIf, excludeUnless, prohibitedIf, prohibitedUnless.in() and notIn() accept BackedEnum class names.required, nullable, sometimes, present, filled, bail, exclude.Macroable and Conditionable support.fluent-validation Boost skill.How can I help you explore Laravel packages today?