This guide will help you migrate your code from Respect\Validation 2.x to 3.0.
Version 3.0 is a major release with breaking changes. This document is organized into two main sections: Breaking Changes (what you must update) and New Features (what you can now use).
Update your composer.json to require version 3.0:
composer require respect/validation:^3.0Respect\Validation 3.0 requires PHP 8.5 or above (2.x required PHP 8.1+).
The main Validator class has been renamed to ValidatorBuilder:
- use Respect\Validation\Validator as v;
+ use Respect\Validation\ValidatorBuilder as v;Alternatively, v is now a global class alias, so you can use it directly without any import:
- use Respect\Validation\Validator as v;
v::intType()->assert($input);Throughout the codebase, "rules" have been renamed to "validators":
| 2.x | 3.0 |
|---|---|
Respect\Validation\Rules namespace |
Respect\Validation\Validators namespace |
Rule interface |
Validator interface |
AbstractRule class |
Base classes in Validators\Core |
Factory class |
ValidatorFactory class |
InvalidRuleConstructorException |
InvalidValidatorException |
In 2.x, validate() returned a boolean. In 3.0, it returns a ResultQuery object:
- if (v::intType()->validate($input)) {
+ if (v::intType()->isValid($input)) {
// validation passed
}In 2.x, there were two exception-based methods:
check()threw rule-specific exceptions (e.g.,IntTypeException)assert()threwNestedValidationException
In 3.0, both are unified into assert(), which throws ValidationException:
-use Respect\Validation\Exceptions\IntTypeException;
+use Respect\Validation\Exceptions\ValidationException;
try {
- v::intType()->check($input);
-} catch (IntTypeException $exception) {
+ v::intType()->assert($input);
+} catch (ValidationException $exception) {
echo $exception->getMessage();
}The ValidationException provides all methods previously split between exceptions:
-use Respect\Validation\Exceptions\NestedValidationException;
+use Respect\Validation\Exceptions\ValidationException;
try {
v::intType()->positive()->assert($input);
-} catch (NestedValidationException $exception) {
+} catch (ValidationException $exception) {
$exception->getMessage(); // First error message
$exception->getFullMessage(); // Full error tree
$exception->getMessages(); // All errors as array
}The type() validator has been replaced in favor of specific type validators:
- v::type('string')->assert($input);
+ v::stringType()->assert($input);
- v::type('int')->assert($input);
+ v::intType()->assert($input);
- v::type('array')->assert($input);
+ v::arrayType()->assert($input);- v::yes()->assert('yes');
+ v::trueVal()->assert('yes');
- v::no()->assert('no');
+ v::falseVal()->assert('no');- v::keyNested('user.profile.name', v::stringType())->assert($data);
+ v::key('user', v::key('profile', v::key('name', v::stringType())))->assert($data);These validators have been replaced by DateTimeDiff, which provides more flexibility by allowing you to specify the time unit and comparison validator:
- v::minAge(18)->assert($birthDate);
+ v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert($birthDate);
- v::maxAge(65)->assert($birthDate);
+ v::dateTimeDiff('years', v::lessThanOrEqual(65))->assert($birthDate);
- v::age(18, 65)->assert($birthDate);
+ v::dateTimeDiff('years', v::between(18, 65))->assert($birthDate);Combine Satisfies with a mathematical library of your choice:
- v::primeNumber()->assert(7);
+ v::satisfies(static fn ($input) => \MathPHP\NumberTheory\Integer::isPrime($input))->assert(7);See: https://github.com/markrogoyski/math-php
Use Satisfies instead:
- v::filterVar(FILTER_VALIDATE_INT)->assert(123);
+ v::satisfies(static fn($input) => filter_var($input, FILTER_VALIDATE_INT) !== false)->assert(123);Use Satisfies instead:
- v::uploaded()->assert($fileName);
+ v::satisfies('is_uploaded_file')->assert($fileName);We offer no recommendation for replacing this validator.
The Attribute validator has been replaced by three separate validators with different signatures.
| Validator | Description |
|---|---|
Property |
Property must exist and pass validation |
PropertyOptional |
Validates only if property exists |
PropertyExists |
Only checks if property exists |
// Property must exist and be valid
- v::attribute('name', v::stringType())->assert($user);
+ v::property('name', v::stringType())->assert($user);
// Property is optional (mandatory = false)
- v::attribute('age', v::intType(), false)->assert($user);
+ v::propertyOptional('age', v::intType())->assert($user);
// Only check existence (no validator, mandatory = true)
- v::attribute('id')->assert($user);
+ v::propertyExists('id')->assert($user);The Key validator has been split into three separate validators with different signatures.
| Validator | Description |
|---|---|
Key |
Key must exist and pass validation |
KeyOptional |
Validates only if key exists |
KeyExists |
Only checks if key exists |
// Key must exist and be valid
- v::key('email', v::email())->assert($data);
+ v::key('email', v::email())->assert($data);
// Key is optional (mandatory = false)
- v::key('phone', v::phone(), false)->assert($data);
+ v::keyOptional('phone', v::phone())->assert($data);
// Only check existence (no validator, mandatory = true)
- v::key('id')->assert($data);
+ v::keyExists('id')->assert($data);The length() validator no longer accepts scalar min/max arguments. Use the new composition syntax:
- v::length(5, 10)->assert($input);
+ v::length(v::between(5, 10))->assert($input);
- v::length(5, null)->assert($input);
+ v::length(v::greaterThanOrEqual(5))->assert($input);
- v::length(null, 10)->assert($input);
+ v::length(v::lessThanOrEqual(10))->assert($input);Or use the prefixed shortcuts:
v::lengthBetween(5, 10)->assert($input);
v::lengthGreaterThanOrEqual(5)->assert($input);
v::lengthLessThanOrEqual(10)->assert($input);The size() validator no longer accepts string sizes like '5MB'. Use the new composition syntax:
- v::size('5MB', '10MB')->assert($file);
+ v::size('MB', v::between(5, 10))->assert($file);
- v::size('5MB', null)->assert($file);
+ v::size('MB', v::greaterThanOrEqual(5))->assert($file);
- v::size(null, '10MB')->assert($file);
+ v::size('MB', v::lessThanOrEqual(10))->assert($file);The Each validator now rejects stdClass and other non-iterable values:
// These now fail in 3.0
v::each(v::alwaysValid())->isValid(new stdClass()); // falseAllOf, AnyOf, NoneOf, and OneOf now require at least two validators:
- v::allOf(v::intType())->assert($input);
+ v::intType()->assert($input);After (previously Call) now does not handle PHP errors inside the callback you provided.
v::after('strtolower', v::equals('foo'))->assert(123); // Error bubbles outYou can use anonymous functions to handle errors or perform type conversions instead:
v::after(static fn ($i) => strtolower((string) $i), v::equals('123'));The parameter $identical, which controlled case-insensitivy and strict typing was
removed. Now these validators will always compare case-sensitive and use
strict typing.
- v::contains('needle', identical: true);
- v::containsAny(['needle1', 'needle2'], identical: true);
- v::in(['hay', 'stack'], compareIdentical: true);
- v::startsWith('needle', identical: true);
- v::endsWith('needle', identical: true);
+ v::contains('needle'); // always strict case and type
+ v::containsAny(['needle1', 'needle2']); // always strict case and type
+ v::in(['hay', 'stack']); // always strict case and type
+ v::startsWith('needle'); // always strict case and type
+ v::endsWith('needle'); // always strict case and typeFor more information, refer to case-sensitiveness.md
newsscheme is now unsupported. Consult the validator documentation for the list of supported schemes.mailtoURLs now are also validated with theEmailvalidator.- Supported schemes now validate using
IpandDomainwhen appropriate.
Some validators now require additional packages:
| Validators | Required packages |
|---|---|
CountryCode, LanguageCode, SubdivisionCode, CurrencyCode |
sokil/php-isocodes and sokil/php-isocodes-db-only |
Uuid |
ramsey/uuid |
Install with:
composer require sokil/php-isocodes sokil/php-isocodes-db-only
composer require ramsey/uuid| 2.x | 3.0 | Notes |
|---|---|---|
Call |
After |
Clearer intent |
Callback |
Satisfies |
Clearer intent |
Min |
GreaterThanOrEqual |
Clearer comparison semantics |
Max |
LessThanOrEqual |
Clearer comparison semantics |
Nullable |
NullOr |
Consistent naming pattern |
Optional |
UndefOr |
Validates if value is undefined or passes |
KeyValue |
Factory |
Deferred validator creation |
- v::call('strtolower', v::equals('foo'))->assert($input);
+ v::after('strtolower', v::equals('foo'))->assert($input);
- v::callback('is_int')->assert($input);
+ v::satisfies('is_int')->assert($input);
- v::min(18)->assert($age);
+ v::greaterThanOrEqual(18)->assert($age);
- v::max(100)->assert($age);
+ v::lessThanOrEqual(100)->assert($age);
- v::nullable(v::email())->assert($email);
+ v::nullOr(v::email())->assert($email);
- v::optional(v::email())->assert($email);
+ v::undefOr(v::email())->assert($email);In 3.0, Min and Max validators exist but have different semantics. They extract the minimum/maximum value from a collection and validate it (see Result composition).
| Validator | 2.x | 3.x |
|---|---|---|
Min |
Single value >= | Pick minimum value from iterable and validate |
Max |
Single value <= | Pick minimum value from iterable and validate |
In 2.x, NotBlank validated that a value was not blank. In 3.0, Blank validates that a value is blank, so you need to negate it:
- v::notBlank()->assert($input);
+ v::not(v::blank())->assert($input);Or use the prefixed shortcut (see New Features):
v::notBlank()->assert($input);The NotEmpty validator was renamed to Falsy with inverted logic:
- v::notEmpty()->assert($input);
+ v::not(v::falsy())->assert($input);Or use the prefixed shortcut:
v::notFalsy()->assert($input);The NoWhitespace validator was renamed to Spaced with inverted logic. Spaced validates that a string contains whitespace:
- v::noWhitespace()->assert($input);
+ v::not(v::spaced())->assert($input);Or use the prefixed shortcut:
v::notSpaced()->assert($input);- v::iterableType()->assert($input);
+ v::iterableVal()->assert($input);Note: IterableType still exists in 3.0 but now strictly uses PHP's is_iterable() function (rejecting stdClass).
- v::intType()->setTemplate('Please provide an integer')->check($input);
+ v::templated('Please provide an integer', v::intType())->assert($input);- v::intType()->setName('User ID')->check($input);
+ v::named('User ID', v::intType())->assert($input);The main placeholder in message templates has been renamed from {{name}} to {{subject}}:
- {{name}} must be valid
+ {{subject}} must be validThe value of {{subject}} is determined by context:
| Context | Value | Example |
|---|---|---|
| Key/Property/Each validators | The path | `.email` must be valid |
Named validator |
The custom name | Email must be valid |
| Both path and name defined | Path with name suffix | `.email` (<- User Email) must be valid |
| No path or name | The input value | "invalid#email" must be valid |
The Factory class has been replaced by a dependency injection container approach using ContainerRegistry:
- use Respect\Validation\Factory;
+ use Respect\Validation\ContainerRegistry;
+ use Symfony\Contracts\Translation\TranslatorInterface;
- Factory::setDefaultInstance(
- (new Factory())
- ->withRuleNamespace('My\\Validation\\Rules')
- ->withTranslator($translator)
- );
+ $container = ContainerRegistry::createContainer([
+ // Add custom validator namespaces
+ 'respect.validation.rule_factory.namespaces' => [
+ 'My\\Validation\\Validators',
+ 'Respect\\Validation\\Validators', // Keep the default namespace
+ ],
+ // Add a translator
+ TranslatorInterface::class => $translator,
+ ]);
+ ContainerRegistry::setContainer($container);The ContainerRegistry::createContainer() returns a PHP-DI container. You can also use any PSR-11 compatible container with ContainerRegistry::setContainer().
In 2.x, custom validators required a separate exception class to define error message templates. In 3.0, templates are defined using PHP attributes directly on the validator class.
Before (2.x): Two files required - validator and exception:
// CustomException.php
use Respect\Validation\Exceptions\ValidationException;
final class CustomException extends ValidationException
{
protected $defaultTemplates = [
self::MODE_DEFAULT => [
self::STANDARD => '{{name}} must be custom',
],
self::MODE_NEGATIVE => [
self::STANDARD => '{{name}} must not be custom',
],
];
}
// Custom.php
use Respect\Validation\Rules\AbstractRule;
final class Custom extends AbstractRule
{
public function validate($input): bool
{
return $this->checkSomething($input);
}
// Implementation of checkSomething()...
}After (3.0): Single file with #[Template] attribute:
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Validator;
#[Template(
'{{subject}} must be custom',
'{{subject}} must not be custom',
)]
final class Custom implements Validator
{
public function evaluate(mixed $input): Result
{
return Result::of($this->checkSomething($input), $input, $this);
}
// Implementation of checkSomething()...
}Base classes available in Respect\Validation\Validators\Core:
Simple- For validators with simple boolean logicComposite- For validators that combine multiple validatorsEnvelope- For validators that modify how another validator works
Version 3.0 introduces several new validators:
| Validator | Description |
|---|---|
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) |
Hetu |
Validates Finnish personal identity codes (henkilötunnus) |
KeyExists |
Checks if an array key exists |
KeyOptional |
Validates an array key only if it exists |
Factory |
Creates validators dynamically based on input |
Masked |
Masks sensitive input values in error messages |
Named |
Customizes the subject name in error messages |
PropertyExists |
Checks if an object property exists |
PropertyOptional |
Validates an object property only if it exists |
Templated |
Attaches custom error message templates |
Validates that every item in an iterable passes the given validator:
v::all(v::intType())->assert([1, 2, 3]); // passes
v::all(v::positive())->assert([1, -2, 3]); // failsValidates object properties using PHP attributes defined on the class:
use Respect\Validation\Validators as Validator;
final readonly class User
{
public function __construct(
#[Validator\Email]
public string $email,
#[Validator\Between(18, 120)]
public int $age,
#[Validator\Length(new Validator\Between(1, 100))]
public string $name,
) {
}
}
// Validate all properties at once
v::attributes()->assert($user);Validates that a value is between two bounds, excluding the bounds themselves:
v::betweenExclusive(1, 10)->assert(5); // passes
v::betweenExclusive(1, 10)->assert(1); // fails (1 is not > 1)
v::betweenExclusive(1, 10)->assert(10); // fails (10 is not < 10)Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
$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']); // passesValidates the count of occurrences of a value:
v::containsCount('a', v::equals(3))->assert('banana'); // passes (3 'a's)
v::containsCount('x', v::greaterThan(0))->assert('example'); // passesValidates date/time differences. Replaces the removed Age, MinAge, and MaxAge validators:
v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert('2000-01-01'); // passes if 18+ years ago
v::dateTimeDiff('days', v::lessThan(30))->assert('2024-01-15'); // passes if less than 30 days agoValidates Finnish personal identity codes (henkilötunnus):
v::hetu()->assert('010101-123N'); // passesChecks if an array key exists (without validating its value):
v::keyExists('email')->assert(['email' => 'user@example.com']); // passes
v::keyExists('email')->assert(['name' => 'John']); // failsValidates an array key only if it exists:
v::keyOptional('phone', v::phone())->assert(['name' => 'John']); // passes (key absent)
v::keyOptional('phone', v::phone())->assert(['phone' => '+1234567890']); // passes
v::keyOptional('phone', v::phone())->assert(['phone' => 'invalid']); // failsCreates validators dynamically based on the input, useful for cross-field validation:
// Validate that 'confirmation' matches 'password'
v::factory(
fn($input) => v::key('confirmation', v::equals($input['password'] ?? null))
)->assert(['password' => 'secret', 'confirmation' => 'secret']); // passesDecorates a validator to mask sensitive input values in error messages while still validating the original unmasked data. This allows applications to protect sensitive information like passwords, credit cards, or emails without implementing a custom layer between Validation and end users:
v::masked('1-@', v::email(),v::email())->assert('invalid@example.com');
// → "*******@example.com" must be a valid email address
v::masked('6-12', v::creditCard(), 'X')->assert('4111111111111211');
// → "41111XXXXXXX1211" must be a valid credit card numberCustomizes the subject name in error messages:
v::named('User ID', v::intType())->assert('abc');
// → User ID must be an integerChecks if an object property exists (without validating its value):
v::propertyExists('name')->assert($user); // passes if $user->name existsValidates an object property only if it exists:
v::propertyOptional('nickname', v::stringType())->assert($user); // passes if absent
v::propertyOptional('nickname', v::stringType())->assert($user); // validates if presentAttaches custom error message templates to validators:
v::templated('Please provide a valid number', v::intType())->assert('abc');
// → Please provide a valid numberThe validate() method now returns a ResultQuery object that provides detailed error inspection:
$result = v::numericVal()->positive()->between(1, 255)->validate($input);
$result->hasFailed(); // Check if the validation has failed (boolean)
$result->getMessage(); // Get the first error message
$result->getFullMessage(); // Get all error messages as a tree
$result->getMessages(); // Get all error messages as an arrayUse findByPath() to locate validation results using dot notation for nested structures:
$result = v::init()
->key('user', v::key('email', v::email()))
->key('items', v::each(v::positive()))
->validate([
'user' => ['email' => 'invalid'],
'items' => [10, -5, 20],
]);
echo $result->findByPath('user.email');
// → `.user.email` must be a valid email address
echo $result->findByPath('items.1');
// → `.items.1` must be a positive numberUse findByName() to locate validation results by the custom name set with the Named validator:
$result = v::named('User Email', v::email())->validate('bad');
echo $result->findByName('User Email');
// → User Email must be a valid email addressUse findById() to locate validation results by the validator's ID (e.g., stringType, email):
$result = v::stringType()->email()->validate(123);
echo $result->findById('stringType');
// → 123 must be a stringAll finder methods return either a ResultQuery instance or null if not found.
Version 3.0 introduces convenient prefixed shortcuts for common patterns:
// Null-or validators
v::nullOrEmail()->assert(null); // passes
v::nullOrEmail()->assert('user@example.com'); // passes
// Undefined-or validators
v::undefOrPositive()->assert(null); // passes
v::undefOrPositive()->assert(5); // passes
// All (array item) validators
v::allPositive()->assert([1, 2, 3]); // passes
v::allEmail()->assert(['a@b.com', 'c@d.com']); // passes
// Key validators
v::keyEmail('email')->assert(['email' => 'user@example.com']); // passes
// Property validators
v::propertyPositive('age')->assert($user); // passes if $user->age is positive
// Length validators
v::lengthBetween(5, 10)->assert('hello'); // passes
// Min/Max validators
v::minGreaterThan(0)->assert([1, 2, 3]); // passes
v::maxLessThan(10)->assert([1, 2, 3]); // passes
// Negation prefix
v::notBlank()->assert('hello'); // passesValidators compose results for clearer error messages:
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::key('age', v::greaterThanOrEqual(18))->assert(['age' => 16]);
// → `.age` must be greater than or equal to 18Error messages now include full paths for nested structures:
v::init()
->key('mysql', v::init()->key('host', v::stringType()))
->key('postgresql', v::init()->key('host', v::stringType()))
->assert($input);
// → `.mysql.host` must be a stringWhen validation fails, stack traces now point to your code instead of library internals.
2.x:
Stack trace:
#0 /opt/vendor/respect/validation/library/Factory.php(233): ReflectionClass->newInstance(1.0, 'intVal', Array, Object(Respect\Validation\Message\Formatter))
#1 /opt/vendor/respect/validation/library/Factory.php(164): Respect\Validation\Factory->createValidationException('Respect\\Validat...', 'intVal', 1.0, Array, Object(Respect\Validation\Message\Formatter))
#2 /opt/vendor/respect/validation/library/Rules/AbstractRule.php(70): Respect\Validation\Factory->exception(Object(Respect\Validation\Rules\IntVal), 1.0, Array)
#3 /opt/vendor/respect/validation/library/Rules/AbstractRule.php(45): Respect\Validation\Rules\AbstractRule->reportError(1.0)
#4 /opt/vendor/respect/validation/library/Rules/AbstractRule.php(53): Respect\Validation\Rules\AbstractRule->assert(1.0)
#5 /opt/vendor/respect/validation/library/Rules/AllOf.php(50): Respect\Validation\Rules\AbstractRule->check(1.0)
#6 /opt/vendor/respect/validation/library/Validator.php(68): Respect\Validation\Rules\AllOf->check(1.0)
#7 /opt/example.php(11): Respect\Validation\Validator->check(1.0)
#8 {main}
3.0:
Stack trace:
#0 /opt/example.php(11): Respect\Validation\Validator->assert(1.0)
#1 {main}
Pass your own exception directly to assert():
v::email()->assert($input, new DomainException('Invalid email'));Or use a callable to access the original exception message:
v::email()->assert(
$input,
fn(ValidationException $e) => new DomainException($e->getMessage()),
);Customize how values are rendered in templates using pipes:
// `|raw` - removes quotes
v::templated(
'{{subject|raw}} is not a UUID v{{version}}',
v::uuid(1),
)->assert('eb3115e5-bd16-4939-ab12-2b95745a30f3');
// → eb3115e5-bd16-4939-ab12-2b95745a30f3 is not a UUID v1
// `|quote` - uses backticks
v::templated(
'Value must match {{pattern|quote}}',
v::regex('/^\d+$/'),
)->assert('abc');
// → Value must match `/^\d+$/`
// `|trans` - translates the value (when using a translator)
v::templated(
'Le champ {{field|trans}} est invalide',
v::email(),
['field' => 'email_address'],
)->assert('invalid');
// → Le champ "adresse e-mail" est invalide
// `|list:or` and `|list:and` - formats arrays as readable lists
v::templated(
'Status must be {{haystack|list:or}}',
v::in(['active', 'pending']),
)->assert('deleted');
// → Status must be "active" or "pending" // Main class
- use Respect\Validation\Validator as v;
+ use Respect\Validation\ValidatorBuilder as v;
// Boolean validation (unchanged)
v::intType()->isValid($input);
// Result-based validation
- v::intType()->validate($input); // bool
+ v::intType()->isValid($input); // bool
// Exception-based validation
- v::intType()->check($input); // IntTypeException
+ v::intType()->assert($input); // ValidationException
- v::intType()->assert($input); // NestedValidationException
+ v::intType()->assert($input); // ValidationException
// Renamed validators
- v::min(18);
+ v::greaterThanOrEqual(18);
- v::max(100);
+ v::lessThanOrEqual(100);
- v::nullable(v::email());
+ v::nullOr(v::email());
// Attribute → Property
- v::attribute('name', v::stringType());
+ v::property('name', v::stringType());
- v::attribute('age', v::intType(), false);
+ v::propertyOptional('age', v::intType());
- v::attribute('id');
+ v::propertyExists('id');
// Key changes
- v::key('phone', v::phone(), false);
+ v::keyOptional('phone', v::phone());
- v::key('id');
+ v::keyExists('id');
// Removed validators
- v::keyNested('a.b.c', $validator);
+ v::key('a', v::key('b', v::key('c', $validator)));
- v::yes();
+ v::trueVal();
- v::no();
+ v::falseVal();
- v::minAge(18);
+ v::dateTimeDiff('years', v::greaterThanOrEqual(18));
- v::maxAge(65);
+ v::dateTimeDiff('years', v::lessThanOrEqual(65));- Check the Feature Guide for detailed usage examples
- Browse the List of Validators for validator-specific documentation
- Review the article What's New in Respect\Validation 3.0
- Open an issue on GitHub if you find bugs or have questions