Skip to content

Create ShortCircuit validator and ShortCircuitable interface#1663

Open
henriquemoody wants to merge 1 commit intoRespect:mainfrom
henriquemoody:validator/short-circuit
Open

Create ShortCircuit validator and ShortCircuitable interface#1663
henriquemoody wants to merge 1 commit intoRespect:mainfrom
henriquemoody:validator/short-circuit

Conversation

@henriquemoody
Copy link
Member

@henriquemoody henriquemoody commented Feb 1, 2026

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:

  • AllOf: stops at first failure (like &&)
  • AnyOf: stops at first success (like ||)
  • OneOf: stops when two validators pass (already invalid)
  • NoneOf: stops at first success (already invalid)
  • Each/All: stops at first failing item

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

Screenshot 2026-02-02 at 00 29 41

After

Screenshot 2026-02-02 at 00 29 09

@henriquemoody henriquemoody requested a review from alganet February 1, 2026 12:36
@codecov
Copy link

codecov bot commented Feb 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.55%. Comparing base (570ba48) to head (c2957db).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@henriquemoody henriquemoody force-pushed the validator/short-circuit branch from e6287f2 to 2bc3837 Compare February 1, 2026 12:37
@henriquemoody henriquemoody force-pushed the validator/short-circuit branch from 2bc3837 to fdd7ce6 Compare February 1, 2026 12:56
Comment on lines +13 to +17
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 42 to 77
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)];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On #1633, I decided not to tackle Each, but you did. You should also include a benchmark for it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@alganet alganet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@henriquemoody henriquemoody force-pushed the validator/short-circuit branch 3 times, most recently from a69fddc to 9dc4e93 Compare February 1, 2026 23:29
@henriquemoody henriquemoody force-pushed the validator/short-circuit branch 6 times, most recently from 3f77b12 to 68593c2 Compare February 5, 2026 11:05
@henriquemoody henriquemoody changed the title Create ShortCircuit validator and ShortCircuitCapable interface Create ShortCircuit validator and ShortCircuitable interface Feb 5, 2026
@henriquemoody henriquemoody requested a review from Copilot February 5, 2026 11:06
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Circuit validator to ShortCircuit with updated documentation and all references
  • Added ShortCircuitable interface for validators that support short-circuit evaluation
  • Implemented short-circuit behavior in composite validators: AllOf, AnyOf, OneOf, NoneOf, Each, All, and KeySet
  • Added new check() method to ValidatorBuilder for 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.

@henriquemoody henriquemoody force-pushed the validator/short-circuit branch 3 times, most recently from 1820e37 to 7838e3a Compare February 5, 2026 12:02
@henriquemoody henriquemoody requested a review from alganet February 5, 2026 12:02
@henriquemoody henriquemoody marked this pull request as ready for review February 5, 2026 12:03
@henriquemoody
Copy link
Member Author

@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.

@henriquemoody henriquemoody force-pushed the validator/short-circuit branch from 7838e3a to f44e979 Compare February 5, 2026 12:09
@henriquemoody henriquemoody force-pushed the validator/short-circuit branch 3 times, most recently from 72eb4b0 to ef966ae Compare February 5, 2026 12:15
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)
@henriquemoody henriquemoody force-pushed the validator/short-circuit branch from ef966ae to c2957db Compare February 5, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants