Skip to content

Commit b701fac

Browse files
Create ShortCircuit validator and ShortCircuitable 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. 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 e636b63 commit b701fac

62 files changed

Lines changed: 1445 additions & 226 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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,20 @@ Note that you can combine multiple validators for a complex validation.
2727

2828
### Validating using exceptions
2929

30-
The `assert()` method throws an exception when validation fails. You can handle those exceptions with `try/catch` for more robust error handling.
30+
The `assert()` method throws an exception when validation fails. It evaluates all validators in the chain and collects every error before throwing. You can handle those exceptions with `try/catch` for more robust error handling.
3131

3232
```php
3333
v::intType()->positive()->assert($input);
3434
```
3535

36+
The `check()` method also throws an exception when validation fails, but it stops at the first failure instead of collecting all errors. Internally, it wraps the chain in a `ShortCircuit` validator.
37+
38+
```php
39+
v::intType()->positive()->check($input);
40+
```
41+
42+
The difference is visible when multiple validators fail. With `assert()`, you get all error messages; with `check()`, you get only the first one.
43+
3644
### Validating using results
3745

3846
You can validate data and handle the result manually without using exceptions:
@@ -131,7 +139,9 @@ Beyond the examples above, Respect\Validation provides specialized validators fo
131139
- **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).
132140
- **Iteration**: Validate every item in a collection with [Each](validators/Each.md).
133141
- **Length, Min, Max**: Validate derived values with [Length](validators/Length.md), [Min](validators/Min.md), [Max](validators/Max.md).
134-
- **Special cases**: Handle dynamic rules with [Factory](validators/Factory.md), short-circuit on first failure with [Circuit](validators/Circuit.md), or transform input before validation with [After](validators/After.md).
142+
- **Special cases**: Handle dynamic rules with [Factory](validators/Factory.md), selectively short-circuit on first failure with [ShortCircuit](validators/ShortCircuit.md), or transform input before validation with [After](validators/After.md).
143+
144+
Note: While `check()` automatically short-circuits the entire chain, the `ShortCircuit` validator gives you fine-grained control over which specific group of validators should stop at the first failure. Use `check()` when you want the whole chain to fail fast, and `ShortCircuit` when you want only a specific part of your validation to fail fast while the rest continues collecting errors.
135145

136146
## Customizing error messages
137147

docs/handling-exceptions.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
77

88
# Handling exceptions
99

10-
The `ValidatorBuilder::assert()` method throws a `ValidationException` when validation fails. This exception provides detailed feedback on what went wrong.
10+
Both `ValidatorBuilder::assert()` and `ValidatorBuilder::check()` throw a `ValidationException` when validation fails. This exception provides detailed feedback on what went wrong.
11+
12+
The difference between the two methods is that `assert()` evaluates all validators in the chain and collects every error, while `check()` stops at the first failure (using `ShortCircuit` internally).
1113

1214
## The `ValidationException`
1315

@@ -21,6 +23,16 @@ try {
2123
}
2224
```
2325

26+
The same applies to `check()`:
27+
28+
```php
29+
try {
30+
v::alnum()->lowercase()->check($input);
31+
} catch (InvalidArgumentException $exception) {
32+
echo $exception->getMessage(); // Only the first failure
33+
}
34+
```
35+
2436
### Helpful stack traces
2537

2638
When an exception is thrown, PHP reports where it was *created*, not where it was *caused*. In most validation libraries that means stack traces point deep inside library internals. You end up hunting through the trace to find your actual code.

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

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,24 @@ In 2.x, `validate()` returned a boolean. In 3.0, it returns a `ResultQuery` obje
6969
}
7070
```
7171

72-
#### `check()` removed, `assert()` unified
72+
#### `check()` and `assert()` behavior changes
7373

74-
In 2.x, there were two exception-based methods:
74+
In 2.x, both `check()` and `assert()` threw validator-specific exception classes (e.g., `IntTypeException`), which were children of `ValidationException`. The difference was that `check()` threw these exceptions directly and stopped at the first failure, while `assert()` threw a `NestedValidationException` (also a child of `ValidationException`) that collected all errors and provided methods like `getFullMessage()` and `getMessages()` not available on the base `ValidationException`.
7575

76-
- `check()` threw rule-specific exceptions (e.g., `IntTypeException`)
77-
- `assert()` threw `NestedValidationException`
78-
79-
In 3.0, both are unified into `assert()`, which throws `ValidationException`:
76+
In 3.0, validator-specific exception classes and `NestedValidationException` no longer exist. Both `check()` and `assert()` throw a unified `ValidationException` that includes `getMessage()`, `getFullMessage()`, and `getMessages()`. The behavioral distinction is preserved: `check()` still fails fast (stopping at the first failure, using `ShortCircuit` internally), while `assert()` collects all errors.
8077

