Create ShortCircuit validator and ShortCircuitable interface#1663
Create ShortCircuit validator and ShortCircuitable interface#1663henriquemoody wants to merge 1 commit intoRespect:mainfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1663 +/- ##
============================================
+ Coverage 99.53% 99.55% +0.01%
- Complexity 929 963 +34
============================================
Files 190 191 +1
Lines 2170 2257 +87
============================================
+ Hits 2160 2247 +87
Misses 10 10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
e6287f2 to
2bc3837
Compare
2bc3837 to
fdd7ce6
Compare
| Like PHP's `&&` operator, it uses short-circuit evaluation: once the outcome is determined, remaining validators are | ||
| skipped. Unlike [AllOf](AllOf.md), which evaluates all validators and collects all failures, `ShortCircuit` returns | ||
| immediately. |
There was a problem hiding this comment.
We should compare somewhere what v::shortCircuit() does (localized stop-on-first-error behavior) and what ->check() does (global stop-on-first-error behavior) and their differences, with helpful examples.
| yield 'allOf(10)' => ['allOf', $this->buildValidators(10)]; | ||
| yield 'oneOf(10)' => ['oneOf', $this->buildValidators(10)]; | ||
| yield 'anyOf(10)' => ['anyOf', $this->buildValidators(10)]; | ||
| yield 'noneOf(10)' => ['noneOf', $this->buildValidators(10)]; | ||
| yield 'allOf(100)' => ['allOf', $this->buildValidators(100)]; | ||
| yield 'oneOf(100)' => ['oneOf', $this->buildValidators(100)]; | ||
| yield 'anyOf(100)' => ['anyOf', $this->buildValidators(100)]; | ||
| yield 'noneOf(100)' => ['noneOf', $this->buildValidators(100)]; |
There was a problem hiding this comment.
On #1633, I decided not to tackle Each, but you did. You should also include a benchmark for it.
There was a problem hiding this comment.
I added some for Each, All, and Domain, and I was surprised to see that it actually became slower in the majority of cases. I have to investigate a bit further. I think the different is negligible, but I would like to dig a bit deeper because we're creating less objects, I don't see an obvious reason.
There was a problem hiding this comment.
You're not providing Each and All many items. You're passing only three items: 42, 43, 44.
The logical validators break on the list of validators. Each and All will execute all of them.
So, in order to properly evaluate them, you need to make series of random input items that are likely to break at different points instead of series of validators that run against the same input.
ValidatorBuilder::__callStatic(...$params)->isValid($randomItems);For the validator chain, I used 10 and 100 (a small chain with 10 validators, a large chain with 100 validators).
I would recommend using 10 and 100 as well for the number of items.
Small losses with fewer number of validators/items are expected due to the increase in abstraction. This is what you are observing because you're only using a size of 3: [42, 43, 44]. You can further track down the expensive abstractions doing bench:profile to see hot paths (methods that are most called) and further optimize. 2~3% loss in small chains is irrelevant though.
I also recommend adding a benchmark for ShortCircuit itself, since it's the core of it all.
There was a problem hiding this comment.
Overall seems like a solid prototype of what #1633 also aims to achieve, with similar gains and more ambitious (tackling Each, for example).
If we're aiming for completeness (no multi-step PRs), I do believe KeySet should also be available for global short circuit. Otherwise, I would prefer if you did it in smaller steps (logical AllOf/OneOf/NoneOf/AnyOf/Circuit first, then the others that require more heuristic to short circuit properly). It's up to you (single-shot or smaller PRs, I'm okay with either choice).
a69fddc to
9dc4e93
Compare
3f77b12 to
68593c2
Compare
There was a problem hiding this comment.
Pull request overview
This PR renames the Circuit validator to ShortCircuit and introduces a ShortCircuitable interface that enables composite validators to implement optimized short-circuit evaluation. The renaming better reflects the concept of short-circuit evaluation (similar to boolean operators && and ||) rather than the misleading "fail fast" terminology.
Changes:
- Renamed
Circuitvalidator toShortCircuitwith updated documentation and all references - Added
ShortCircuitableinterface for validators that support short-circuit evaluation - Implemented short-circuit behavior in composite validators:
AllOf,AnyOf,OneOf,NoneOf,Each,All, andKeySet - Added new
check()method toValidatorBuilderfor short-circuit validation with exceptions - Modified
isValid()to use short-circuit evaluation for performance optimization - Updated all mixin interfaces to reflect the naming change
Reviewed changes
Copilot reviewed 59 out of 59 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Validators/ShortCircuit.php | Renamed from Circuit, implements short-circuit evaluation of validators |
| src/Validators/Core/ShortCircuitable.php | New interface for validators supporting short-circuit evaluation |
| src/Helpers/CanEvaluateShortCircuit.php | Helper trait for delegating to short-circuit evaluation when available |
| src/ValidatorBuilder.php | Added check() method and ShortCircuitable interface implementation |
| src/Validators/{AllOf,AnyOf,OneOf,NoneOf,Each,All,KeySet}.php | Implemented short-circuit evaluation for composite validators |
| src/Validators/{Domain,Tld,Url}.php | Updated to use ShortCircuit instead of Circuit |
| tests/unit/Validators/ShortCircuitTest.php | Renamed and updated test class |
| tests/unit/Validators/{AllOf,AnyOf,OneOf,NoneOf,Each,All,KeySet}Test.php | Added short-circuit behavior tests |
| tests/feature/Validators/*.php | Comprehensive feature tests for short-circuit behavior |
| tests/benchmark/CompositeValidatorsBench.php | New benchmark tests for performance comparison |
| src/Mixins/*.php | Updated all mixin interfaces to use shortCircuit instead of circuit |
| docs/validators/ShortCircuit.md | New documentation replacing Circuit.md |
| docs/validators/Circuit.md | Removed old documentation |
| docs/validators/{AllOf,AnyOf,OneOf,NoneOf,After}.md | Updated references from Circuit to ShortCircuit |
| docs/validators.md | Updated validator listing and categorization |
| docs/migrating-from-v2-to-v3.md | Updated migration guide with ShortCircuit information |
| docs/feature-guide.md | Updated feature guide references |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1820e37 to
7838e3a
Compare
|
@alganet, I think this might be blocking our release, but I'm short on time in the upcoming days. I wanted to track the performance issue with each and all, but I don't have much time at moment, and I'm prioritizing reviewing your merge request. I think we could try working on performance of all and each later on, and merge this as is, but let me know if you think otherwise. |
7838e3a to
f44e979
Compare
72eb4b0 to
ef966ae
Compare
This commit introduces a mechanism for validators to return early once the validation outcome is determined, rather than evaluating all child validators. The ShortCircuit validator evaluates validators sequentially and stops at the first failure, similar to how PHP's && operator works. This is useful when later validators depend on earlier ones passing, or when you want only the first error message. The ShortCircuitCapable interface allows composite validators (AllOf, AnyOf, OneOf, NoneOf, Each, All) to implement their own short-circuit logic. Why "ShortCircuit" instead of "FailFast": The name "FailFast" was initially considered but proved misleading. While AllOf stops on failure (fail fast), AnyOf stops on success (succeed fast), and OneOf stops on the second success. The common behavior is not about failing quickly, but about returning as soon as the outcome is determined—which is exactly what short-circuit evaluation means. This terminology is familiar to developers from boolean operators (&& and ||), making the behavior immediately understandable. Co-authored-by: Alexandre Gomes Gaigalas <alganet@gmail.com> Assisted-by: Claude Code (Opus 4.5)
ef966ae to
c2957db
Compare
This commit introduces a mechanism for validators to return early once the validation outcome is determined, rather than evaluating all child validators.
The ShortCircuit validator evaluates validators sequentially and stops at the first failure, similar to how PHP's && operator works. This is useful when later validators depend on earlier ones passing, or when you want only the first error message.
The ShortCircuitCapable interface allows composite validators (AllOf, AnyOf, OneOf, NoneOf, Each, All) to implement their own short-circuit logic:
Why "ShortCircuit" instead of "FailFast":
The name "FailFast" was initially considered but proved misleading. While AllOf stops on failure (fail fast), AnyOf stops on success (succeed fast), and OneOf stops on the second success. The common behavior is not about failing quickly, but about returning as soon as the outcome is determined—which is exactly what short-circuit evaluation means. This terminology is familiar to developers from boolean operators (&& and ||), making the behavior immediately understandable.
Local benchmark results
Before
After