You can validate your data using the spiral/validation
component. The component provides an array-based DSL to construct
complex validation chains.
The component contains Checkers, Conditions, and Validation object. The Web and GRPC bundle of spiral includes this component by default.
Check Filter/Request Object for deep structural validations.
To install the component:
$ composer require spiral/validation
Please note that the spiral/framework >= 2.6 already includes this component.
To install in spiral use bootloader Spiral\Bootloader\Security\ValidationBootloader
.
To edit the default component configuration create and edit file app/config/validation.php
:
<?php
declare(strict_types=1);
use Spiral\Validation;
return [
// Checkers are resolved using container and provide the ability to isolate some validation rules
// under common name and class. You can register new checkers at any moment without any
// performance issues.
'checkers' => [
'type' => Validation\Checker\TypeChecker::class,
'number' => Validation\Checker\NumberChecker::class,
'mixed' => Validation\Checker\MixedChecker::class,
'address' => Validation\Checker\AddressChecker::class,
'string' => Validation\Checker\StringChecker::class,
'file' => Validation\Checker\FileChecker::class,
'image' => Validation\Checker\ImageChecker::class,
'datetime' => Validation\Checker\DatetimeChecker::class,
'entity' => Validation\Checker\EntityChecker::class,
],
// Enable/disable validation conditions
'conditions' => [
'withAny' => Validation\Condition\WithAnyCondition::class,
'withoutAny' => Validation\Condition\WithoutAnyCondition::class,
'withAll' => Validation\Condition\WithAllCondition::class,
'withoutAll' => Validation\Condition\WithoutAllCondition::class,
],
// Aliases are only used to simplify developer life.
'aliases' => [
'notEmpty' => 'type::notEmpty',
'required' => 'type::notEmpty',
'datetime' => 'datetime::valid',
'timezone' => 'datetime::timezone',
'bool' => 'type::boolean',
'boolean' => 'type::boolean',
'cardNumber' => 'mixed::cardNumber',
'regexp' => 'string::regexp',
'email' => 'address::email',
'url' => 'address::url',
'file' => 'file::exists',
'uploaded' => 'file::uploaded',
'filesize' => 'file::size',
'image' => 'image::valid',
'array' => 'is_array',
'callable' => 'is_callable',
'double' => 'is_double',
'float' => 'is_float',
'int' => 'is_int',
'integer' => 'is_integer',
'numeric' => 'is_numeric',
'long' => 'is_long',
'null' => 'is_null',
'object' => 'is_object',
'real' => 'is_real',
'resource' => 'is_resource',
'scalar' => 'is_scalar',
'string' => 'is_string',
'match' => 'mixed::match',
]
];
Use the component via provider factory:
namespace App\Controller;
use Spiral\Validation;
class HomeController
{
public function index(Validation\ValidationInterface $validation)
{
$validator = $validation->validate(
// data
[
'key' => null
],
// rules
[
'key' => [
'notEmpty'
]
]
);
dump($validator instanceof Validation\Validator);
dump($validator->isValid());
dump($validator->withData(['key' => 'value'])->isValid());
}
}
You can use the
validator
prototype property.
The result of ValidationInterface
->validate
method is ValidatorInterface
. The interface provides basic
API to get result errors and allows them to attach to new data or context (immutable).
interface ValidatorInterface
{
public function withData($data): ValidatorInterface;
public function getValue(string $field, $default = null);
public function withContext($context): ValidatorInterface;
public function getContext();
public function isValid(): bool;
public function getErrors(): array;
}
The proper flow is valid:
public function index(Validation\ValidationInterface $validation)
{
$validator = $validation->validate(
['key' => null],
['key' => ['notEmpty']]
);
if (!$validator->isValid()) {
dump($validator->getErrors());
}
}
A validator can accept any array data source, but internally it will be converted into array form (unless ArrayAccess).
The validation component will always return one error and first fired error per key.
[
'name' => 'This field is required.',
'key' => 'Another error'
]
Error messages can be localized using a
spiral/translator
.
The default spiral validator accepts validation rules in form of nested array. The key is the name of the property to be validated, where the value is an array of rules to be applied to the value sequentially:
$validator = $validation->validate(
['key' => null],
[
'key' => [
'notEmpty', // key must not be empty
'string' // must be string
]
]
);
if (!$validator->isValid()) {
dump($validator->getErrors());
}
The rule, in this case, is the name of the checker method or any available PHP function, which can accept value
as the first argument.
For example we can use is_numeric
directly inside your rule:
$validator = $validation->validate(
['key' => null],
[
'key' => [
'notEmpty', // key must not be empty
'is_numeric' // must be numeric
]
]
);
In many cases, you would need to declare additional rule parameters, conditions, or custom error messages. To achieve that, wrap the rule declaration into an array ([]
).
$validator = $validation->validate(
['key' => null],
[
'key' => [
['notEmpty'], // key must not be empty
['is_numeric'] // must be numeric
]
]
);
You can omit the
[]
if the rule does not need any parameters.
You can split your rule name using ::
prefix, where first part is checker name and second is method name, for example:
$validator = $validation->validate(
['file' => null],
[
'file' => [
'file::uploaded'
]
]
);
All values listed in rule array will be passed as rule arguments. For example to check value using in_array
:
$validator = $validation->validate(
['name' => 'f'],
[
'name' => [
'notEmpty',
['in_array', ['a', 'b', 'c'], true] // in_array($value, ['a', 'b', 'c'], true)
]
]
);
To specify regexp pattern:
$validator = $validation->validate(
['name' => 'b'],
[
'name' => [
'notEmpty',
['regexp', '/^a+$/'] // aaa...
]
]
);
Validator will render default error message for any custom rule, to set custom error message set the rule attribute:
$validator = $validation->validate(
['file' => 'b'],
[
'file' => [
'notEmpty',
['regexp', '/^a+$/', 'error' => 'Invalid pattern, "a+" wanted.'] // aaa...
]
]
);
You can assign custom error messages to any rule.
In some cases the rule must only be activated based on some external condition, use rule attribute if
for this
purpose:
$validator = $validation->validate(
[
'password' => '',
'confirmPassword' => ''
],
[
'password' => [
['notEmpty']
],
'confirmPassword' => [
['notEmpty', 'if' => ['withAll' => ['password']]]
]
]
);
In the example, the required error on
confirmPassword
will show ifpassword
is not empty.
You can use multiple conditions or combine them with complex rules:
$validator = $validation->validate(
[
'password' => 'abc',
'confirmPassword' => 'cde'
],
[
'password' => [
['notEmpty']
],
'confirmPassword' => [
['notEmpty', 'if' => ['withAll' => ['password']]],
['match', 'password', 'error' => 'Passwords do not match.']
]
]
);
There are two composition conditions: anyOf
and noneOf
, they contain nested conditions:
$validator = $validation->validate(
[
'password' => 'abc',
'confirmPassword' => 'cde'
],
[
'password' => [
['notEmpty']
],
'confirmPassword' => [
['notEmpty', 'if' => ['anyOf' => ['withAll' => ['password'], 'withoutAll' => ['otherField']]]],
[
'match',
'password',
'error' => 'Passwords do not match.',
'if' => ['noneOf' => ['some condition', 'another condition']]
]
]
]
);
Following conditions available for the usage:
Name | Options | Description |
---|---|---|
withAny | array | When at least one field is not empty. |
withoutAny | array | When at least one field is empty. |
withAll | array | When all fields are not empty. |
withoutAll | array | When all fields are empty. |
present | array | When all fields are presented in the request. |
absent | array | When all fields are absent in the request. |
noneOf | array | When none of nested conditions is met. |
anyOf | array | When any of nested conditions is met. |
You can create your conditions using
Spiral\Validation\ConditionInterface
.
The following validation rules are available.
You can create your own validation rules using
Spiral\Validation\AbstractChecker
orSpiral\Validation\CheckerInterface
.
The most used rule-set is available thought the set of shortcuts:
Alias | Rule |
---|---|
notEmpty | type::notEmpty |
required | type::notEmpty |
datetime | datetime::valid |
timezone | datetime::timezone |
bool | type::boolean |
boolean | type::boolean |
arrayOf | array::of, |
cardNumber | mixed::cardNumber |
regexp | string::regexp |
address::email | |
url | address::url |
file | file::exists |
uploaded | file::uploaded |
filesize | file::size |
image | image::valid |
array | is_array |
callable | is_callable |
double | is_double |
float | is_float |
int | is_int |
integer | is_integer |
numeric | is_numeric |
long | is_long |
null | is_null |
object | is_object |
real | is_real |
resource | is_resource |
scalar | is_scalar |
string | is_string |
match | mixed::match |
prefix
type::
Rule | Parameters | Description |
---|---|---|
notEmpty | asString:bool - true | Value should not be empty (same as !empty ). |
notNull | --- | Value should not be null. |
boolean | --- | Value has to be boolean or integer[0,1]. |
All of the rules of this checker are available without prefix.
Rule | Parameters | Description |
---|---|---|
notEmpty | asString:bool - true | Value should not be empty. |
Examples:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'name' => [
['notEmpty'],
['my::abc']
]
];
}
prefix
mixed::
Rule | Parameters | Description |
---|---|---|
cardNumber | --- | Check credit card passed by Luhn algorithm. |
match | field:string, strict:bool - false | Check if value matches value from another field. |
All of the rules of this checker are available without prefix.
prefix
address::
Rule | Parameters | Description |
---|---|---|
--- | Check if email is valid. | |
url | schemas:?array - null, defaultSchema:?string - null | Check if URL is valid. |
uri | --- | Check if URI is valid. |
url
rules are available withoutaddress
prefix via aliases, foruri
useaddress::uri
.
prefix
number::
Rule | Parameters | Description |
---|---|---|
range | begin:float, end:float | Check if the number is in a specified range. |
higher | limit:float | Check if the value is bigger or equal to that which is specified. |
lower | limit:float | Check if the value is smaller or equal to that which is specified. |
prefix
string::
Rule | Parameters | Description |
---|---|---|
regexp | expression:string | Check string using regexp. |
shorter | length:int | Check if string length is shorter or equal that specified value. |
longer | length:int | Check if the string length is longer or equal to that specified value. |
length | length:int | Check if the string length is equal to specified value. |
range | left:int, right:int | Check if the string length fits within the specified range. |
Examples:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'name' => [
['notEmpty'],
['string::length', 5]
]
];
}
prefix
file::
File checker fully supports the filename provided in a string form or using UploadedFileInterface
(PSR-7).
Rule | Parameters | Description |
---|---|---|
exists | --- | Check if file exist. |
uploaded | --- | Check if file was uploaded. |
size | size:int | Check if file size less that specified value in KB. |
extension | extensions:array | Check if file extension in whitelist. Client name of uploaded file will be used! |
prefix
image::
The image checker extends the file checker and fully supports its features.
Rule | Parameters | Description |
---|---|---|
type | types:array | Check if the image is within a list of allowed image types. |
valid | --- | Shortcut to check if the image has an allowed type (JPEG, PNG, and GIF are allowed). |
smaller | width:int, height:int | Check if image is smaller than a specified shape (height check if optional). |
bigger | width:int, height:int | Check if image is bigger than a specified shape (height check is optional). |
prefix
datetime::
This checker can apply now
value in the constructor
Rule | Parameters | Description |
---|---|---|
future | orNow:bool - false, useMicroSeconds:bool - false |
Value has to be a date in the future. |
past | orNow:bool - false, useMicroSeconds:bool - false |
Value has to be a date in the past. |
format | format:string | Value should match the specified date format |
before | field:string, orEquals:bool - false, useMicroSeconds:bool - false |
Value should come before a given threshold. |
after | field:string, orEquals:bool - false, useMicroSeconds:bool - false |
Value should come after a given threshold. |
valid | --- | Value has to be valid datetime definition including numeric timestamp. |
timezone | --- | Value has to be valid timezone. |
Setting
useMicroSeconds
into true allows to check datetime with microseconds.
Be careful, twonew \DateTime('now')
objects will 99% have different microseconds values so they will never be equal.
prefix
entity::
Cycle ORM specific checker.
Rule | Parameters | Description |
---|---|---|
exists | class:string, field:string - null | If an entity is presented in the db by a given PK or a custom field. class is an entity class name. |
unique | class:string, field:string, withFields:string[] | Value has to be unique. withFields represents an array of fields to be fetched from the validator input so all of them will be used in the unique check. |
Exists by PK example:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'id' => [
['entity::exists', \App\Database\User::class]
]
];
}
Checks if the user exists by a given PK
Exists by custom field example:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'email' => [
['entity::exists', \App\Database\User::class, 'email']
]
];
}
Checks if the user exists by a given email
You can pass an active entity as a context object. If the value is presented in the context then it is counted as unchanged, and the checker will return true, otherwise the checker will look into the database.
Example:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'email' => [
['entity::unique', \App\Database\User::class, 'email', ['company']]
]
];
}
It says that the given value should be unique in the database as an
company
value
With the validator context you can pass the current values so they will not conflict with the current entity in the database:
/** @var \App\Database\User $user */
$request->setContext($user);
Special composition checker to validate all array values:
class MyRequest extends \Spiral\Filters\Filter
{
public const VALIDATES = [
'emails' => [
['arrayOf', 'address::email']
]
];
}
It is possible to create application-specific validation rules via custom checker implementation.
namespace App\Security;
use Spiral\Database\Database;
use Spiral\Validation\AbstractChecker;
class DBChecker extends AbstractChecker
{
public const MESSAGES = [
'user' => 'No such user.'
];
/** @var Database */
private $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* @param $id
* @return bool
*/
public function user($id): bool
{
return $this->db->table('users')->select()->where('id', $id)->count() === 1;
}
}
Use prebuild constant
MESSAGES
to define a custom error template.
To activate checker, register it in ValidationBootloader
:
namespace App\Bootloader;
use App\Security\DBChecker;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Bootloader\Security\ValidationBootloader;
class CheckerBootloader extends Bootloader
{
public function boot(ValidationBootloader $validation)
{
$validation->addChecker('db', DBChecker::class);
}
}
You can use the validation now via db::user
rule.