8178
```diff
8279
-use Respect\Validation\Exceptions\IntTypeException;
8380
+use Respect\Validation\Exceptions\ValidationException;
8481

8582
try {
86-
- v::intType()->check($input);
83+
v::intType()->check($input);
8784
-} catch (IntTypeException $exception) {
88-
+ v::intType()->assert($input);
8985
+} catch (ValidationException $exception) {
9086
echo $exception->getMessage();
9187
}
9288
```
9389

94-
The `ValidationException` provides all methods previously split between exceptions:
95-
9690
```diff
9791
-use Respect\Validation\Exceptions\NestedValidationException;
9892
+use Respect\Validation\Exceptions\ValidationException;
@@ -589,9 +583,9 @@ Version 3.0 introduces several new validators:
589583
| `All` | Validates that every item in an iterable passes validation |
590584
| `Attributes` | Validates object properties using PHP attributes |
591585
| `BetweenExclusive` | Validates that a value is between two bounds (exclusive) |
592-
| `Circuit` | Short-circuit validation, stops at first failure |
593586
| `ContainsCount` | Validates the count of occurrences in a value |
594587
| `DateTimeDiff` | Validates date/time differences (replaces Age validators) |
588+
| `ShortCircuit` | Stops at first failure instead of collecting all errors |
595589
| `Hetu` | Validates Finnish personal identity codes (henkilötunnus) |
596590
| `KeyExists` | Checks if an array key exists |
597591
| `KeyOptional` | Validates an array key only if it exists |
@@ -647,26 +641,6 @@ v::betweenExclusive(1, 10)->assert(1); // fails (1 is not > 1)
647641
v::betweenExclusive(1, 10)->assert(10); // fails (10 is not < 10)
648642
```
649643

650-
#### Circuit
651-
652-
Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
653-
654-
```php
655-
$validator = v::circuit(
656-
v::key('countryCode', v::countryCode()),
657-
v::factory(
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-
670644
#### ContainsCount
671645

672646
Validates the count of occurrences of a value:
@@ -685,6 +659,26 @@ v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert('2000-01-01'); // pa
685659
v::dateTimeDiff('days', v::lessThan(30))->assert('2024-01-15'); // passes if less than 30 days ago
686660
```
687661

662+
#### ShortCircuit
663+
664+
Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
665+
666+
```php
667+
$validator = v::shortCircuit(
668+
v::key('countryCode', v::countryCode()),
669+
v::factory(
670+
fn($input) => v::key(
671+
'subdivisionCode',
672+
v::subdivisionCode($input['countryCode'])
673+
)
674+
),
675+
);
676+
677+
$validator->assert([]); // → `.countryCode` must be present
678+
$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present
679+
$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes
680+
```
681+
688682
#### Hetu
689683

690684
Validates Finnish personal identity codes (henkilötunnus):
@@ -983,11 +977,12 @@ v::templated(
983977
- v::intType()->validate($input); // bool
984978
+ v::intType()->isValid($input); // bool
985979

986-
// Exception-based validation
987-
- v::intType()->check($input); // IntTypeException
988-
+ v::intType()->assert($input); // ValidationException
980+
// Exception-based validation (fail-fast)
981+
- v::intType()->check($input); // IntTypeException (child of ValidationException)
982+
+ v::intType()->check($input); // ValidationException
989983

990-
- v::intType()->assert($input); // NestedValidationException
984+
// Exception-based validation (collect all errors)
985+
- v::intType()->assert($input); // AllOfExceptopn (child of NestedValidationException)
991986
+ v::intType()->assert($input); // ValidationException
992987

993988
// Renamed validators

docs/validators.md

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

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

22-
**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [NoneOf][] - [OneOf][]
22+
**Composite**: [AllOf][] - [AnyOf][] - [NoneOf][] - [OneOf][] - [ShortCircuit][]
2323

24-
**Conditions**: [Circuit][] - [Not][] - [When][]
24+
**Conditions**: [Not][] - [ShortCircuit][] - [When][]
2525

2626
**Core**: [Named][] - [Not][] - [Templated][]
2727

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

4444
**Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][]
4545

