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).
A request that defines only schema() can now call ->rules() and get the rules
the builder produced, so tooling or interop that expects a rules() method works
with schema-only requests.
rules() on schema-only requests. HasFluentRules now declares a rules()
method that returns schema()'s output (the raw field map) when a request has
not defined its own rules(). It works whether the request was resolved through
the container or instantiated directly. A rules() you define yourself still
takes precedence — the validator detects a consumer-declared rules() and only
then treats it as a rule source, so the fallback never merges with itself.HasFluentRules now declares public function rules(): array|RuleSet. A rules()
you define must be compatible with that signature: public, returning array or
RuleSet (Laravel's scaffolded rules(): array already is). Untyped or
non-public rules() methods need a small adjustment — see
UPGRADING.md.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.32.0...1.33.0
A FormRequest that declares both a schema() builder and a rules() method now
merges the two instead of schema() silently shadowing rules(). This lets an
abstract base request — or a trait — supply shared fields through one method while
each concrete request overrides or extends them through the other, without either
side being quietly dropped.
schema() and rules() are merged, not either-or. When a request declares
both a FluentSchema-typed schema() and a rules() — anywhere in its class
hierarchy — their rule sets are combined. On a shared field the more specific
declaration wins: the deeper class in the hierarchy, or a body definition over a
trait import, so overriding a base field behaves exactly like a plain method
override, while non-colliding fields from both layers survive. When both are
declared on the same class the tie resolves to schema(). Detection still keys
off the FluentSchema-typed parameter, so an unrelated schema() method is never
hijacked, and either method may still return a plain array or a RuleSet.
This changes behavior only for a request that already declared both methods:
previously schema() won outright and rules() was ignored; now the two merge,
with the more specific declaration winning shared fields. A request that declares
only one of the two is unaffected.
Two additions this release: the FluentSchema builder — an instance-based mirror
of the FluentRule factory that lets you drop the FluentRule:: prefix — and a fifth
automatic optimization, worker-global memoization of Laravel's rule-string parsing.
Both are backward-compatible; existing rules() methods and call sites are unchanged.
FluentSchema builder. Define a schema(FluentSchema $rules) method on a form
request and chain field starters off the injected $rules builder instead of
repeating the FluentRule:: prefix on every line. When a request defines both
schema() and rules(), schema() wins — detection keys off the
FluentSchema-typed parameter, so an unrelated schema() method is never hijacked.
For inline use, RuleSet::define(fn (FluentSchema $rules) => [...]) builds a
RuleSet from the same builder. FluentSchema exposes typed delegates for every
FluentRule factory (single source of truth on FluentRule) and forwards macros,
and mirrors the schema(JsonSchema) shape Laravel's AI SDK uses for structured
output.
MemoizingValidator. A Validator subclass that memoizes
ValidationRuleParser::parse() for string rules in a bounded, worker-global static.
Laravel re-parses each string rule on every internal probe — and, on a validator
reused across array items, once per item; the memo collapses those repeats to a
single hash lookup. OptimizedValidator now extends it, so the residual slow path
of the optimized entry points (HasFluentRules, FluentValidator,
RuleSet::validate(), HasFluentValidation) picks it up automatically. Output stays
byte-identical to Laravel; only string rules are cached. The cache is soft-capped and
reset on overflow, and holds only rule-string → parse-result pairs (never request
data), so it stays Octane-safe.
RuleSet::validate() and HasFluentRules route per-item slow-rule validators
through MemoizingValidator, so a large array whose items fall through to Laravel
(custom Rule objects, cross-field references) parses each rule string once for the
whole array rather than re-parsing per item. The gain is largest on conditional-heavy
and slow-rule-heavy payloads; simple fast-checkable arrays are unaffected — they
already bypass Laravel entirely.Validator::resolver(), the per-item path uses that validator unchanged, so any
resolver-provided behaviour continues to apply.ValidatorStateCopier, and taught it to carry wildcard implicitAttributes so
nested-wildcard expansion metadata pairs with the copied rules.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.30.1...1.31.0
Closes the gap between the optimized validation paths and native Laravel on
conditional rules, and — most importantly — makes 1.30.0's conditional
pre-evaluation actually reach the fluent ->excludeUnless() API. All five fixes
are pinned to native Laravel by parity tests. Surfaced from a production
JSON-import use case profiling large conditional wildcard arrays.
->excludeUnless() / ->excludeIf() now pre-evaluate1.30.0 added conditional pre-evaluation but only recognised the array-tuple rule
form ([['exclude_unless', 'items.*.type', 'chapter'], …]). The fluent
->excludeUnless() API — and any plain string rule — compiles to the string
form (exclude_unless:items.*.type,chapter), which the three pre-evaluation
sites skipped, so an excludeUnless-heavy ruleset stayed fully expanded and
quadratic. A shared extractor now parses tuple, string, and pipe-joined forms,
so the optimization reaches the idiomatic API and nested children() rules
keyed off the parent item's wildcard type. On a 100-item, 20-conditional-field
import this turns a quadratic curve linear.
exclude_unless/exclude_if (and required_if/required_unless on the
per-item path) compared the dependent value with a string cast, diverging from
Laravel when the value needed coercion. A shared matcher now reproduces
Laravel's rules: a null dependent against null, a boolean-declared
dependent submitted as the raw string '0'/'1' against true/false, and
numeric-string loose matching. Verdicts and the validated() payload now match
native across the tuple, string, fluent, and enum-valued forms.
exclude_if is inactive when its dependent field is absentMirroring native Laravel's existence short-circuit, exclude_if no longer
excludes a field when the referenced dependent key is missing from the payload
(an explicitly null dependent still excludes). Previously a missing dependent
could silently drop a field and suppress its validation.
A regex:/^(foo|bar)$/ rule written through the fluent API was corrupted when
the compiled rules were pipe-joined into a single string (Laravel's parser then
split the pattern on its |). Compilation now keeps the array form whenever a
rule token contains a literal |, so the pattern reaches Laravel intact.
A field carrying more than one exclude_* rule had only its first condition
honoured on the per-item path; all of them are now evaluated (the field is
excluded if any fires), matching native. Tuple-form required_if /
required_unless on a field that also carries an exclude rule are no longer
dropped during exclude reduction.
ConditionalValueMatcher (Laravel-parity
dependent-value coercion) and ExcludeConditionExtractor (tuple/string form
recognition) — so preExcludeRules, ConditionalEvaluationPhase, and
ItemRuleCompiler evaluate conditionals identically.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.30.0...1.30.1
FluentValidator now carries the same optimizations as the other entry points,
and conditional rules written as fluent objects against a wildcard sibling path
now match native Laravel. Both surfaced from a production JSON-import use case
validating large conditional wildcard arrays.
FluentValidator is now O(n) on conditional wildcard arraysFluentValidator previously only applied O(n) wildcard expansion and then
handed the fully-expanded rules to a plain Illuminate\Validation\Validator,
which re-resolved exclude_unless/exclude_if itself — quadratic on large
arrays. It now extends OptimizedValidator and shares the full optimized
preparation with HasFluentRules and RuleSet::validate(): pre-evaluation of
conditional rules, fast-check closures, and batched exists/unique queries
(one whereIn instead of one query per row).
On a conditional-heavy import the difference is an order of magnitude — what was
super-linear is now linear, on par with RuleSet::validate(). This also makes
the README's "four optimizations" description accurate for FluentValidator,
which previously only received the first.
Behaviour note: FluentValidator's parent changed from Validator to
OptimizedValidator (itself a Validator subclass, so the public API is
unchanged). Validation verdicts move toward native parity; if a subclass
overrode passes() or depended on the plain validator's internal flow, review
it on upgrade.
A conditional rule written as a FluentRule object whose dependent path is the
full wildcard sibling — e.g.
FluentRule::field()->requiredUnless('items.*.type', 'pause') — was
mis-evaluated in the per-item path: the items.*. prefix was only stripped for
array- and string-form rules, so the object form kept the wildcard and the
reducer's lookup missed. Object and array forms now behave identically and match
native Laravel. Custom labels and messages on the object are preserved.
exclude_* parity for coerced and associative-key dependentsThe conditional pre-evaluation compared the dependent value with a plain string
match, which diverged from Laravel for values needing coercion. exclude_unless
/ exclude_if against a null, boolean, or non-scalar dependent — and against
an associative (non-numeric) wildcard key — are now deferred to the validator
and evaluated authoritatively, so verdicts and the validated() payload match
native. Wildcard dependent paths also resolve to the key at the matching path
position, so associative parents (items.foo.type) are no longer back-filled
from an unrelated numeric descendant.
PreparesOptimizedRules trait used by both HasFluentRules and
FluentValidator, so the two entry points cannot drift.exclude_* cases, batched-DB query counts, and a
FluentValidator conditional-import benchmark.Full Changelog: https://github.com/SanderMuller/laravel-fluent-validation/compare/1.29.0...1.30.0
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?