Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/feature-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Beyond the examples above, Respect\Validation provides specialized validators fo
- **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).
- **Iteration**: Validate every item in a collection with [Each](validators/Each.md).
- **Length, Min, Max**: Validate derived values with [Length](validators/Length.md), [Min](validators/Min.md), [Max](validators/Max.md).
- **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).
- **Special cases**: Handle dynamic rules with [Factory](validators/Factory.md), short-circuit on first failure with [ShortCircuit](validators/ShortCircuit.md), or transform input before validation with [After](validators/After.md).

## Customizing error messages

Expand Down
42 changes: 21 additions & 21 deletions docs/migrating-from-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,9 @@ Version 3.0 introduces several new validators:
| `All` | Validates that every item in an iterable passes validation |
| `Attributes` | Validates object properties using PHP attributes |
| `BetweenExclusive` | Validates that a value is between two bounds (exclusive) |
| `Circuit` | Short-circuit validation, stops at first failure |
| `ContainsCount` | Validates the count of occurrences in a value |
| `DateTimeDiff` | Validates date/time differences (replaces Age validators) |
| `ShortCircuit` | Stops at first failure instead of collecting all errors |
| `Hetu` | Validates Finnish personal identity codes (henkilötunnus) |
| `KeyExists` | Checks if an array key exists |
| `KeyOptional` | Validates an array key only if it exists |
Expand Down Expand Up @@ -645,26 +645,6 @@ v::betweenExclusive(1, 10)->assert(1); // fails (1 is not > 1)
v::betweenExclusive(1, 10)->assert(10); // fails (10 is not < 10)
```

#### Circuit

Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:

```php
$validator = v::circuit(
v::key('countryCode', v::countryCode()),
v::factory(
fn($input) => v::key(
'subdivisionCode',
v::subdivisionCode($input['countryCode'])
)
),
);

$validator->assert([]); // → `.countryCode` must be present
$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present
$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes
```

#### ContainsCount

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

#### ShortCircuit

Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:

```php
$validator = v::shortCircuit(
v::key('countryCode', v::countryCode()),
v::lazy(
fn($input) => v::key(
'subdivisionCode',
v::subdivisionCode($input['countryCode'])
)
),
);

$validator->assert([]); // → `.countryCode` must be present
$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present
$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes
```

#### Hetu

Validates Finnish personal identity codes (henkilötunnus):
Expand Down
10 changes: 5 additions & 5 deletions docs/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ In this page you will find a list of validators by their category.

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

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

**Conditions**: [Circuit][] - [Not][] - [When][]
**Conditions**: [Not][] - [ShortCircuit][] - [When][]

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

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

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

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

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

Expand Down Expand Up @@ -78,7 +78,6 @@ In this page you will find a list of validators by their category.
- [Bsn][] - `v::bsn()->assert('612890053');`
- [CallableType][] - `v::callableType()->assert(function () {});`
- [Charset][] - `v::charset('ASCII')->assert('sugar');`
- [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);`
- [Cnh][] - `v::cnh()->assert('02650306461');`
- [Cnpj][] - `v::cnpj()->assert('00394460005887');`
- [Consonant][] - `v::consonant()->assert('xkcd');`
Expand Down Expand Up @@ -186,6 +185,7 @@ In this page you will find a list of validators by their category.
- [Roman][] - `v::roman()->assert('IV');`
- [Satisfies][] - `v::satisfies(fn (int $input): bool => $input % 5 === 0,)->assert(10);`
- [ScalarVal][] - `v::scalarVal()->assert(135.0);`
- [ShortCircuit][] - `v::shortCircuit(v::intVal(), v::positive())->assert(15);`
- [Size][] - `v::size('KB', v::greaterThan(1))->assert('/path/to/file');`
- [Slug][] - `v::slug()->assert('my-wordpress-title');`
- [Sorted][] - `v::sorted('ASC')->assert([1, 2, 3]);`
Expand Down Expand Up @@ -234,7 +234,6 @@ In this page you will find a list of validators by their category.
[Bsn]: validators/Bsn.md "Validates a Dutch citizen service number (BSN)."
[CallableType]: validators/CallableType.md "Validates whether the pseudo-type of the input is callable."
[Charset]: validators/Charset.md "Validates if a string is in a specific charset."
[Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails."
[Cnh]: validators/Cnh.md "Validates a Brazilian driver's license."
[Cnpj]: validators/Cnpj.md "Validates if the input is a Brazilian National Registry of Legal Entities (CNPJ) number."
[Consonant]: validators/Consonant.md "Validates if the input contains only consonants."
Expand Down Expand Up @@ -342,6 +341,7 @@ In this page you will find a list of validators by their category.
[Roman]: validators/Roman.md "Validates if the input is a Roman numeral."
[Satisfies]: validators/Satisfies.md "Validates the input using the return of a given callable."
[ScalarVal]: validators/ScalarVal.md "Validates whether the input is a scalar value or not."
[ShortCircuit]: validators/ShortCircuit.md "Validates the input against a series of validators, stopping at the first failure."
[Size]: validators/Size.md "Validates whether the input is a file that is of a certain size or not."
[Slug]: validators/Slug.md "Validates whether the input is a valid slug."
[Sorted]: validators/Sorted.md "Validates whether the input is sorted in a certain order or not."
Expand Down
6 changes: 3 additions & 3 deletions docs/validators/After.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ v::after(
```

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

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

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

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

Expand Down
2 changes: 1 addition & 1 deletion docs/validators/AllOf.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Used when all validators have failed.
## See Also

- [AnyOf](AnyOf.md)
- [Circuit](Circuit.md)
- [NoneOf](NoneOf.md)
- [OneOf](OneOf.md)
- [ShortCircuit](ShortCircuit.md)
- [When](When.md)
2 changes: 1 addition & 1 deletion docs/validators/AnyOf.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ so `AnyOf()` returns true.
## See Also

- [AllOf](AllOf.md)
- [Circuit](Circuit.md)
- [ContainsAny](ContainsAny.md)
- [NoneOf](NoneOf.md)
- [OneOf](OneOf.md)
- [ShortCircuit](ShortCircuit.md)
- [When](When.md)
70 changes: 0 additions & 70 deletions docs/validators/Circuit.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/validators/Factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ on the input itself (`$_POST`), but it will use any input that’s given to the

- [After](After.md)
- [CallableType](CallableType.md)
- [Circuit](Circuit.md)
- [ShortCircuit](ShortCircuit.md)
2 changes: 1 addition & 1 deletion docs/validators/NoneOf.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Used when all validators have passed.

- [AllOf](AllOf.md)
- [AnyOf](AnyOf.md)
- [Circuit](Circuit.md)
- [Not](Not.md)
- [OneOf](OneOf.md)
- [ShortCircuit](ShortCircuit.md)
- [When](When.md)
2 changes: 1 addition & 1 deletion docs/validators/OneOf.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ Used when more than one validator has passed.

- [AllOf](AllOf.md)
- [AnyOf](AnyOf.md)
- [Circuit](Circuit.md)
- [NoneOf](NoneOf.md)
- [ShortCircuit](ShortCircuit.md)
- [When](When.md)
78 changes: 78 additions & 0 deletions docs/validators/ShortCircuit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!--
SPDX-FileCopyrightText: (c) Respect Project Contributors
SPDX-License-Identifier: MIT
-->

# ShortCircuit

- `ShortCircuit()`
- `ShortCircuit(Validator ...$validators)`

Validates the input against a series of validators, stopping at the first failure.

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.
Comment on lines +13 to +15
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.


```php
v::shortCircuit(v::intVal(), v::positive())->assert(15);
// Validation passes successfully
```

This is useful when:

- You want only the first error message instead of all of them
- Later validators depend on earlier ones passing (e.g., checking a format before checking a value)
- You want to avoid unnecessary validation work

This validator is particularly useful in combination with [Factory](Factory.md) when later validations depend on earlier
results. For example, validating a subdivision code that depends on a valid country code:

```php
$validator = v::shortCircuit(
v::key('countryCode', v::countryCode()),
v::factory(static fn($input) => v::key('subdivisionCode', v::subdivisionCode($input['countryCode']))),
);

$validator->assert([]);
// → `.countryCode` must be present

$validator->assert(['countryCode' => 'US']);
// → `.subdivisionCode` must be present

$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'ZZ']);
// → `.subdivisionCode` must be a subdivision code of United States

$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']);
// Validation passes successfully
```

Because [SubdivisionCode](SubdivisionCode.md) requires a valid country code, it only makes sense to validate the
subdivision after the country code passes. You could achieve this with [When](When.md), but you would have to repeat
`v::key('countryCode', v::countryCode())` twice.

## Templates

This validator does not have templates of its own. It returns the result of the first failing validator, or the result
of the last validator when all pass.

## Categorization

- Composite
- Conditions
- Nesting

## Changelog

| Version | Description |
| ------: | :---------- |
| 3.0.0 | Created |

## See Also

- [AllOf](AllOf.md)
- [AnyOf](AnyOf.md)
- [NoneOf](NoneOf.md)
- [OneOf](OneOf.md)
- [SubdivisionCode](SubdivisionCode.md)
- [When](When.md)
2 changes: 1 addition & 1 deletion docs/validators/SubdivisionCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ v::subdivisionCode('US')->assert('CA');

## See Also

- [Circuit](Circuit.md)
- [CountryCode](CountryCode.md)
- [CurrencyCode](CurrencyCode.md)
- [Nip](Nip.md)
- [Pesel](Pesel.md)
- [PolishIdCard](PolishIdCard.md)
- [PublicDomainSuffix](PublicDomainSuffix.md)
- [ShortCircuit](ShortCircuit.md)
- [Tld](Tld.md)

[ISO 3166-1 alpha-2]: http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 "ISO 3166-1 alpha-2"
Expand Down
2 changes: 1 addition & 1 deletion docs/validators/When.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ When `$else` is not defined use [AlwaysInvalid](AlwaysInvalid.md)
- [AllOf](AllOf.md)
- [AlwaysInvalid](AlwaysInvalid.md)
- [AnyOf](AnyOf.md)
- [Circuit](Circuit.md)
- [NoneOf](NoneOf.md)
- [OneOf](OneOf.md)
- [ShortCircuit](ShortCircuit.md)
27 changes: 27 additions & 0 deletions src/Helpers/CanEvaluateShortCircuit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Helpers;

use Respect\Validation\Result;
use Respect\Validation\Validator;
use Respect\Validation\Validators\Core\ShortCircuitable;

trait CanEvaluateShortCircuit
{
private function evaluateShortCircuitWith(Validator $validator, mixed $input): Result
{
if ($validator instanceof ShortCircuitable) {
return $validator->evaluateShortCircuit($input);
}

return $validator->evaluate($input);
}
}
Loading