docs/migrating-from-v2-to-v3.md
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.0
Respect\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 |
validate() now returns an objectIn 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
}
check() and assert() behavior changesIn 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.
-use Respect\Validation\Exceptions\IntTypeException;
+use Respect\Validation\Exceptions\ValidationException;
try {
v::intType()->check($input);
-} catch (IntTypeException $exception) {
+} catch (ValidationException $exception) {
echo $exception->getMessage();
}
-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
}
TypeThe 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);
Yes and No- v::yes()->assert('yes');
+ v::trueVal()->assert('yes');
- v::no()->assert('no');
+ v::falseVal()->assert('no');
KeyNested- 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 MaxAgeThese 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);
PrimeNumber, Fibonacci, PerfectSquareCombine 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
FilterVarUse Satisfies instead:
- v::filterVar(FILTER_VALIDATE_INT)->assert(123);
+ v::satisfies(static fn($input) => filter_var($input, FILTER_VALIDATE_INT) !== false)->assert(123);
UploadedUse Satisfies instead:
- v::uploaded()->assert($fileName);
+ v::satisfies('is_uploaded_file')->assert($fileName);
VideoUrlWe offer no recommendation for replacing this validator.
Attribute replaced by Property variantsThe 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);
Key split into three validatorsThe 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);
Length signature changedThe 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);
Size signature changedThe 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);
Each validator stricterThe Each validator now rejects stdClass and other non-iterable values:
// These now fail in 3.0
v::each(v::alwaysValid())->isValid(new stdClass()); // false
AllOf, AnyOf, NoneOf, and OneOf now require at least two validators:
- v::allOf(v::intType())->assert($input);
+ v::intType()->assert($input);
After does not handle errors anymoreAfter (previously Call) now does not handle PHP errors inside the callback you provided.
v::after('strtolower', v::equals('foo'))->assert(123); // Error bubbles out
You can use anonymous functions to handle errors or perform type conversions instead:
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.
- 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.Ip and Domain when 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 |
NotBlank logic invertedIn 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);
NotEmpty renamed to FalsyThe 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);
NoWhitespace renamed to SpacedThe 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);
IterableType renamed to IterableVal- 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).
setTemplate() removed- v::intType()->setTemplate('Please provide an integer')->check($input);
+ v::templated('Please provide an integer', v::intType())->assert($input);
setName() removed- v::intType()->setName('User ID')->check($input);
+ v::named('User ID', v::intType())->assert($input);
{{name}} placeholder renamedThe main placeholder in message templates has been renamed from {{name}} to {{subject}}:
- {{name}} must be valid
+ {{subject}} must be valid
The 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 worksVersion 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) |
ContainsCount | Validates the count of occurrences in a value |
DateTimeDiff | Validates date/time differences (replaces Age validators) |
Formatted | Formats input values in error messages |
ShortCircuit | Stops at first failure instead of collecting all errors |
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 |
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]); // fails
Validates 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 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'); // passes
Validates 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 ago
Decorates a validator to format input values in error messages while still validating the original input:
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
Validates input against a series of validators, stopping at the first failure. Useful for dependent validations:
$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
Validates Finnish personal identity codes (henkilötunnus):
v::hetu()->assert('010101-123N'); // passes
Checks if an array key exists (without validating its value):
v::keyExists('email')->assert(['email' => '[email protected]']); // passes
v::keyExists('email')->assert(['name' => 'John']); // fails
Validates 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']); // fails
Creates 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']); // passes
Customizes the subject name in error messages:
v::named('User ID', v::intType())->assert('abc');
// → User ID must be an integer
Checks if an object property exists (without validating its value):
v::propertyExists('name')->assert($user); // passes if $user->name exists
Validates 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 present
Attaches custom error message templates to validators:
v::templated('Please provide a valid number', v::intType())->assert('abc');
// → Please provide a valid number
The 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 array
Use 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 number
Use 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 address
Use 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 string
All 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('[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
Validators 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 18
Error 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 string
When 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()),
);
The project now uses Symfony translation contracts instead of a custom callback.
See messages/translation.md for more info.
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 (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));