Валидировали, валидировали... і вывалидировали! Порівнюємо валідатори даних в PHP


Зображення взято з сайту Michiana Stransportation (Bike Shops)
Якщо ви ще не в курсі, що таке Kontrolio, пропоную прочитати першу частину — «Тримайте свої дані під контролем». Коротенько, це моя бібліотека для валідації даних, написана на PHP.
У попередній статті я обіцяв написати порівняння своєї власної бібліотеки з іншими наявними рішеннями, так що сьогодні ми розглянемо валідацію з допомогою Aura.Filter, Respect Validation, Sirius Validation і Valitron.
Давайте уявимо, що у нас в розробці є якийсь публічний сервіс, який передбачає реєстрацію користувачів для повного доступу до всіх функцій. Таким чином, форма реєстрації буде містити такі поля:
  1. name. Має містити рівно два слова, де перша — це ім'я користувача, а друге — прізвище.
  2. login. Якщо значення передано, то воно має тільки латинські літери, тире та нижнє підкреслення.
  3. email. Має містити валідний адресу електронної пошти.
  4. password. Має бути встановлений і мати довжину не більше 64 символів.
  5. agreed. Типовий параметр, який користувач повинен встановити, щоб підтвердити свою згоду з умовами сервісу.
Отже, у нас п'ять полів, які користувач повинен заповнити, щоб зареєструватися в нашому уявному сервісі. Давайте уявимо, що нам на вхід надійшли повністю невалідні дані:
$data = [
'name' => 'Альберт', // Повинно бути два слова
'login' => '@lbert', // "Заборонений" символ @
'email' => 'не', // Тут повинен бути e-mail
'password' => " // Пароль взагалі не вказаний
// 'agreed' немає в масиві, тому що користувач не встановив прапорець
];

Aura.Filter
Валідація з використанням Aura.Filter починається з фабрики фільтрів. Нам необхідно створити так званий «фільтр суб'єкта», так як ми буде валідувати масив, а не індивідуальне значення.
Визначаємо правила
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('Вам необхідно погодитися з умовами сервісу.');

Як бачите, опис правил досить просте. Aura.Filter надає цілий набір корисних правил «з коробки» і деякі з них були використані в прикладі вище:
  1. метод isNotBlank. Вказує, що поле не може мати пусте значення.
  2. alnum. Це правило допускає тільки латинські літери.
  3. email. І так зрозуміло :)
  4. strlenMax. Вказує, що поле не може перевищувати довжину, зазначену другим аргументом методу
    is
    .
  5. callback. Цей тип правила схожий на замикання з Kontrolio. Він дозволяє визначити правило у вигляді замикання. В цей замикання Aura.Filter передає «суб'єкт», наш масив даних з форми, і поле, в даному випадку
    agreed
    .
Напевно ви помітили, що я не вказав правило
two_words
. Природно, в Aura.Filter такого правила немає, тому нам необхідно його створити. Як свідчить документація, це робиться з допомогою окремого класу для правила:
/**
* Правило, яке валидирует ім'я користувача.
* Ім'я користувача складається з двох слів: імені та прізвища, відокремлених одним пропуском.
*/
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);
}
}

The second step is to let the filter factory know about our new rule. It's done by passing the first argument as an array of rules to the filter factory:
Наступний крок — повідомити Aura.Filter про те, що ми створили нове правило і хочемо його використовувати. Це робиться з допомогою передачі масиву правил у перший аргумент фабрики:
use Aura\Filter\FilterFactory;

$rules = [
'two_words' => function() {
return new UserNameRule;
}
];

$filter = (new FilterFactory($rules))->newSubjectFilter();

Тепер наше правило
two_words
може бути використана так само, як і будь-яке інше правило зі стандартної поставки.
Зворотний зв'язок
Як ви пам'ятаєте, вхідні дані, які ми валидируем, — повністю невалидны, тому що кожне поле містить некоректне значення або не містять його зовсім. Тому передбачається, що в результаті валідації ми отримаємо помилки і відповідні повідомлення про них.
Валидируем з Aura.Filter ми наступним чином:
$valid = $filter->apply($data);

