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

12 KiB

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.

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.

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.

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:

$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:

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

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:

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 for more information.

Other validation types

Beyond the examples above, Respect\Validation provides specialized validators for common patterns:

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:

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:

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:

// 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:

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.

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.

Using your own exceptions

Exception objects

Integrate your own exception objects when the validation fails:

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:

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:

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:

$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:

$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:

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.

$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:

$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:

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.