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)
7.3 KiB
Handling exceptions
Both ValidatorBuilder::assert() and ValidatorBuilder::check() throw a ValidationException when validation fails. This exception provides detailed feedback on what went wrong.
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).
The ValidationException
The ValidationException extends PHP's default InvalidArgumentException. That means you can simply catch InvalidArgumentException.
try {
v::alnum()->assert($input);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage();
}
The same applies to check():
try {
v::alnum()->lowercase()->check($input);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage(); // Only the first failure
}
Helpful stack traces
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.
Although the ValidationException is thrown deep inside Validation's internals, we overwrite the stack trace to provide a helpful message. If v::intType()->assert($input) fails in /opt/example.php line 78, your stack trace looks like this:
Stack trace:
#0 /opt/example.php(78): Respect\Validation\Validator->assert(1.0)
#1 {main}
Exception message
The getMessage() method returns the message from the first failed validator.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->lowercase()->assert('The Panda');
} catch (ValidationException $exception) {
echo $exception->getMessage();
}
The code above generates the following output:
"The Panda" must consist only of letters (a-z) and digits (0-9)
Full exception message
The getFullMessage() method will return a full comprehensive explanation of validators that didn't pass in a nested Markdown list format.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->lowercase()->assert('The Panda');
} catch (ValidationException $exception) {
echo $exception->getFullMessage();
}
The code above generates the following output:
- "The Panda" must pass all the rules
- "The Panda" must consist only of letters (a-z) and digits (0-9)
- "The Panda" must consist only of lowercase letters
Full exception messages as an array
Retrieve validation messages in array format using getMessages().
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->lowercase()->assert('The Panda');
} catch (ValidationException $exception) {
print_r($exception->getMessages());
}
The code above generates the following output:
Array
(
[__root__] => "The Panda" must pass all the rules
[alnum] => "The Panda" must consist only of letters (a-z) and digits (0-9)
[lowercase] => "The Panda" must consist only of lowercase letters
)
When validating with Key or Property, the keys will correspond to the name of the key or property that failed the validation.
Custom exception
You can pass a custom exception as the second argument of ValidatorBuilder::assert() in two ways.
Custom exception as object
Pass a Throwable to throw a custom exception instead of ValidationException.
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->assert($input, new DomainValidationException('Validation failed!'));
} catch (DomainValidationException $exception) {
echo $exception->getMessage();
}
Custom exception as callable
Pass a callable to intercept the ValidationException before throwing your own exception.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->assert(
$input,
fn (ValidationException $exception) => new DomainException(
'Validation error: ' . $exception->getMessage(),
$exception->getCode(),
$exception
)
);
} catch (DomainException $exception) {
echo $exception->getMessage();
}
Custom templates
The assert() method accepts different types of templates as the second argument to customize exception messages.
Custom templates as string
Pass a single string template to replace the root message of the exception.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()->assert('The Panda', 'Invalid username provided');
} catch (ValidationException $exception) {
echo $exception->getFullMessage();
}
The code above generates the following output.
- Invalid username provided
Custom templates as array
Pass custom templates as an array to the assert() method for one-off use cases.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::alnum()
->lowercase()
->assert(
'The Panda',
[
'__root__' => 'The given input is not valid',
'alnum' => 'Your username must consist only of letters and digits',
'lowercase' => 'Your username must be lowercase',
]
);
} catch (ValidationException $exception) {
print_r($exception->getMessages());
}
The code above generates the following output.
Array
(
[__root__] => The given input is not valid
[alnum] => Your username must consist only of letters and digits
[lowercase] => Your username must be lowercase
)
Custom templates with Templated validator
Use the Templated validator to attach templates directly to the chain. That way your chain of validators will always have the same template.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::templated('Invalid email address', v::email())
->assert('not an email');
} catch (ValidationException $exception) {
echo $exception->getMessage();
}
The code above generates the following output.
Invalid email address
You can also use Templated with template parameters to create dynamic messages.
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\ValidatorBuilder as v;
try {
v::templated(
'The author of the page {{title}} is empty',
v::notBlank(),
['title' => 'Feature Guide']
)->assert('');
} catch (ValidationException $exception) {
echo $exception->getMessage();
}
The code above generates the following output.
The author of the page "Feature Guide" is empty