if ( ! $valid) {
$failures = $filter->getFailures();
$messages = $failures->getMessages();
}

$messages записується масив, тому для виведення повідомлень нам буде потрібно два вкладених foreach:
<ul class="errors">
<?php
foreach ($messages as $field => $errors) {
foreach ($errors as $error) {
printf('<li class="error">%s</li>', $error);
}
}
?>
</ul>

Respect Validation
Друга бібліотека, використана мною в порівнянні, — щодо популярне рішення під назвою Respect Validation. Раз люди їй довіряють, думаю, там є що подивитися.
Для чистоти експерименту при порівнянні бібліотек ми будемо використовувати один і той же набір даних, визначений на початку:
use Respect\Validation\Validator as v;

$data = [
'name' => 'Альберт', // Повинно бути два слова
'login' => '@lbert', // "Заборонений" символ @
'email' => 'не', // Тут повинен бути e-mail
'password' => " // Пароль взагалі не вказаний
// 'agreed' немає в масиві, тому що користувач не встановив прапорець
];

Визначаємо правила
Як і у випадку з Aura.Filter, нам необхідно власне правило валідації для імені користувача, тому давайте з нього і почнемо:
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);
}
}

Зовнішнє API правил практично ідентично Aura.Filter, тільки використовується метод validate() замість магії __invoke(). Мені воно, це API, здалося більш простим і зрозумілим. Ну, і до Kontrolio воно ближче :)
У документації я не знайшов про це згадки, тим не менш крім самого правила для нього необхідно створити власний тип винятку. Назва класу виключення повинно складатися з імені класу правила і постфикса Exception.
use Respect\Validation\Exceptions\NestedValidationException;

class UserNameRuleException extends NestedValidationException
{
//
}

Ну і нарешті-то ми можемо провалидировать наші дані. Для початку ми передаємо валідатору наше нове правило, щоб він дізнався про нього, і ми змогли його використовувати у подальшому. В Respect Validation це робиться викликом методу with() з передачею простору імен, в якому знаходяться нестандартні правила.
v::with('MyNamespace\\');

Тепер всі нестандартні правила, що знаходяться в просторі імен MyNamespace, будуть «упізнані» валідатором. Наступний крок — описати необхідні правила і виконати валідацію.
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);

Зверніть увагу на те, як ми застосовуємо наш правило до атрибуту name. Тут назва класу правило трансформувалася у назву методу валідатора. Інші правила, загалом-то інтуїтивно зрозумілі.
Окремо варто сказати про те, навіщо ми наводимо масив $data до об'єкта. Справа в тому, що Respect Validation приймає на вхід об'єкти, а не масиви. Це слід врахувати при розробці з використанням даної бібліотеки.
Зворотний зв'язок
На відміну від Aura.Filter валідатор Respect викидає виключення, коли валідація провалена. І в цьому виключення містяться повідомлення про помилки валідації. Тому приклад, який тільки що був показаний, повинен бути записаний наступним чином:
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();
}

Використовуючи getMessages(), ми отримаємо плоский масив повідомлень, який валідатор зібрав у процесі валідації. Задампив масив, ми отримаємо приблизно такий результат:
array(5) {
[0] => string(29) "Data validation failed for %s"
[1] => string(60) "login must contain only letters (a-z), 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"
}

Можна поміняти повідомлення на свої власні. Можливо, я щось не так зрозумів цю бібліотеку, але мені цей процес не здавався таким вже очевидним: необхідно використовувати метод findMessages() на обробленому виключення, в якому ви визначаєте повідомлення не для атрибутів, а для правил.
$ex->findMessages([
'userNameRule' => 'Ім'я користувача повинне складатися з двох слів.',
'alnum' => 'Ваш логін нам не подобається.',
'email' => 'Ви явно не хочете давати нам свій e-mail.',
'notEmpty' => 'Ну і де ж ваш пароль?',
'agreed' => 'Шкода, що ви не згодні.'
]);

