respect-validation/docs/feature-guide.md
Henrique Moody b701fac656
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)
2026-02-05 17:32:42 +01:00

331 lines
12 KiB
Markdown

<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: (c) Respect Project Contributors
SPDX-FileContributor: Antonio <info@antoniodelucas.es>
SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
-->
# Feature Guide
The `ValidatorBuilder` class is the core of Respect\Validation, offering a fluent interface for building validators.
For convenience, the `ValidatorBuilder` class is aliased as `v`. This means you can write `v::intType()` instead of `\Respect\Validation\ValidatorBuilder::intType()`.
## Validation methods
### Validating using booleans
With the `isValid()` method, determine if your input meets a specific validator.
```php
if (!v::intType()->positive()->isValid($input)) {
echo 'The input you gave me is not a positive integer';
}
```
Note that you can combine multiple validators for a complex validation.
### Validating using exceptions
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.
```php
v::intType()->positive()->assert($input);
```
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.
```php
v::intType()->positive()->check($input);
```
The difference is visible when multiple validators fail. With `assert()`, you get all error messages; with `check()`, you get only the first one.
### Validating using results
You can validate data and handle the result manually without using exceptions:
```php
$result = v::numericVal()->positive()->between(1, 255)->validate($input);
if ($result->hasFailed()) {
echo $result;
}
```
The `validate()` method returns a `ResultQuery` object that has the following methods to output messages:
- `getMessage()`: Returns the first message from the deepest failed result in chain.
- `getFullMessage()`: Returns the full message including all failed results.
- `getMessages()`: Returns an array of all messages from failed results.
## Smart validation
Respect\Validation offers over 150 validators, many of which are designed to address common scenarios.
### PHP attributes
PHP attributes are supported, allowing you to use any validator as an attribute:
```php
use Respect\Validation\Validators as Validator;
use Respect\Validation\Rules\GreaterThan;
final readonly class User
{
public function __construct(
#[Validator\Email]
public string $email,
#[Validator\Between(18, 120)]
public int $age,
#[Validator\Length(new GreaterThan(1))]
public string $name,
) {
}
}
// Validate everything at once
v::attributes()->assert($user);
```
### Result composition
Validators can wrap others and combine their results into a single, coherent message. The outer validator provides context (what was extracted), and the inner validator provides the validation (what was expected).
```php
v::all(v::intType())->assert(['1', '2', '3']);
// → Every item in `["1", "2", "3"]` must be an integer
v::length(v::greaterThan(3))->assert('abc');
// → The length of "abc" must be greater than 3
v::min(v::positive())->assert([3, 1, 0, -5]);
// → The minimum of `[3, 1, 0, -5]` must be a positive number
v::max(v::positive())->assert([-1, -2, -3]);
// → The maximum of `[-1, -2, -3]` must be a positive number
v::size('MB', v::not(v::greaterThan(5)))->assert('path/to/file.zip');
// → The size in megabytes of "path/to/file.zip" must not be greater than 5
v::dateTimeDiff('years', v::greaterThan(18))->assert('2025');
// → The number of years between now and "2025" must be greater than 18
```
### Prefixed shortcuts
For convenience, several prefixed shortcuts are available for validators that wrap other validators:
```php
v::allEmoji()->assert($input); // all items must be emojis
v::keyEmail('email')->assert($input); // key 'email' must be valid email
v::propertyPositive('age')->assert($input); // property 'age' must be positive
v::lengthBetween(5, 10)->assert($input); // length between 5 and 10
v::maxLessThan(100)->assert($input); // max value less than 100
v::minGreaterThan(0)->assert($input); // min value greater than 0
v::nullOrEmail()->assert($input); // null or valid email
v::undefOrPositive()->assert($input); // undefined or positive number
```
See [Prefixes](prefixes.md) for more information.
### Other validation types
Beyond the examples above, Respect\Validation provides specialized validators for common patterns:
- **Arrays**: Access and validate array keys with [Key](validators/Key.md), [KeyOptional](validators/KeyOptional.md), [KeyExists](validators/KeyExists.md).
- **Array structures**: Enforce exact key schemas with [KeySet](validators/KeySet.md).
- **Object properties**: Validate object state with [Property](validators/Property.md), [PropertyOptional](validators/PropertyOptional.md), [PropertyExists](validators/PropertyExists.md).
- **Conditional validation**: Handle nullable or optional values with [NullOr](validators/NullOr.md), [UndefOr](validators/UndefOr.md), [When](validators/When.md).
- **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), selectively short-circuit on first failure with [ShortCircuit](validators/ShortCircuit.md), or transform input before validation with [After](validators/After.md).
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.
## Customizing error messages
Respect\Validation provides several ways to customize error messages to better fit your application's needs.
### Using custom templates
Define your own error message when the validation fails:
```php
v::between(1, 256)->assert($input, '{{subject}} is not what I was expecting');
```
### Custom templates per validator
Provide unique messages for each validator in a chain:
```php
v::alnum()->lowercase()->assert($input, [
'alnum' => 'Your username must consist only of letters and digits',
'lowercase' => 'Your username must be lowercase',
]);
```
### Custom templates for nested structures
When using validators that handle structures (like `Key` and `Property`), you can define the template by the path of the validator:
```php
// Target nested structures by path
v::key('name', v::stringType())
->key('age', v::intVal())
->assert($input, [
'__root__' => 'Please check your user data',
'name' => 'Please provide a valid name',
'age' => 'Age must be a number',
]);
```
The `__root__` key targets the root validator. In this case, that's an `AllOf` that wraps the chain.
### Attaching templates to the chain
For reusable templates, you can attach them directly to the chain using the `Templated` validator:
```php
v::templated('This is my template', v::email())->assert($input);
// → This is my template
```
The `Templated` validator also allows you to pass parameters to your template, so you can inject your own placeholders.
### Placeholder pipes
Placeholder pipes allow you to customize how values are rendered in error message templates by adding a pipe (`|`) followed by a modifier name to your placeholder.
```php
v::templated(
'The {{field|raw}} field is required',
v::notEmpty(),
['field' => 'email'],
)->assert('');
// → The email field is required
// (instead of: The "email" field is required)
```
For detailed information on all available placeholder pipes, see the [Placeholder Pipes documentation](messages/placeholder-pipes.md).
## Using your own exceptions
### Exception objects
Integrate your own exception objects when the validation fails:
```php
v::alnum()->assert($input, new DomainException('Not a valid username'));
```
### Exception objects via callable
Provide a callable that creates an exception object to be used when the validation fails:
```php
use Respect\Validation\ValidatorBuilder as v;
use Respect\Validation\Exceptions\ValidationException;
v::alnum()->lowercase()->assert(
$input,
fn(ValidationException $exception) => new DomainException('Username: '. $exception->getMessage())
);
```
## Naming validators
Template messages include the placeholder `{{subject}}`, which defaults to the input. Use the `named()` validator to replace it with a more descriptive label:
```php
v::named('Your email', v::email())->assert($input);
// → Your email must be a valid email
```
## Reusing validators
The `ValidatorBuilder` is immutable. Every time you add a new validator to the chain, you get a new instance while the original remains unchanged. This makes validators safe to create once and reuse:
```php
$validator = v::alnum()->lowercase();
$validator->assert('respect');
$validator->assert('validation');
$validator->assert('alexandre gaigalas');
```
Every time you add a new node to the chain, a new immutable instance is created. That means you can do things like:
```php
$baseValidator = v::intType()->between(1, 155);
$baseValidator->even()->assert($input1);
$baseValidator->odd()->assert($input2);
```
Both the `even()` and `odd()` calls created a new instance, which means that the `$baseValidator` remained unchanged.
## Advanced features
### Inverting validators
Use the `not` prefix to invert a validator:
```php
v::notEquals('main')->assert($input);
```
### Nested validation paths
Validation can trace the path of nested structures and display helpful messages. Use `v::init()` to start a chain without an implicit `AllOf` wrapper—this is useful when you want full control over how validators are grouped.
```php
$validator = v::init()
->key(
'mysql',
v::init()
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType()),
)
->key(
'postgresql',
v::init()
->key('host', v::stringType())
->key('user', v::stringType())
->key('password', v::stringType())
->key('schema', v::stringType()),
)
->assert($input);
// → `.mysql.host` must be a string
```
Not only do you have the full path of the nested structure, but it's also clear that `.mysql.host` is a path, not a name.
The `ResultQuery` also has methods to query nested results:
```php
$result = $validator->validate($input);
$mysqlUserResult = $result->findByPath('mysql.user');
if ($mysqlUserResult !== null) {
echo $mysqlUserResult;
}
```
The `findByPath()` returns either a `ResultQuery` or `null`, and you can also use `findByName()` and `findById()`.
### Helpful stack traces
When an exception is thrown, the stack trace points to your code, not library internals:
```text
Respect\Validation\Exceptions\ValidationException: "string" must be an integer in /opt/examples/file.php:11
Stack trace:
#0 /opt/examples/file.php(11): Respect\Validation\Validator->assert(1.0)
#1 {main}
```
Your file. Your line. Your problem to fix — not ours to hide.