v1.30.0
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.
Performance
FluentValidator is now O(n) on conditional wildcard arrays
FluentValidator 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.
Fixed
Object-form conditional rules with a wildcard dependent path
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 dependents
The 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.
Internal
- Extracted the shared optimized-validation preparation into a single
PreparesOptimizedRulestrait used by bothHasFluentRulesand
FluentValidator, so the two entry points cannot drift. - Conditional pre-evaluation now returns an explicit three-state verdict
(exclude / not-excluded / defer); deferred attributes are left intact for the
validator rather than fast-checked away. - Added parity regression tests covering object-form wildcard conditionals, the
coercion/associativeexclude_*cases, batched-DB query counts, and a
FluentValidatorconditional-import benchmark.
Full Changelog: 1.29.0...1.30.0
Benchmark results
| Scenario | Optimizations | Native Laravel | Optimized | Speedup |
|---|---|---|---|---|
| Product import — 500 items, simple rules | Wildcard, fast-check | 222.0ms | 3.2ms | ~69x |
| Nested order lines — 1000 orders × 5 line items | Wildcard, fast-check (nested) | 2918.3ms | 18.2ms | ~160x |
| Event scheduling — 100 items, field-ref dates | Wildcard, partial fast-check | 32.1ms | 1.0ms | ~32x |
| Article submission — 50 items, custom Rule objects | Wildcard only | 10.2ms | 3.3ms | ~3x |
| Conditional import — 100 items, 47 conditional fields | Wildcard, pre-evaluation | 3539.6ms | 68.2ms | ~52x |
| Login form — 3 fields, no wildcards | Fast-check (flat) | 0.2ms | 0.0ms | ~14x |