Не знаю, в чому помилка, але є пара речей, які я так і не зрозумів. Ось що ми отримаємо, визначивши правила вищевказаним способом:
array(5) {
[0] = > string(40) "Ім'я користувача повинне складатися з двох слів."
[1] => string(31) "Ваш логін нам не подобається."
[2] => string(25) "email must be valid email"
[3] = > string(5) "Ну і де ж ваш пароль?"
[4] = > string(9) "Шкода, що ви не згодні."
}

Як бачите, повідомлення для поля електронної пошти не применилось, залишилося стандартне. А ось повідомлення за індексом 4 навпаки! І це при тому, що я не використовував назву правила, а назва поля. У той час як якщо б я використав назву правила (trueVal), моє повідомлення би кудись загубилася. Коментарі досвідчених користувачів цієї бібліотеки дуже вітаються.
Sirius Validation
Гаразд, давайте перейдемо до наступної бібліотеці та подивимося, як вона впорається зі схожими завданнями.
Визначаємо правила
І знову нам необхідно визначити правило для імені користувача. Ми його напишемо якось так:
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);
}
}

Зверніть увагу на різницю в підходах в порівнянні з вже розглянутими бібліотеками. Ми визначаємо два види повідомлень в константах, ніж використовуючи властивості, методи чи аргументи правила.
Тепер давайте опишемо логіку валідації:
$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, 'Чому ж ви не погодилися?');

Як бачите, набір правил вельми простий і читабельний. Для опису ми використовуємо назви, розділені горизонтальними рисками. Цей підхід схожий на той, що використовується в Laravel і Kontrolio.
Четвертий аргумент методу add() описує повідомлення про помилку валідації, яке Sirius використовує, якщо валідація буде провалена. А чому ж ми не додали повідомлення для нашого нового правила UserNameRule?
$validator->add('name', 'required | MyApp\Validation\Rule\UserNameRule')

Так це тому, що повідомлення вже описані в константах класу:
class UserNameRule extends AbstractRule
{
// Повідомлення про помилки
const MESSAGE = 'Ім'я користувача повинне складатися з двох слів.';
...

Інший варіант — використовувати метод addMessage() самого валідатора:
$validator->addMessage('email', 'будь Ласка, введіть коректний e-mail.');

Зверніть увагу, що кастомні правила ідентифікуються за повного їх назві класу, у той час як в Kontrolio можна задати псевдонім/псевдонім.
Зворотний зв'язок
Щоб виконати валідацію, ми викликаємо метод валідатора validate(), передаючи дані:
$data = [
'name' => 'Альберт', // Повинно бути два слова
'login' => '@lbert', // "Заборонений" символ @
'email' => 'не', // Тут повинен бути e-mail
'password' => " // Пароль взагалі не вказаний
// 'agreed' немає в масиві, тому що користувач не встановив прапорець
];

$validator->validate($data);

На відміну від Respect, Sirius не викине виняток, а просто поверне false. Повідомлення про помилки валідації можна отримати через метод валідатора getMessages(). Він повертає помилки, згруповані за атрибутами, так що для проходу по помилок нам знадобиться два циклу foreach:
foreach ($validator->getMessages() as $attribute => $messages) {
foreach ($messages as $message) {
echo $message->getTemplate() . "\n";
}
}

Тут $message — об'єкт класу Sirius\Validation\ErrorMessage, у якого є метод getTemplate(), який повертає те саме необхідне нам повідомлення.
Valitron
Поїхали далі. Ще одне цікаве рішення — Valitron. Valitron відрізняється від інших реалізацією додавання і опису правил валідації.
Визначаємо правила
Перша відмінність: щоб додати нове правило, не потрібно створювати окремий клас. Можна просто використовувати замикання, повертає булевский результат.
Для додавання кастомних правил Valitron є статичний метод addRule(), в якому перші два аргументи є обов'язковими, а третій — за бажанням. Мені сподобався такий спосіб, так як тут відразу в одному місці вказується ідентифікатор правила, логіка та повідомлення про помилку.
use Valitron\Validator;

Validator::addRule('two_words', function($field, $value) {
return (bool) preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $value);
}, 'Ім'я користувача повинне складатися рівно з двох слів.');

Друга відмінність — те, як правила застосовуються до атрибутів. У всіх попередніх випадках ми бачили, що атрибут — річ як первинна.
У Valitron пішли іншим шляхом і на перше місце поставили саме правила валідації. Описуючи правила, ви застосовуєте атрибути до цих правил, а не навпаки.
$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');

Як видно з прикладу, в методі rule() ми спочатку пишемо назву правила, а вже потім вказуємо атрибути, які повинні відповідати цьому правилу. Більш явний приклад — правило
required
, де показано, як атрибути «належать» цим правилом.
Valitron (як і інші рішення, які ми встигли розглянути) надає стандартні повідомлення про помилки. Якщо ви скористаєтеся ними, то побачите, що кожне повідомлення починається з назви відповідного атрибута.
Valitron підставляє імена атрибутів в текст повідомлення навіть у тому випадку, коли використовуються нестандартні повідомлення про помилки. Тому ми і скористалися методом label() з порожнім рядком, щоб прибрати ім'я атрибута.
$validator->rule('two_words', 'name')->label(")

Зворотний зв'язок
що стосується Конкретно валідації, API бібліотеки Valitron практично нічим не відрізняється від того, що ми вже бачили в статті. Щоб виконати валідацію ми викликаємо метод валідатора validate():
$validator->validate();

Повідомлення про помилки валідації можна отримати за допомогою методу getErrors():
$validator->errors();

Повідомлення тут группирутся за атрибутами точно так само, як і в Sirius Validation, за виключенням того, що для повідомлення немає окремого класу, і ми отримуємо звичайний багатовимірний масив.
foreach ($validator->errors() as $attribute => $messages) {
foreach ($messages as $message) {
echo $message . "\n";
}
}

Kontrolio
Ну і нарешті, остання бібліотека на сьогодні — моя власна розробка під назвою Kontrolio.
Визначаємо правила
Знову, в п'ятий раз ми створимо правило валідації для імені користувача. Все відносно просто і стандартно:
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);
}
}

