Back to Validation

Migrating from v2.x to v3.x

docs/migrating-from-v2-to-v3.md

3.1.131.7 KB
Original Source
<!-- SPDX-License-Identifier: MIT SPDX-FileCopyrightText: (c) Respect Project Contributors SPDX-FileContributor: Alexandre Gomes Gaigalas <[email protected]> SPDX-FileContributor: Henrique Moody <[email protected]> -->

Migrating from v2.x to v3.x

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

Installation

Update your composer.json to require version 3.0:

shell
composer require respect/validation:^3.0

Respect\Validation 3.0 requires PHP 8.5 or above (2.x required PHP 8.1+).


Breaking changes

Class and namespace changes

Main class rename

The main Validator class has been renamed to ValidatorBuilder:

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

diff
- use Respect\Validation\Validator as v;

v::intType()->assert($input);

Internal terminology

Throughout the codebase, "rules" have been renamed to "validators":

2.x3.0
Respect\Validation\Rules namespaceRespect\Validation\Validators namespace
Rule interfaceValidator interface
AbstractRule classBase classes in Validators\Core
Factory classValidatorFactory class
InvalidRuleConstructorExceptionInvalidValidatorException

Validation methods

validate() now returns an object

In 2.x, validate() returned a boolean. In 3.0, it returns a ResultQuery object:

diff
- if (v::intType()->validate($input)) {
+ if (v::intType()->isValid($input)) {
      // validation passed
  }

check() and assert() behavior changes

In 2.x, both check() and assert() threw validator-specific exception classes (e.g., IntTypeException), which were children of ValidationException. The difference was that check() threw these exceptions directly and stopped at the first failure, while assert() threw a NestedValidationException (also a child of ValidationException) that collected all errors and provided methods like getFullMessage() and getMessages() not available on the base ValidationException.

In 3.0, validator-specific exception classes and NestedValidationException no longer exist. Both check() and assert() throw a unified ValidationException that includes getMessage(), getFullMessage(), and getMessages(). The behavioral distinction is preserved: check() still fails fast (stopping at the first failure, using ShortCircuit internally), while assert() collects all errors.

diff
-use Respect\Validation\Exceptions\IntTypeException;
+use Respect\Validation\Exceptions\ValidationException;

try {
    v::intType()->check($input);
-} catch (IntTypeException $exception) {
+} catch (ValidationException $exception) {
    echo $exception->getMessage();
}
diff
-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
}

Validator changes

Removed validators

Type

The type() validator has been replaced in favor of specific type validators:

diff
- 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);
Yes and No
diff
- v::yes()->assert('yes');
+ v::trueVal()->assert('yes');

- v::no()->assert('no');
+ v::falseVal()->assert('no');
KeyNested
diff
- v::keyNested('user.profile.name', v::stringType())->assert($data);
+ v::key('user', v::key('profile', v::key('name', v::stringType())))->assert($data);
Age, MinAge, and MaxAge

These validators have been replaced by DateTimeDiff, which provides more flexibility by allowing you to specify the time unit and comparison validator:

diff
- 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);
PrimeNumber, Fibonacci, PerfectSquare

Combine Satisfies with a mathematical library of your choice:

diff
- v::primeNumber()->assert(7);
+ v::satisfies(static fn ($input) => \MathPHP\NumberTheory\Integer::isPrime($input))->assert(7);

See: https://github.com/markrogoyski/math-php

FilterVar

Use Satisfies instead:

diff
- v::filterVar(FILTER_VALIDATE_INT)->assert(123);
+ v::satisfies(static fn($input) => filter_var($input, FILTER_VALIDATE_INT) !== false)->assert(123);
Uploaded

Use Satisfies instead:

diff
- v::uploaded()->assert($fileName);
+ v::satisfies('is_uploaded_file')->assert($fileName);
VideoUrl

We offer no recommendation for replacing this validator.

Behavior changes

Attribute replaced by Property variants

The Attribute validator has been replaced by three separate validators with different signatures.

ValidatorDescription
PropertyProperty must exist and pass validation
PropertyOptionalValidates only if property exists
PropertyExistsOnly checks if property exists
diff
  // 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);
Key split into three validators

The Key validator has been split into three separate validators with different signatures.

ValidatorDescription
KeyKey must exist and pass validation
KeyOptionalValidates only if key exists
KeyExistsOnly checks if key exists
diff
  // 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);
Length signature changed

The length() validator no longer accepts scalar min/max arguments. Use the new composition syntax:

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

php
v::lengthBetween(5, 10)->assert($input);
v::lengthGreaterThanOrEqual(5)->assert($input);
v::lengthLessThanOrEqual(10)->assert($input);
Size signature changed

The size() validator no longer accepts string sizes like '5MB'. Use the new composition syntax:

diff
- 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);
Each validator stricter

The Each validator now rejects stdClass and other non-iterable values:

php
// These now fail in 3.0
v::each(v::alwaysValid())->isValid(new stdClass()); // false
Composite validators require two or more validators

AllOf, AnyOf, NoneOf, and OneOf now require at least two validators:

diff
- v::allOf(v::intType())->assert($input);
+ v::intType()->assert($input);
After does not handle errors anymore

After (previously Call) now does not handle PHP errors inside the callback you provided.

php
v::after('strtolower', v::equals('foo'))->assert(123); // Error bubbles out

You can use anonymous functions to handle errors or perform type conversions instead:

php
v::after(static fn ($i) => strtolower((string) $i), v::equals('123'));
Contains, ContainsAny, In, EndsWith and StartsWith strict by default.

The parameter $identical, which controlled case-insensitivy and strict typing was removed. Now these validators will always compare case-sensitive and use strict typing.

diff
- 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 type

For more information, refer to case-sensitiveness.md

Url now validates domains, IP addresses and domains when appropriate.
  • news scheme is now unsupported. Consult the validator documentation for the list of supported schemes.
  • mailto URLs now are also validated with the Email validator.
  • Supported schemes now validate using Ip and Domain when appropriate.
New package dependencies

Some validators now require additional packages:

ValidatorsRequired packages
CountryCode, LanguageCode, SubdivisionCode, CurrencyCodesokil/php-isocodes and sokil/php-isocodes-db-only
Uuidramsey/uuid

Install with:

shell
composer require sokil/php-isocodes sokil/php-isocodes-db-only
composer require ramsey/uuid

Renamed validators

2.x3.0Notes
CallAfterClearer intent
CallbackSatisfiesClearer intent
MinGreaterThanOrEqualClearer comparison semantics
MaxLessThanOrEqualClearer comparison semantics
NullableNullOrConsistent naming pattern
OptionalUndefOrValidates if value is undefined or passes
KeyValueFactoryDeferred validator creation
diff
- 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).

Validator2.x3.x
MinSingle value >=Pick minimum value from iterable and validate
MaxSingle value <=Pick minimum value from iterable and validate
NotBlank logic inverted

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:

diff
- v::notBlank()->assert($input);
+ v::not(v::blank())->assert($input);

Or use the prefixed shortcut (see New Features):

php
v::notBlank()->assert($input);
NotEmpty renamed to Falsy

The NotEmpty validator was renamed to Falsy with inverted logic:

diff
- v::notEmpty()->assert($input);
+ v::not(v::falsy())->assert($input);

Or use the prefixed shortcut:

php
v::notFalsy()->assert($input);
NoWhitespace renamed to Spaced

The NoWhitespace validator was renamed to Spaced with inverted logic. Spaced validates that a string contains whitespace:

diff
- v::noWhitespace()->assert($input);
+ v::not(v::spaced())->assert($input);

Or use the prefixed shortcut:

php
v::notSpaced()->assert($input);
IterableType renamed to IterableVal
diff
- 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).

Custom message methods removed

setTemplate() removed

diff
- v::intType()->setTemplate('Please provide an integer')->check($input);
+ v::templated('Please provide an integer', v::intType())->assert($input);

setName() removed

diff
- v::intType()->setName('User ID')->check($input);
+ v::named('User ID', v::intType())->assert($input);

{{name}} placeholder renamed

The main placeholder in message templates has been renamed from {{name}} to {{subject}}:

diff
- {{name}} must be valid
+ {{subject}} must be valid

The value of {{subject}} is determined by context:

ContextValueExample
Key/Property/Each validatorsThe path`.email` must be valid
Named validatorThe custom nameEmail must be valid
Both path and name definedPath with name suffix`.email` (<- User Email) must be valid
No path or nameThe input value"invalid#email" must be valid

Factory and container changes

The Factory class has been replaced by a dependency injection container approach using ContainerRegistry:

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

Custom validators

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:

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

php
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 logic
  • Composite - For validators that combine multiple validators
  • Envelope - For validators that modify how another validator works

New features

New validators

Version 3.0 introduces several new validators:

ValidatorDescription
AllValidates that every item in an iterable passes validation
AttributesValidates object properties using PHP attributes
BetweenExclusiveValidates that a value is between two bounds (exclusive)
ContainsCountValidates the count of occurrences in a value
DateTimeDiffValidates date/time differences (replaces Age validators)
FormattedFormats input values in error messages
ShortCircuitStops at first failure instead of collecting all errors
HetuValidates Finnish personal identity codes (henkilötunnus)
KeyExistsChecks if an array key exists
KeyOptionalValidates an array key only if it exists
FactoryCreates validators dynamically based on input
NamedCustomizes the subject name in error messages
PropertyExistsChecks if an object property exists
PropertyOptionalValidates an object property only if it exists
TemplatedAttaches custom error message templates

All

