Skip to content

Commit a69fddc

Browse files
Create ShortCircuit validator and ShortCircuitCapable interface
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. Co-authored-by: Alexandre Gomes Gaigalas <alganet@gmail.com> Assisted-by: Claude Code (Opus 4.5)
1 parent 699f060 commit a69fddc

48 files changed

Lines changed: 732 additions & 200 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/feature-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ Beyond the examples above, Respect\Validation provides specialized validators fo
129129
- **Grouped validation**: Combine validators with AND/OR logic using [AllOf](validators/AllOf.md), [AnyOf](validators/AnyOf.md), [NoneOf](validators/NoneOf.md), [OneOf](validators/OneOf.md).
130130
- **Iteration**: Validate every item in a collection with [Each](validators/Each.md).
131131
- **Length, Min, Max**: Validate derived values with [Length](validators/Length.md), [Min](validators/Min.md), [Max](validators/Max.md).
132-
- **Special cases**: Handle dynamic rules with [Lazy](validators/Lazy.md), short-circuit on first failure with [Circuit](validators/Circuit.md), or transform input before validation with [Call](validators/Call.md).
132+
- **Special cases**: Handle dynamic rules with [Lazy](validators/Lazy.md), short-circuit on first failure with [ShortCircuit](validators/ShortCircuit.md), or transform input before validation with [Call](validators/Call.md).
133133

134134
## Customizing error messages
135135

docs/migrating-from-v2-to-v3.md

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,10 @@ composer require ramsey/uuid
373373

