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.
Performance
Fluent ->excludeUnless() / ->excludeIf() now pre-evaluate
1.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.
Fixed
Conditional dependent-value coercion matches native
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 absent
Mirroring 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.
Regex rules containing a pipe survive compilation
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.
Every exclude condition on a field is evaluated
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.
Internal
- Extracted two shared collaborators —
ConditionalValueMatcher(Laravel-parity
dependent-value coercion) andExcludeConditionExtractor(tuple/string form
recognition) — sopreExcludeRules,ConditionalEvaluationPhase, and
ItemRuleCompilerevaluate conditionals identically. - Added parity regression tests covering the coercion grid, string-form pruning
(flat, nested children, enum values), regex-with-pipe, the existence guard,
and multi-condition / required-tuple survival.
Full Changelog: 1.30.0...1.30.1
Benchmark results
| Scenario | Optimizations | Native Laravel | Optimized | Speedup |
|---|---|---|---|---|
| Product import — 500 items, simple rules | Wildcard, fast-check | 224.9ms | 3.2ms | ~69x |
| Nested order lines — 1000 orders × 5 line items | Wildcard, fast-check (nested) | 2839.9ms | 18.2ms | ~156x |
| Event scheduling — 100 items, field-ref dates | Wildcard, partial fast-check | 32.2ms | 1.0ms | ~32x |
| Article submission — 50 items, custom Rule objects | Wildcard only | 10.4ms | 3.3ms | ~3x |
| Conditional import — 100 items, 47 conditional fields | Wildcard, pre-evaluation | 3606.6ms | 70.2ms | ~51x |
| Login form — 3 fields, no wildcards | Fast-check (flat) | 0.2ms | 0.0ms | ~16x |