Image taken from Michiana Stransportation ( Bike Shops )
If you still do not know what Kontrolio is, I suggest reading the first part - “ Keep your data under control ”. In short, this is my data validation library written in PHP.
In the previous article, I promised to write a comparison of my own library with other existing solutions, so today we will look at validation using Aura.Filter , Respect Validation , Sirius Validation and Valitron .
Let's imagine that we have in the development of a public service, which involves registering users for full access to all functions. Thus, the registration form will contain the following fields:
So, we have five fields that the user must fill in to register with our imaginary service. Let's imagine that we received completely invalid data at the entrance:
$data = [ 'name' => '', // 'login' => '@lbert', // "" @ 'email' => '- ', // e-mail 'password' => '' // // 'agreed' , ];
Validation using Aura.Filter starts with a filter factory. We need to create a so-called "subject filter", since we will validate the array, not the individual value.
use Aura\Filter\FilterFactory; $filter = (new FilterFactory)->newSubjectFilter(); $filter->validate('name') ->isNotBlank() ->is('two_words') ->setMessage(' .'); $filter->validate('login') ->isBlankOr('alnum') ->setMessage(' , .'); $filter->validate('email') ->isNotBlank() ->is('email') ->setMessage(', . .'); $filter->validate('password') ->isNotBlank() ->is('strlenMax', 64) ->setMessage(', .'); $filter->validate('agreed') ->is('callback', function($subject, $field) { return $subject->{$field} === true; })->setMessage(' .');
As you can see, the description of the rules is quite simple. Aura.Filter provides a whole set of useful rules out of the box and some of them were used in the example above:
is
method.agreed
.You two_words
noticed that I didn’t have a two_words
rule. Naturally, in Aura.Filter there is no such rule, so we need to create it. As the documentation says, this is done using a separate class for the rule:
/** * , . * : , . */ class UserNameRule { /** * . * * @param object|array $subject * @param string $field * @param int $max * * @return bool */ public function __invoke($subject, $field, $max = null) { $value = $subject->{$field}; if ( ! is_scalar($value)) { return false; } return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $value); } }
Our second step. This is a list of rules for the filter factory:
The next step is to notify Aura.Filter that we have created a new rule and want to use it. This is done by passing the array of rules to the first argument of the factory:
use Aura\Filter\FilterFactory; $rules = [ 'two_words' => function() { return new UserNameRule; } ]; $filter = (new FilterFactory($rules))->newSubjectFilter();
Now our two_words
rule can be used just like any other rule from the standard delivery.
As you remember, the incoming data that we validate is completely invalid, because each field contains an incorrect value or does not contain it at all. Therefore, it is assumed that as a result of validation, we will receive errors and corresponding messages about them.
Validating with Aura.Filter we are as follows:
$valid = $filter->apply($data); if ( ! $valid) { $failures = $filter->getFailures(); $messages = $failures->getMessages(); }
An array is written to $ messages , so to display messages we need two foreach nested:
<ul class="errors"> <?php foreach ($messages as $field => $errors) { foreach ($errors as $error) { printf('<li class="error">%s</li>', $error); } } ?> </ul>
The second library I used in comparison is a relatively popular solution called Respect Validation . Since people trust her, I think there is something to see.
For the purity of the experiment, when comparing libraries, we will use the same data set, defined at the beginning:
use Respect\Validation\Validator as v; $data = [ 'name' => '', // 'login' => '@lbert', // "" @ 'email' => '- ', // e-mail 'password' => '' // // 'agreed' , ];
As in the case of Aura.Filter, we need our own validation rule for the username, so let's start with it:
namespace MyNamespace; use Respect\Validation\Rules\AbstractRule; class UserNameRule extends AbstractRule { public function validate($input) { return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $input); } }
The external rules API is almost identical to Aura.Filter, only the validate () method is used instead of __invoke () magic . It, this API, seemed to me simpler and more understandable. Well, it's closer to Kontrolio :)
I did not find any mention of this in the documentation; nevertheless, besides the rule itself, it is necessary to create your own type of exception. The class name of the exception must consist of the class name of the rule and the postfix Exception .
use Respect\Validation\Exceptions\NestedValidationException; class UserNameRuleException extends NestedValidationException { // }
Well, finally, we can validate our data. For a start, we pass the validator our new rule so that he learns about it, and we can use it in the future. In Respect Validation, this is done by calling the with () method with the transfer of the namespace, which contains non-standard rules.
v::with('MyNamespace\\');
Now all non-standard rules that are in the MyNamespace namespace will be “recognized” by the validator. The next step is to describe the necessary rules and perform validation.
v::attribute('name', v::userNameRule()) ->attribute('login', v::alnum('-_')) ->attribute('email', v::email()) ->attribute('password', v::notEmpty()->stringType()->length(null, 64)) ->attribute('agreed', v::trueVal()) ->assert((object) $data);
Notice how we apply our rule to the name attribute. Here the class name of the rule was transformed into the name of the validator method. The rest of the rules, in general, are intuitive.
Separately, it is worth mentioning why we are assigning the $ data array to the object. The point is that Respect Validation accepts objects as input, not arrays. This should be considered when developing using this library.
Unlike Aura.Filter, the Respect validator throws an exception when validation fails. And this exception contains validation error messages. Therefore, the example that has just been shown should be written as follows:
try { v::with('RespectValidationExample\\'); v::attribute('name', v::userNameRule()) ->attribute('login', v::alnum('-_')) ->attribute('email', v::email()) ->attribute('password', v::notEmpty()->stringType()->length(null, 64)) ->attribute('agreed', v::trueVal()) ->assert((object) $data); } catch (NestedValidationException $ex) { $messages = $ex->getMessages(); }
Using getMessages () , we get a flat array of all messages that the validator collected during the validation process. Given the array, we get something like this:
array(5) { [0] => string(29) “Data validation failed for %s” [1] => string(60) “login must contain only letters (az), digits (0–9) and “-_”” [2] => string(25) “email must be valid email” [3] => string(26) “password must not be empty” [4] => string(32) “Attribute agreed must be present” }
You can change the messages on your own. Maybe I somehow misunderstood this library, but this process did not seem so obvious to me: you need to use the findMessages () method on the processed exception, in which you define messages not for attributes, but for rules.
$ex->findMessages([ 'userNameRule' => ' .', 'alnum' => ' .', 'email' => ' e-mail.', 'notEmpty' => ' ?', 'agreed' => ', .' ]);
I do not know what the error is, but there are a couple of things that I did not understand. Here is what we get by defining the rules in the above way:
array(5) { [0] => string(40) “ .” [1] => string(31) “ .” [2] => string(25) “email must be valid email” [3] => string(5) “ ?” [4] => string(9) “, .” }
As you can see, the message for the email field did not apply, the standard remained. But the message for the index 4 is the opposite! And this is despite the fact that I used not the name of the rule, but the name of the field. While if I used the rule name (trueVal), my message would get lost somewhere. Comments from experienced users of this library are very welcome.
Ok, let's move on to the next library and see how it handles similar tasks.
Again, we need to define a rule for the username. We will write it something like this:
class UserNameRule extends AbstractRule { // const MESSAGE = ' .'; const LABELED_MESSAGE = '{label} .'; public function validate($value, $valueIdentifier = null) { return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $value); } }
Pay attention to the difference in approaches in comparison with the already reviewed libraries. We define two kinds of messages in constants, rather than using properties, methods, or rule arguments.
Now let's describe the validation logic:
$validator = new Validator; $validator ->add('name', 'required | MyApp\Validation\Rule\UserNameRule') ->add('login', 'required | alphanumhyphen', null, ' , .') ->add('email', 'required | email', null, ', e-mail.') ->add('password', 'required | maxlength(64)', null, ' , .') ->add('agree', 'required | equal(true)', null, ' ?');
As you can see, the set of rules is quite simple and readable. For the description we use names separated by horizontal dashes. This approach is similar to the one used in Laravel and Kontrolio.
The fourth argument to the add () method describes the validation error message that Sirius uses if validation fails. And why we did not add the message for our new rule UserNameRule ?
$validator->add('name', 'required | MyApp\Validation\Rule\UserNameRule')
So this is because the messages are already described in class constants:
class UserNameRule extends AbstractRule { // const MESSAGE = ' .'; ...
Another option is to use the validator’s addMessage () method:
$validator->addMessage('email', ', e-mail.');
Please note that custom rules are identified by the full name of their class, while in Kontrolio you can specify an alias / alias.
To perform a validation, we call the validate () validator method, passing data to it:
$data = [ 'name' => '', // 'login' => '@lbert', // "" @ 'email' => '- ', // e-mail 'password' => '' // // 'agreed' , ]; $validator->validate($data);
Unlike Respect, Sirius does not throw an exception, but simply returns false . Validation error messages can be obtained through the getMessages () validator method. It returns errors grouped by attributes, so for passing by errors we need two foreach cycles:
foreach ($validator->getMessages() as $attribute => $messages) { foreach ($messages as $message) { echo $message->getTemplate() . "\n"; } }
Here, $ message is an object of the Sirius \ Validation \ ErrorMessage class , which has a getTemplate () method that returns the message we need.
Let's go further. Another interesting solution is Valitron . Valitron differs from the rest in the implementation of the addition and description of validation rules.
The first difference: to add a new rule, you do not need to create a separate class. You can simply use a closure that returns a boolean result.
To add custom rules to Valitron, there is a static method addRule () , in which the first two arguments are required, and the third is optional. I liked this method, since here immediately in one place indicates the identifier of the rule, logic and error message.
use Valitron\Validator; Validator::addRule('two_words', function($field, $value) { return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $value); }, ' .');
The second difference is how the rules apply to attributes. In all previous cases, we have seen that the attribute is a kind of primary thing.
In Valitron, we went the other way and put the validation rules in the first place. When describing rules, you seem to apply attributes to these rules, and not vice versa.
$validator = new Validator($data); $validator ->rule('two_words', 'name')->label('') ->rule('required', [ 'name', 'login', 'email', 'password', 'agreed' ]) ->rule('slug', 'login') ->rule('email', 'email') ->rule('accepted', 'agreed');
As you can see from the example, in the rule () method, we first write the name of the rule, and then we specify the attributes that must comply with this rule. A more explicit example is the required
rule, where it is shown how the attributes “belong” to this rule.
Valitron (like other solutions we’ve seen) provides standard error messages. If you simply use them, you will see that each message begins with the name of the corresponding attribute.
Valitron substitutes attribute names in the message text even when using non-standard error messages. Therefore, we used the label () method with an empty string to remove the attribute name.
$validator->rule('two_words', 'name')->label('')
Specifically with regard to validation, the Valitron library API is practically no different from what we have already seen in the article. To validate, we call the validate () validator method:
$validator->validate();
You can get validation error messages using the getErrors () method:
$validator->errors();
Messages here are grouped by attributes in the same way as in Sirius Validation, except for the fact that there is no separate class for the message, and we get a regular multidimensional array.
foreach ($validator->errors() as $attribute => $messages) { foreach ($messages as $message) { echo $message . "\n"; } }
Finally, the latest library for today is my own development called Kontrolio .
Again, for the fifth time, we will create a validation rule for the username. Everything is relatively simple and standard:
namespace MyProject\Validation\Rules; use Kontrolio\Rules\AbstractRule; class TwoWords extends Kontrolio\Rules\AbstractRule { public function isValid($input = null) { return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $input); } }
Now we create a factory and register the rule in it using the extend () method:
namespace MyProject; use Kontrolio\Factory; use MyProject\Validation\Rules\TwoWords; $factory = Kontrolio\Factory::getInstance()->extend([TwoWords::class]);
After registering the rule, we can use it, including by the name - two_words
. Let's create a validator:
$data = [ 'name' => '', // 'login' => '@lbert', // "" @ 'email' => '- ', // e-mail 'password' => '' // // 'agreed' , ]; $rules = [ 'name' => 'two_words', 'login' => 'sometimes|alphadash', 'email' => 'email', 'password' => 'length:1,64', 'agreed' => 'accepted' ]; $messages = [ 'name' => ' .', 'login' => ' .', 'email' => ' e-mail.', 'password' => ' ?', 'agreed' => ', .' ]; $validator = $factory->make($data, $rules, $messages);
We described the rules using a syntax similar to that used in Laravel, although we could use a more verbose option:
$rules = [ 'name' => new TwoWords, 'login' => [new Sometimes, new Alphadash], 'email' => new Email, 'password' => new Length(1, 64), 'agreed' => new Accepted ];
Validation is started by the same validate () method:
$validator->validate();
Now we can get error messages using one of the getErrors () or getErrorsList () methods. The first method allows you to make a more complex error output, while the second returns a flat array. Using getErrors () we can display messages like this:
<ul class="errors"> <?php foreach ($errors as $attribute => $messages): ?> <li class="errors__attribute"> <b><?= $attribute; ?></b> <ul> <?php foreach ($messages as $message): ?> <li><?= $message; ?></li> <?php endforeach; ?> </ul> </li> <?php endforeach; ?> </ul>
And with getErrorsList () you can make a simpler list of messages:
<?php $errors = $validator->getErrorsList(); ?> <ul class="errors"> <?php foreach($errors as $error): ?> <li class="errors__error"><?= $error; ?></li> <?php endforeach; ?> </ul>
In this article, I showed examples of using the following libraries:
The “real world example” may seem too simple. I have to agree, since, indeed, some of the possibilities of libraries were left out of the article. In principle, if you find this interesting, you can study their features on your own.
Each library offers its own chips, has its dark sides, so I think that this is a matter of taste and the task - to choose the very one.
Thanks for reading. Make the right choice.
Source: https://habr.com/ru/post/308298/
All Articles