46-
**Nesting**: [After][] - [AllOf][] - [AnyOf][] - [Circuit][] - [Each][] - [Factory][] - [Key][] - [KeySet][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][]
46+
**Nesting**: [After][] - [AllOf][] - [AnyOf][] - [Each][] - [Factory][] - [Key][] - [KeySet][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [ShortCircuit][] - [UndefOr][] - [When][]
4747

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

@@ -80,7 +80,6 @@ In this page you will find a list of validators by their category.
8080
- [Bsn][] - `v::bsn()->assert('612890053');`
8181
- [CallableType][] - `v::callableType()->assert(function () {});`
8282
- [Charset][] - `v::charset('ASCII')->assert('sugar');`
83-
- [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);`
8483
- [Cnh][] - `v::cnh()->assert('02650306461');`
8584
- [Cnpj][] - `v::cnpj()->assert('00394460005887');`
8685
- [Consonant][] - `v::consonant()->assert('xkcd');`
@@ -189,6 +188,7 @@ In this page you will find a list of validators by their category.
189188
- [Roman][] - `v::roman()->assert('IV');`
190189
- [Satisfies][] - `v::satisfies(fn (int $input): bool => $input % 5 === 0,)->assert(10);`
191190
- [ScalarVal][] - `v::scalarVal()->assert(135.0);`
191+
- [ShortCircuit][] - `v::shortCircuit(v::intVal(), v::positive())->assert(15);`
192192
- [Size][] - `v::size('KB', v::greaterThan(1))->assert('/path/to/file');`
193193
- [Slug][] - `v::slug()->assert('my-wordpress-title');`
194194
- [Sorted][] - `v::sorted('ASC')->assert([1, 2, 3]);`
@@ -237,7 +237,6 @@ In this page you will find a list of validators by their category.
237237
[Bsn]: validators/Bsn.md "Validates a Dutch citizen service number (BSN)."
238238
[CallableType]: validators/CallableType.md "Validates whether the pseudo-type of the input is callable."
239239
[Charset]: validators/Charset.md "Validates if a string is in a specific charset."
240-
[Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails."
241240
[Cnh]: validators/Cnh.md "Validates a Brazilian driver's license."
242241
[Cnpj]: validators/Cnpj.md "Validates the structure and mathematical integrity of Brazilian CNPJ identifiers."
243242
[Consonant]: validators/Consonant.md "Validates if the input contains only consonants."
@@ -346,6 +345,7 @@ In this page you will find a list of validators by their category.
346345
[Roman]: validators/Roman.md "Validates if the input is a Roman numeral."
347346
[Satisfies]: validators/Satisfies.md "Validates the input using the return of a given callable."
348347
[ScalarVal]: validators/ScalarVal.md "Validates whether the input is a scalar value or not."
348+
[ShortCircuit]: validators/ShortCircuit.md "Validates the input against a series of validators, stopping at the first failure."
349349
[Size]: validators/Size.md "Validates whether the input is a file that is of a certain size or not."
350350
[Slug]: validators/Slug.md "Validates whether the input is a valid slug."
351351
[Sorted]: validators/Sorted.md "Validates whether the input is sorted in a certain order or not."

docs/validators/After.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,17 @@ v::after(
5555
```
5656

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

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

65-
v::circuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert(123);
65+
v::shortCircuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert(123);
6666
// → 123 must be a string
6767

68-
v::circuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert('ABC');
68+
v::shortCircuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert('ABC');
6969
// Validation passes successfully
7070
```
7171

docs/validators/AllOf.md

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

6060
- [AnyOf](AnyOf.md)
61-
- [Circuit](Circuit.md)
6261
- [NoneOf](NoneOf.md)
6362
- [OneOf](OneOf.md)
63+
- [ShortCircuit](ShortCircuit.md)
6464
- [When](When.md)

docs/validators/AnyOf.md

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

5555
- [AllOf](AllOf.md)
56-
- [Circuit](Circuit.md)
5756
- [ContainsAny](ContainsAny.md)
5857
- [NoneOf](NoneOf.md)
5958
- [OneOf](OneOf.md)
59+
- [ShortCircuit](ShortCircuit.md)
6060
- [When](When.md)

docs/validators/Circuit.md

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

docs/validators/Factory.md

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

6161
- [After](After.md)
6262
- [CallableType](CallableType.md)
63-
- [Circuit](Circuit.md)
63+
- [ShortCircuit](ShortCircuit.md)

docs/validators/NoneOf.md

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

6363
- [AllOf](AllOf.md)
6464
- [AnyOf](AnyOf.md)
65-
- [Circuit](Circuit.md)
6665
- [Not](Not.md)
6766
- [OneOf](OneOf.md)
67+
- [ShortCircuit](ShortCircuit.md)
6868
- [When](When.md)

0 commit comments

Comments
 (0)