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)
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:
- Arrays: Access and validate array keys with Key, KeyOptional, KeyExists.
- Array structures: Enforce exact key schemas with KeySet.
- Object properties: Validate object state with Property, PropertyOptional, PropertyExists.
- Conditional validation: Handle nullable or optional values with NullOr, UndefOr, When.
- Grouped validation: Combine validators with AND/OR logic using AllOf, AnyOf, NoneOf, OneOf.
- Iteration: Validate every item in a collection with Each.
- Length, Min, Max: Validate derived values with Length, Min, Max.
- Special cases: Handle dynamic rules with Factory, selectively short-circuit on first failure with ShortCircuit, or transform input before validation with After.
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.