374374
In 3.0, `Min` and `Max` validators exist but have different semantics. They extract the minimum/maximum value from a collection and validate it (see [Result composition](#result-composition)).
375375

376-
377-
| Validator | 2.x | 3.x |
378-
| ---------- | --------------- | --------------------------------------------- |
379-
| `Min` | Single value >= | Pick minimum value from iterable and validate |
380-
| `Max` | Single value <= | Pick minimum value from iterable and validate |
376+
| Validator | 2.x | 3.x |
377+
| --------- | --------------- | --------------------------------------------- |
378+
| `Min` | Single value >= | Pick minimum value from iterable and validate |
379+
| `Max` | Single value <= | Pick minimum value from iterable and validate |
381380

382381
##### `NotBlank` logic inverted
383382

@@ -572,9 +571,9 @@ Version 3.0 introduces several new validators:
572571
| `All` | Validates that every item in an iterable passes validation |
573572
| `Attributes` | Validates object properties using PHP attributes |
574573
| `BetweenExclusive` | Validates that a value is between two bounds (exclusive) |
575-
| `Circuit` | Short-circuit validation, stops at first failure |
576574
| `ContainsCount` | Validates the count of occurrences in a value |
577575
| `DateTimeDiff` | Validates date/time differences (replaces Age validators) |
576+
| `ShortCircuit` | Stops at first failure instead of collecting all errors |
578577
| `Hetu` | Validates Finnish personal identity codes (henkilötunnus) |
579578
| `KeyExists` | Checks if an array key exists |
580579
| `KeyOptional` | Validates an array key only if it exists |
@@ -630,26 +629,6 @@ v::betweenExclusive(1, 10)->assert(1); // fails (1 is not > 1)
630629
v::betweenExclusive(1, 10)->assert(10); // fails (10 is not < 10)
631630
```
632631

633-
#### Circuit
634-
635-
Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
636-
637-
```php
638-
$validator = v::circuit(
639-
v::key('countryCode', v::countryCode()),
640-
v::lazy(
641-
fn($input) => v::key(
642-
'subdivisionCode',
643-
v::subdivisionCode($input['countryCode'])
644-
)
645-
),
646-
);
647-
648-
$validator->assert([]); // → `.countryCode` must be present
649-
$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present
650-
$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes
651-
```
652-
653632
#### ContainsCount
654633

655634
Validates the count of occurrences of a value:
@@ -668,6 +647,26 @@ v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert('2000-01-01'); // pa
668647
v::dateTimeDiff('days', v::lessThan(30))->assert('2024-01-15'); // passes if less than 30 days ago
669648
```
670649

650+
#### ShortCircuit
651+
652+
Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
653+
654+
```php
655+
$validator = v::shortCircuit(
656+
v::key('countryCode', v::countryCode()),
657+
v::lazy(
658+
fn($input) => v::key(
659+
'subdivisionCode',
660+
v::subdivisionCode($input['countryCode'])
661+
)
662+
),
663+
);
664+
665+
$validator->assert([]); // → `.countryCode` must be present
666+
$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present
667+
$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes
668+
```
669+
671670
#### Hetu
672671

673672
Validates Finnish personal identity codes (henkilötunnus):

docs/validators.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ In this page you will find a list of validators by their category.
1717

1818
**Comparisons**: [All][] - [Between][] - [BetweenExclusive][] - [Equals][] - [Equivalent][] - [GreaterThan][] - [GreaterThanOrEqual][] - [Identical][] - [In][] - [Length][] - [LessThan][] - [LessThanOrEqual][] - [Max][] - [Min][]
1919

20-
**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [NoneOf][] - [OneOf][]
20+
**Composite**: [AllOf][] - [AnyOf][] - [NoneOf][] - [OneOf][] - [ShortCircuit][]
2121

22-
**Conditions**: [Circuit][] - [Not][] - [When][]
22+
**Conditions**: [Not][] - [ShortCircuit][] - [When][]
2323

2424
**Core**: [Named][] - [Not][] - [Templated][]
2525

@@ -41,7 +41,7 @@ In this page you will find a list of validators by their category.
4141

4242
**Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][]
4343

44-
**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Circuit][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][]
44+
**Nesting**: [AllOf][] - [AnyOf][] - [Call][] - [Each][] - [Key][] - [KeySet][] - [Lazy][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [ShortCircuit][] - [UndefOr][] - [When][]
4545

4646
**Numbers**: [Base][] - [Decimal][] - [Digit][] - [Even][] - [Factor][] - [Finite][] - [FloatType][] - [FloatVal][] - [Infinite][] - [IntType][] - [IntVal][] - [Multiple][] - [Negative][] - [Number][] - [NumericVal][] - [Odd][] - [Positive][] - [Roman][]
4747

@@ -79,7 +79,6 @@ In this page you will find a list of validators by their category.
7979
- [CallableType][] - `v::callableType()->assert(function () {});`
8080
- [Callback][] - `v::callback(fn (int $input): bool => $input % 5 === 0,)->assert(10);`
8181
- [Charset][] - `v::charset('ASCII')->assert('sugar');`
82-
- [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);`
8382
- [Cnh][] - `v::cnh()->assert('02650306461');`
8483
- [Cnpj][] - `v::cnpj()->assert('00394460005887');`
8584
- [Consonant][] - `v::consonant()->assert('xkcd');`
@@ -186,6 +185,7 @@ In this page you will find a list of validators by their category.
186185
- [ResourceType][] - `v::resourceType()->assert(fopen('/path/to/file.txt', 'r'));`
187186
- [Roman][] - `v::roman()->assert('IV');`
188187
- [ScalarVal][] - `v::scalarVal()->assert(135.0);`
188+
- [ShortCircuit][] - `v::shortCircuit(v::intVal(), v::positive())->assert(15);`
189189
- [Size][] - `v::size('KB', v::greaterThan(1))->assert('/path/to/file');`
190190
- [Slug][] - `v::slug()->assert('my-wordpress-title');`
191191
- [Sorted][] - `v::sorted('ASC')->assert([1, 2, 3]);`
@@ -235,7 +235,6 @@ In this page you will find a list of validators by their category.
235235
[CallableType]: validators/CallableType.md "Validates whether the pseudo-type of the input is callable."
236236
[Callback]: validators/Callback.md "Validates the input using the return of a given callable."
237237
[Charset]: validators/Charset.md "Validates if a string is in a specific charset."
238-
[Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails."
239238
[Cnh]: validators/Cnh.md "Validates a Brazilian driver's license."
240239
[Cnpj]: validators/Cnpj.md "Validates if the input is a Brazilian National Registry of Legal Entities (CNPJ) number."
241240
[Consonant]: validators/Consonant.md "Validates if the input contains only consonants."
@@ -342,6 +341,7 @@ In this page you will find a list of validators by their category.
342341
[ResourceType]: validators/ResourceType.md "Validates whether the input is a resource."
343342
[Roman]: validators/Roman.md "Validates if the input is a Roman numeral."
344343
[ScalarVal]: validators/ScalarVal.md "Validates whether the input is a scalar value or not."
344+
[ShortCircuit]: validators/ShortCircuit.md "Validates the input against a series of validators, stopping at the first failure."
345345
[Size]: validators/Size.md "Validates whether the input is a file that is of a certain size or not."
346346
[Slug]: validators/Slug.md "Validates whether the input is a valid slug."
347347
[Sorted]: validators/Sorted.md "Validates whether the input is sorted in a certain order or not."

docs/validators/AllOf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Used when all validators have failed.
5656
## See Also
5757

5858
- [AnyOf](AnyOf.md)
59-
- [Circuit](Circuit.md)
6059
- [NoneOf](NoneOf.md)
6160
- [OneOf](OneOf.md)
61+
- [ShortCircuit](ShortCircuit.md)
6262
- [When](When.md)

docs/validators/AnyOf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ so `AnyOf()` returns true.
5050
## See Also
5151

5252
- [AllOf](AllOf.md)
53-
- [Circuit](Circuit.md)
5453
- [ContainsAny](ContainsAny.md)
5554
- [NoneOf](NoneOf.md)
5655
- [OneOf](OneOf.md)
56+
- [ShortCircuit](ShortCircuit.md)
5757
- [When](When.md)

docs/validators/Call.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,17 @@ v::call(
5353
```
5454

5555
Call does not handle possible errors (type mismatches). If you need to
56-
ensure that your callback is of a certain type, use [Circuit](Circuit.md) or
56+
ensure that your callback is of a certain type, use [ShortCircuit](ShortCircuit.md) or
5757
handle it using a closure:
5858

5959
```php
6060
v::call('strtolower', v::equals('ABC'))->assert(123);
6161
// 𝙭 strtolower(): Argument #1 ($string) must be of type string, int given
6262

63-
v::circuit(v::stringType(), v::call('strtolower', v::equals('abc')))->assert(123);
63+
v::shortCircuit(v::stringType(), v::call('strtolower', v::equals('abc')))->assert(123);
6464
// → 123 must be a string
6565

66-
v::circuit(v::stringType(), v::call('strtolower', v::equals('abc')))->assert('ABC');
66+
v::shortCircuit(v::stringType(), v::call('strtolower', v::equals('abc')))->assert('ABC');
6767
// Validation passes successfully
6868
```
6969

docs/validators/Circuit.md

Lines changed: 0 additions & 70 deletions
This file was deleted.

docs/validators/Lazy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ on the input itself (`$_POST`), but it will use any input that’s given to the
5858

5959
- [Call](Call.md)
6060
- [CallableType](CallableType.md)
61-
- [Circuit](Circuit.md)
61+
- [ShortCircuit](ShortCircuit.md)

docs/validators/NoneOf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Used when all validators have passed.
5959

6060
- [AllOf](AllOf.md)
6161
- [AnyOf](AnyOf.md)
62-
- [Circuit](Circuit.md)
6362
- [Not](Not.md)
6463
- [OneOf](OneOf.md)
64+
- [ShortCircuit](ShortCircuit.md)
6565
- [When](When.md)

docs/validators/OneOf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ Used when more than one validator has passed.
7373

7474
- [AllOf](AllOf.md)
7575
- [AnyOf](AnyOf.md)
76-
- [Circuit](Circuit.md)
7776
- [NoneOf](NoneOf.md)
77+
- [ShortCircuit](ShortCircuit.md)
7878
- [When](When.md)

0 commit comments

Comments
 (0)