This is a re-write of rakit/validation, a standalone validator like Laravel Validation. In keeping with rakit/validation, this library does not have any other dependencies for usage.
Please note that the internal API is substantially different to rakit/validation.
Jump to rules
Install using composer, or checkout / pull the files from github.com.
There are two ways for validating data with this library: using make
to make a validation object,
then validate it using validate
; or use validate
.
For example:
Using make
:
<?php
require('vendor/autoload.php');
use Somnambulist\Components\Validation\Factory;
$validation = (new Factory)->make($_POST + $_FILES, [
'name' => 'required',
'email' => 'required|email',
'password' => 'required|min:6',
'confirm_password' => 'required|same:password',
'avatar' => 'required|uploaded_file:0,500K,png,jpeg',
'skills' => 'array',
'skills.*.id' => 'required|numeric',
'skills.*.percentage' => 'required|numeric'
]);
$validation->validate();
if ($validation->fails()) {
// handling errors
$errors = $validation->errors();
echo "<pre>";
print_r($errors->firstOfAll());
echo "</pre>";
exit;
} else {
// validation passes
echo "Success!";
}
or via validate
:
<?php
require('vendor/autoload.php');
use Somnambulist\Components\Validation\Factory;
$validation = (new Factory)->validate($_POST + $_FILES, [
'name' => 'required',
'email' => 'required|email',
'password' => 'required|min:6',
'confirm_password' => 'required|same:password',
'avatar' => 'required|uploaded_file:0,500K,png,jpeg',
'skills' => 'array',
'skills.*.id' => 'required|numeric',
'skills.*.percentage' => 'required|numeric'
]);
if ($validation->fails()) {
// handling errors
$errors = $validation->errors();
echo "<pre>";
print_r($errors->firstOfAll());
echo "</pre>";
exit;
} else {
// validation passes
echo "Success!";
}
You are strongly advised to use a Dependency Injection container and store the
Factory
as a singleton instead of creating new instances. This will reduce the penalty for creating validation instances and allow custom rules to be more easily managed.
Unlike rakit/validation
, attribute names are not transformed in any way; instead, if you wish to name your
attributes, aliases must be used.
Aliases can be defined in several ways: on the rule itself, or by adding the alias to the validation. Note that
aliases should be set before calling validate
.
use Somnambulist\Components\Validation\Factory;
$validation = (new Factory)->make([
'province_id' => $_POST['province_id'],
'district_id' => $_POST['district_id']
], [
'province_id:Province' => 'required|numeric',
'district_id:District' => 'required|numeric'
]);
// or set the aliases:
$validation->setAlias('province_id', 'Province');
$validation->setAlias('district_id', 'District');
// then validate it
$validation->validate();
After validation, the data results are held in each validation instance. For example:
use Somnambulist\Components\Validation\Factory;
$validation = (new Factory())->validate([
'title' => 'Lorem Ipsum',
'body' => 'Lorem ipsum dolor sit amet ...',
'published' => null,
'something' => '-invalid-'
], [
'title' => 'required',
'body' => 'required',
'published' => 'default:1|required|in:0,1',
'something' => 'required|numeric'
]);
Now you can get the validated data, only the valid data, or only the invalid data:
$validatedData = $validation->getValidatedData();
// [
// 'title' => 'Lorem Ipsum',
// 'body' => 'Lorem ipsum dolor sit amet ...',
// 'published' => '1' // notice this
// 'something' => '-invalid-'
// ]
$validData = $validation->getValidData();
// [
// 'title' => 'Lorem Ipsum',
// 'body' => 'Lorem ipsum dolor sit amet ...',
// 'published' => '1'
// ]
$invalidData = $validation->getInvalidData();
// [
// 'something' => '-invalid-'
// ]
Click to show details.
Sometimes attributes can be left off or can be null. These cases should be handled carefully and have different results after validation.
For optional attributes that can be left out of the data under validation i.e. only validated if the data is present,
the sometimes
rule may be used. If this is specified, then that attribute can be left out completely OR it must meet
the validation criteria. This is very useful for things like search filters, or pagination markers that are not always
required:
[
'filters' => 'sometimes|array',
]
In this example filters
is entirely optional but if specified should be an array of values. Passing [filters => '']
would not be valid, it would have to be: [filters => []]
.
Sometimes instead of the attribute being optional, it should be undefined i.e. null
. Generally it is preferable to use
sometimes
and have the value omitted but there may be a case to maintain the attribute with a null
value. In these
instances use the nullable
rule. This will allow the attribute to be present without any value. For example: the
users birthday may be nullable or a date: nullable|date
.
Unlike rakit/validation
, the use of nullable data can cause issues as this library uses strict typing throughout.
This means that many rules that test for string, or array, or a number error because they receive null
. This is an
ambiguity in the rule definition process. For example the rule: name: string|max:200
as defined implicitly implies
that the name
should be a string and up to 200 characters - null
should not be valid, but to maintain partial
compatibility it will allow null.
The next major version of this library will remove this handling and make this type of definition require that the
field be both present and have a value that is not empty (unless empty is specifically allowed). To allow null values,
the nullable
rule will need to explicitly defined. As such it is good practice to always use nullable or sometimes.
This library can validate complex arrays of data by making use of dot notation to define the structure of the array. There are a couple of variations and some edge cases to be aware of to prevent issues.
The most common situation is wanting to allow an array of options similar to the examples earlier in this readme.
[
'skills' => 'array',
'skills.*.id' => 'required|numeric',
'skills.*.percentage' => 'required|numeric'
],
The earlier example rules are set to validate user related data and includes an array of skills. Each skill has an id
and a percentage value. In this case the parent key skills
should have the rule array
defined. This is needed to
ensure the data is actually an array. Each skill property is then referenced using *
to indicate there are multiple
values within the skills attribute.
These rules would validate the following array structure:
[
'skills' => [
[
'id' => 3,
'percentage' => 50,
],
[
'id' => 17,
'percentage' => 50,
],
]
]
The less common situation is an array of arrays without a parent key. In this case there is no prefix and each sub-key
starts with a *
. In this situation you should be careful not to mix standard key -> value pairs with the array data.
For example:
[
'*.id' => 'required|numeric',
'*.percentage' => 'required|numeric'
]
would be used to validate the following array structure:
[
[
'id' => 3,
'percentage' => 50,
],
[
'id' => 17,
'percentage' => 50,
],
]
To avoid problems you would need to ensure that the data would not include:
[
'name' => 'foo bar',
[
'id' => 3,
'percentage' => 50,
],
[
'id' => 17,
'percentage' => 50,
],
]
Some rules are used to determine the presence or to be required if certain keys are present. Usually these use the
standard key name e.g.: confirm_password
should be the same as the password
field, so the rule is written as:
same:password
.
However: for array data this will not work as the attribute is not the name of the attribute but the path for that attribute.
Using the same skills array as an example, say we wanted to require a label if the skill is new. If this was specified
as required_if:id:null
, then the validation would look for an attribute named id
in the root of the data - but it
does not exist, or it may find the wrong key.
Instead: we have to explicitly bind the rule to the same skill key by writing the rule as: required_if:skills.*.id,null
.
If we don't do this, then the rule will be ignored or fail. The same applies when using array of arrays: referencing
other fields within that array should be prefixed with a *.
e.g. required_if:*.id,null
.
Here are examples of both syntaxes:
[
'skills.*.id' => 'sometimes|numeric',
'skills.*.percentage' => 'required|numeric',
'skills.*.title' => 'required_if:skills.*.id,null|string',
]
And array of arrays:
[
'*.id' => 'sometimes|numeric',
'*.percentage' => 'required|numeric',
'*.title' => 'required_if:*.id,null|string',
]
Validation messages are defined in Resources/i18n/en.php
. Any message can be replaced with a custom
string, or translated to another language. The English strings are always loaded during Factory
instantiation.
Depending on the failure type, various variables will be available to use, however, the following are always available for all messages:
:attribute
: the attribute under validation, alias will be used if set,:value
: the value of the attribute under validation, converted to string with arrays and objects as JSON strings.By default, only the English messages are loaded by the Factory
class. At the time of writing a German translation
has been provided by contributors, however any language can be added by creating a PHP file that returns an array
of strings with the message keys and the new messages.
To load a built-in language, you must call Factor::registerLanguageMessages()
before calling validate. For example:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->registerLanguageMessages('de');
registerLanguageMessages
has a second, optional, argument that allows the path to the language file to be
specified. If not provided, then the library path of <vendor_dir>/src/Resources/i18n
will be used. If you wish
to use a completely customised language file, then use the second argument to provide your file. This can be
an English language file to fully override the default messages.
For example:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->registerLanguageMessages('en', '/path/to/project/i18n/en_US.php');
You can make multiple calls to add multiple languages:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->registerLanguageMessages('en', '/path/to/project/i18n/en_US.php');
$factory->registerLanguageMessages('es', '/path/to/project/i18n/es.php');
$factory->registerLanguageMessages('de', '/path/to/project/i18n/de.php');
All messages are stored in a MessageBag
on the Factory
instance. Additional languages can be added to this
message bag, or customised on the specific validation instance. Additionally, the default language can be set
on the message bag on the Factory, or a specific language set on the validation instance.
To add a new set of messages:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->messages()->add('es', [
'rule.required' => 'Se requiere :attribute',
]);
$validation = $factory->validate($inputs, $rules);
$validation->setLanguage('es')->validate();
Or override the default English strings:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->messages()->replace('en', 'rule.required', 'Se requiere :attribute');
$validation = $factory->validate($inputs, $rules);
$validation->validate();
Or set the default language:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->messages()->default('es');
$validation = $factory->validate($inputs, $rules);
$validation->validate();
Sometimes you may want to set custom messages for specific attribute rules to make them more
explicit or to add other information. This is done by adding a message key for the attribute
with a :
and the rule name.
For example:
use Somnambulist\Components\Validation\Factory;
$validator = new Factory();
$validation_a = $validator->make($input, [
'age' => 'required|min:18'
]);
$validation->messages()->add('en', 'age:min', '18+ only');
$validation->validate();
Sometimes you may wish to use parameters from other rules in your error messages. From version 1.6.0 you can access these using dot notation for the rule name and then the parameter you wish to use. For example:
A password
attribute is validated using required|between:8,16|regex:/^[\\da-zA-Z!$%+.]+$/
but the
error messages want to always reference the min/max values. This can be done as:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->messages()->replace('en', 'password:between', 'Your password must be between :min and :max characters and only [! $ % + .] as special characters.');
$factory->messages()->replace('en', 'password:regex', 'Your password must be between :between.min and :between.max characters and only [! $ % + .] as special characters.');
For the regex
message, the parameters from between
are referenced by prefixing the min/max with between.
.
Not all rules have parameters, in these instances there will be no replacement made.
Note that only rule parameters for the same attribute can be referenced. You cannot access parameters from a completely different attribute e.g.: if you validated email or username, you would not be able to access those parameters in the password context.
Some rules have several possible validation messages. These are all named as rule.<name>.<check>
. To change
the message, override or add the specific message.
For example, uploaded_file
can have failures for the file, min/max size and type. These are bound to:
To change any of the sub-messages, add/override that message key on the message bag.
For example:
use Somnambulist\Components\Validation\Factory;
$validator = new Factory();
$validation_a = $validator->make($input, [
'age' => 'required|min:18'
]);
$validation->messages()->add('en', 'age:min', '18+ only');
$validation->validate();
Unlike
rakit
, it is not possible to set custom messages in theRule
instances directly. Any message must be set in the message bag.
The system for translations in this library is rather basic. If you have complex needs, or wish to handle
countables etc. Then all error messages are stored as ErrorMessage
instances containing the message key
and the variables for that message.
Instead of using the ErrorBag
to display messages, you can use the underlying array (or a DataBag
instance)
and then pass the message keys to your translation system along with the variables.
Note that errors are a nested set by attribute and rule name.
Error messages are collected in an ErrorBag
instance that you can access via errors()
on the validation
instance.
use Somnambulist\Components\Validation\Factory;
$validation = (new Factory())->validate($inputs, $rules);
$errors = $validation->errors();
Now you can use the following methods to retrieve the messages:
all(string $format = ':message')
Get all messages as a flattened array:
$messages = $errors->all();
// [
// 'email is not a valid email address',
// 'password minimum is 6 characters',
// 'password must contain capital letters'
// ]
$messages = $errors->all('<li>:message</li>');
// [
// '<li>email is not a valid email address</li>',
// '<li>password minimum is 6 character</li>',
// '<li>password must contain capital letters</li>'
// ]
firstOfAll(string $format = ':message', bool $dotNotation = false)
Get only the first message from all existing keys:
$messages = $errors->firstOfAll();
// [
// 'email' => 'Email is not valid email',
// 'password' => 'Password minimum 6 character',
// ]
$messages = $errors->firstOfAll('<li>:message</li>');
// [
// 'email' => '<li>Email is not valid email</li>',
// 'password' => '<li>Password minimum 6 character</li>',
// ]
Argument $dotNotation
is for array validation. If it is false
it will return the original array structure,
if it is true
it will return a flattened array with dot notation keys.
For example:
$messages = $errors->firstOfAll(':message', false);
// [
// 'contacts' => [
// 1 => [
// 'email' => 'Email is not valid email',
// 'phone' => 'Phone is not valid phone number'
// ],
// ],
// ]
$messages = $errors->firstOfAll(':message', true);
// [
// 'contacts.1.email' => 'Email is not valid email',
// 'contacts.1.phone' => 'Email is not valid phone number',
// ]
first(string $key)
Get the first message for the given key. It will return a string
if key has any error message, or null
if the key has no errors.
For example:
if ($emailError = $errors->first('email')) {
echo $emailError;
}
toArray()
Get the raw underlying associative array of ErrorMessage objects.
For example:
$messages = $errors->toArray();
// [
// 'email' => [
// 'email' => 'Email is not valid email'
// ],
// 'password' => [
// 'min' => 'Password minimum 6 character',
// 'regex' => Password must contains capital letters'
// ]
// ]
toDataBag()
Get the raw underlying associative array of ErrorMessage objects as a DataBag
instance.
For example:
$message = $errors->toDataBag()->filter()->first();
count()
Get the number of error messages.
has(string $key)
Check if the given key has an error. It returns true
if a key has an error, and false
otherwise.
By default, all built-in rules are registered automatically to the Factory
instance. Some of these
are required internally (e.g. required
and callback
); however you can override or add any number
of new rules to the factory to use for your validations.
This is done by accessing the addRule()
method on the Factory
and adding a new rule instance.
For example, you want to create the unique
validator that will check field availability in a database.
First, lets create UniqueRule
class:
<?php declare(strict_types=1);
use Somnambulist\Components\Validation\Rule;
class UniqueRule extends Rule
{
protected string $message = ":attribute :value has been used";
protected array $fillableParams = ['table', 'column', 'except'];
protected PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function check($value): bool
{
// make sure required parameters exists
$this->assertHasRequiredParameters(['table', 'column']);
// getting parameters
$column = $this->parameter('column');
$table = $this->parameter('table');
$except = $this->parameter('except');
if ($except && $except == $value) {
return true;
}
// do query
$stmt = $this->pdo->prepare(sprintf('select count(*) as count from %s where %s = :value', $table, $column));
$stmt->bindParam(':value', $value);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
// true for valid, false for invalid
return intval($data['count']) === 0;
}
}
Now to register this rule it needs adding to the Factory
instance:
use Somnambulist\Components\Validation\Factory;
$factory = new Factory();
$factory->addRule('unique', new UniqueRule($pdo));
Now you can use it like this:
$validation = $factory->validate($_POST, [
'email' => 'email|unique:users,email,exception@mail.com'
]);
In the UniqueRule
above, the property $message
is used for the invalid message. The property
$fillableParams
defines the order and names of the arguments for the rule. By default,
fillParameters
will fill parameters listed in $fillableParams
from the string rules.
For example, unique:users,email,exception@mail.com
in example above, will set:
$params['table'] = 'users';
$params['column'] = 'email';
$params['except'] = 'exception@mail.com';
If you want your custom rule to accept parameter lists like
in
,not_in
, oruploaded_file
rules, you need to override thefillParameters(array $params)
method in your custom rule class.
Note that the unique
rule that we created above also can be used like this:
$validation = $factory->validate($_POST, [
'email' => [
'required', 'email',
$factory('unique', 'users', 'email')
]
]);
You can improve UniqueRule
class above by adding some methods to set the params instead of using
the string format:
<?php
class UniqueRule extends Rule
{
public function table(string $table): self
{
$this->params['table'] = $table;
return $this;
}
public function column(string $column): self
{
$this->params['column'] = $column;
return $this;
}
public function except(string $value): self
{
$this->params['except'] = $value;
return $this;
}
}
Now configuring the rule becomes:
$validation = $factory->validate($_POST, [
'email' => [
'required', 'email',
$validator('unique')->table('users')->column('email')->except('exception@mail.com')
]
]);
An implicit rule is a rule that if it's invalid, the next rules will be ignored. For example, if
the attribute didn't pass required*
rules, the next rules will be invalid. To prevent unnecessary
validation and error messages, we make required*
rules to be implicit.
To make your custom rule implicit, you can make $implicit
property value to be true
. For example:
<?php
use Somnambulist\Components\Validation\Rule;
class YourCustomRule extends Rule
{
protected bool $implicit = true;
}
In some cases, you may want your custom rule to be able to modify the attribute value like the
default/defaults
rule. In the current and next rule checks, your modified value will be used.
To do this, you should implement Somnambulist\Components\Validation\Rules\Contracts\ModifyValue
and create the method modifyValue(mixed $value)
on your custom rule class.
For example:
<?php
use Somnambulist\Components\Validation\Rule;
use Somnambulist\Components\Validation\Rules\Contracts\ModifyValue;
class YourCustomRule extends Rule implements ModifyValue
{
public function modifyValue(mixed $value): mixed
{
// Do something with $value
return $value;
}
}
You may want to do some preparation before running the validation. For example, the
uploaded_file
rule will resolve the attribute value that comes from $_FILES
(undesirable) array structure to be a well-organized array.
To do this, you should implement Somnambulist\Components\Validation\Rules\Contracts\BeforeValidate
and create the method beforeValidate()
on your custom rule class.
For example:
<?php
use Somnambulist\Components\Validation\Rule;
use Somnambulist\Components\Validation\Rules\Contracts\BeforeValidate;
class YourCustomRule extends Rule implements BeforeValidate
{
public function beforeValidate(): void
{
$attribute = $this->getAttribute();
$validation = $this->validation;
// Do something with $attribute and $validation
// For example change attribute value
$validation->setValue($attribute->getKey(), "your custom value");
}
}
PHPUnit 9+ is used for testing. Run tests via vendor/bin/phpunit
.
Contributions are welcome! Fork the repository and make a PR back. Please ensure that your code is formatted
using PSR-12 coding standards, and all PHP files include declare(strict_types=1);
on the opening <?php
tag.
If in doubt about any code-style convention, look at the existing files and follow along.
This library currently targets PHP 8.0.X and 8.1+. If using 8.1 functions, ensure a suitable fallback is used. Note that external libraries should not be added to this project.
If adding new functionality ensure the README.md
file is updated with your changes and include appropriate
tests and if possible, language translations with English as the primary requirement.
For bug fixes a failing case must be included in a test. Changes without appropriate tests or that cannot be replicated may be rejected.