Тепер ми створюємо фабрику і регистриуем правило в ній, використовуючи метод extend():
namespace MyProject;

use Kontrolio\Factory;
use MyProject\Validation\Rules\TwoWords;

$factory = Kontrolio\Factory::getInstance()->extend([TwoWords::class]);

Після реєстрації правил ми можемо скористатися ним, у тому числі по імені —
two_words
. Давайте створимо валідатор:
$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);

Ми описали правила, використовуючи синтаксис, схожий з тим, що використовується в Laravel, хоча могли використовувати і більш багатослівний варіант:
$rules = [
'name' => new TwoWords,
'login' => [new Sometimes, new Alphadash],
'email' => new Email,
'password' => new Length(1, 64),
'agreed' => new Accepted
];

Зворотний зв'язок
Валідація запускається все тим же методом validate():
$validator->validate();

Тепер ми можемо отримати повідомлення про помилки, використовуючи один з методів getErrors() або getErrorsList(). Перший метод дозволяє зробити більш складний висновок помилок, тоді як другий повертає плоский масив. Використовуючи getErrors() ми можемо вивести повідомлення якось так:
<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>

getErrorsList() можна зробити більш простий список повідомлень:
<?php $errors = $validator->getErrorsList(); ?>

<ul class="errors">
<?php foreach($errors as $error): ?>
<li class="errors__error"><?= $error; ?></li>
<?php endforeach; ?>
</ul>

Підсумок
У даній статті я показав приклади використання наступних бібліотек:
  1. Aura.Filter
  2. Respect Validation
  3. Sirius Validation
  4. Valitron
  5. Kontrolio
«Приклад з реального світу» може здатися занадто простим. Я змушений погодитися, так як, дійсно, деякі можливості бібліотек залишилися за бортом статті. В принципі, якщо вам це буде цікаво, ви можете вивчити їх особливості самостійно.
Кожна з бібліотек пропонує свої фішки, має свої темні сторони, так що я думаю, що це справа смаку і завдання — вибрати ту саму.
Дякую за прочитання. Зробіть правильний вибір :)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.