Validates that every item in an iterable passes the given validator:

php
v::all(v::intType())->assert([1, 2, 3]); // passes
v::all(v::positive())->assert([1, -2, 3]); // fails

Attributes

Validates object properties using PHP attributes defined on the class:

php
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);

BetweenExclusive

Validates that a value is between two bounds, excluding the bounds themselves:

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

ContainsCount

Validates the count of occurrences of a value:

php
v::containsCount('a', v::equals(3))->assert('banana'); // passes (3 'a's)
v::containsCount('x', v::greaterThan(0))->assert('example'); // passes

DateTimeDiff

Validates date/time differences. Replaces the removed Age, MinAge, and MaxAge validators:

php
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 ago

Formatted

Decorates a validator to format input values in error messages while still validating the original input:

php
use Respect\StringFormatter\FormatterBuilder as f;

v::formatted(f::mask('1-4'), v::email())->assert('not an email');
// → "****an email" must be an email address

v::formatted(f::pattern('#### #### #### ####'), v::creditCard())->assert('1234123412341234');
// → "1234 1234 1234 1234" must be a credit card number

ShortCircuit

Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:

php
$validator = v::shortCircuit(
    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']); // passes

Hetu

Validates Finnish personal identity codes (henkilötunnus):

php
v::hetu()->assert('010101-123N'); // passes

KeyExists

Checks if an array key exists (without validating its value):

php
v::keyExists('email')->assert(['email' => '[email protected]']); // passes
v::keyExists('email')->assert(['name' => 'John']); // fails

KeyOptional

Validates an array key only if it exists:

php
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']); // fails

Factory

Creates validators dynamically based on the input, useful for cross-field validation:

php
// Validate that 'confirmation' matches 'password'
v::factory(
    fn($input) => v::key('confirmation', v::equals($input['password'] ?? null))
)->assert(['password' => 'secret', 'confirmation' => 'secret']); // passes

Named

Customizes the subject name in error messages:

php
v::named('User ID', v::intType())->assert('abc');
// → User ID must be an integer

PropertyExists

Checks if an object property exists (without validating its value):

php
v::propertyExists('name')->assert($user); // passes if $user->name exists

PropertyOptional

Validates an object property only if it exists:

php
v::propertyOptional('nickname', v::stringType())->assert($user); // passes if absent
v::propertyOptional('nickname', v::stringType())->assert($user); // validates if present

Templated

Attaches custom error message templates to validators:

php
v::templated('Please provide a valid number', v::intType())->assert('abc');
// → Please provide a valid number

Result-based validation

The validate() method now returns a ResultQuery object that provides detailed error inspection:

php
$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 array

Finding by path

Use findByPath() to locate validation results using dot notation for nested structures:

php
$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 number

Finding by name

Use findByName() to locate validation results by the custom name set with the Named validator:

php
$result = v::named('User Email', v::email())->validate('bad');

echo $result->findByName('User Email');
// → User Email must be a valid email address

Finding by ID

Use findById() to locate validation results by the validator's ID (e.g., stringType, email):

php
$result = v::stringType()->email()->validate(123);

echo $result->findById('stringType');
// → 123 must be a string

All finder methods return either a ResultQuery instance or null if not found.

Prefixed shortcuts

Version 3.0 introduces convenient prefixed shortcuts for common patterns:

php
// Null-or validators
v::nullOrEmail()->assert(null);              // passes
v::nullOrEmail()->assert('[email protected]'); // 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(['[email protected]', '[email protected]']); // passes

// Key validators
v::keyEmail('email')->assert(['email' => '[email protected]']); // 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'); // passes

Result composition

Validators compose results for clearer error messages:

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::key('age', v::greaterThanOrEqual(18))->assert(['age' => 16]);
// → `.age` must be greater than or equal to 18

Paths in error messages

Error messages now include full paths for nested structures:

php
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 string

Helpful stack traces

When validation fails, stack traces now point to your code instead of library internals.

2.x:

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

text
Stack trace:
#0 /opt/example.php(11): Respect\Validation\Validator->assert(1.0)
#1 {main}

Custom exceptions in assert()

Pass your own exception directly to assert():

php
v::email()->assert($input, new DomainException('Invalid email'));

Or use a callable to access the original exception message:

php
v::email()->assert(
    $input,
    fn(ValidationException $e) => new DomainException($e->getMessage()),
);

Translation

The project now uses Symfony translation contracts instead of a custom callback.

See messages/translation.md for more info.

Placeholder pipes

Customize how values are rendered in templates using pipes:

php
// `|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"

Quick reference

diff
  // 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 (fail-fast)
- v::intType()->check($input);   // IntTypeException (child of ValidationException)
+ v::intType()->check($input);   // ValidationException

  // Exception-based validation (collect all errors)
- v::intType()->assert($input);  // AllOfExceptopn (child of 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));

Getting help