Знакомство с Rock Validate

в 10:57, , рубрики: php, validation, валидация, метки:

Знакомство с Rock Validate - 1Валидация данных является одной из множества практик в разработке безопасного web-приложения. Даже совсем «юный» разработчик при первом своём знакомстве с html-формой пытается вывести красивое сообщение об ошибке. Что уж говорить про модель в каком-нибудь навороченном фреймворке. А потому…

Предлагаю вашему вниманию библиотеку для валидации данных с кастомизацией, интернационализацией и иными «плюшками». Используя известный инструмент Respect/Validation с множеством вбитых по ходу костылей, я в какой-то момент сказал себе: Хватит!

Были поставлены задачи:

  • сохранить элегантный синтаксис (сцепной принцип для правил);
  • реализовать «лёгкую» и гибкую альтернативу;
  • добавить интернационализацию;
  • предоставить возможность добавлять свои правила;
  • подготовить фундамент для санитизатора — обеспечить единый стиль реализации для обеих библиотек.

Всё до безобразия просто

$v = Validate::length(10, 20, true)->regex('/^[a-z]+$/i');
$v->validate('O’Reilly'); // output: false

$v->getErrors();
/*
output:
[
  'length' => 'value must have a length between 10 and 20',
  'regex' => 'value contains invalid characters'
]
*/

$v->getFirstError();
// output: value must have a length between 10 and 20

Список ошибок представлен в виде ассоциативного массива.

  • getErrors() — вывод всего стека ошибок;
  • getFirstError() — возвращает первую ошибку;
  • getLastError() — возвращает последнею ошибку.

Правила

Набор правил достаточно широк, ибо с небольшими изменениями перекачивал из стана «конкурента».

Существуют группы правил:

  • общего назначения
  • строковые
  • числовые
  • дата и время
  • файловой системы
  • сетевые
  • и др.

Полный список правил

Любой дополнительный каприз реализуется кастомизацией, либо pull request-ом.

Валидация по атрибутам

Для валидация массива/объекта по атрибутам используется метод attributes().

$input = [
    'username' => 'O’Reilly',
    'email' => 'o-reilly@site'
];
$attributes = [
    'username' => Validate::required()
                    ->length(2, 20, true)
                    ->regex('/^[a-z]+$/i'),
    'email' => Validate::required()->email()
];

$v = Valiadte::attributes($attributes);
$v->validate($input); // output: false
$v->getErrors();
/*
output:
[
    'username' => [
        'regex' => 'value contains invalid characters',
    ],
    'email' => [
        'email' => 'email must be valid',
    ],
] 
*/

Использование одного набора правил для каждого атрибута:

Validate::attributes(Validate::required()->string())->validate($input);

Отрицание правил

Инвертировать поведение правил можно с помощью метода notOf(). В это случае, используется «негативный» шаблон сообщения Locale::MODE_NEGATIVE.

$v = Validate::notOf(Validate::required());
$v->validate(''); // output: true

Данный метод применим, как для правил внутреннего атрибута ['email' => Validate::notOf(Validate::email())], так и для всех атрибутов в целом. Пример:

$input = [
    'email' => 'tom@site',
    'username' => ''
];
$attributes = Validate::attributes([
      'email' => Validate::email(),
      'username' => Validate::required()
]);
$v = Validate::notOf($attributes);
$v->validate($input); // output: true

Правило oneOf()

Если хотя бы одно правило неверно, то проверка останавливается. Пример:

$input = 7;
$v = Validate::oneOf(Validate::string()->email());

$v->validate($input); // output: false
$v->getErrors();
/*
output:
[
  'string' => 'value must be string'
]
*/

Для валидации по атрибутам сценарий аналогичен отрицанию:

$input =  [
    'email' => 'tom@site',
    'username' => ''
];

$attributes = Validate::attributes([
    'email' => Validate::email(),
    'username' => Validate::required()
]);
$v = Validate::oneOf($attributes);
$v->validate($input); // output: false

$v->getErrors();
/*
output:
[
  'email' => [
    'email' => 'email must be valid',
  ]
]
*/

Правило when()

Необходимо для реализации условия (тернарная условная операция). Общий синтаксис метода выглядит так:

v::when(v $if, v $then, v $else = null)

Пример:

$v = Validate::when(Validate::equals('Tom'), Validate::numeric());
$v->validate('Tom'); // output false

$v->getErrors();
/*
output:
[
   'numeric' => 'value must be numeric',
]
*/

Замена плейсхолдеров, сообщений и шаблонов

Многие сообщения об ошибках содержат плейсхолдеры (к примеру, {{name}}), которые заменяются значениями по умолчанию. Заменить на свои не составит труда:

$v = Validate::length(10, 20)
    ->regex('/^[a-z]+$/i')
    ->placeholders(['name' => 'username']);
$v->validate('O’Reilly'); // output: false

$v->getErrors();
/*
output:
[
  'length' => 'username must have a length between 10 and 20',
  'regex' => 'username contains invalid characters',
]
*/

Аналогично, такой «горячей» замены подлежит и всё сообщение:

$v = Validate::length(10, 20)
    ->regex('/^[a-z]+$/i')
    ->messages(['regex' => 'Хьюстон, у нас проблемы!']);
$v->validate('O’Reilly'); // output: false

$v->getErrors();
/*
output:
[
  'length' => 'username must have a length between 10 and 20',
  'regex' => 'Хьюстон, у нас проблемы!'
]
*/

В зависимости от заданных аргументов в методе правила, шаблон сообщения подбирается автоматически, но никто не мешает подставить любой другой, который описан в текущей локали. Пример:

$v = Validate::length(10, 20)->templates(['length' => Length::GREATER]);
$v->validate('O’Reilly'); // output: false

$v->getErrors();
/*
output:
[
  'length' => 'value  must have a length lower than 20',
]
*/

Интернационализация

На текущий момент времени существуют два словаря сообщений: русский и английский. По умолчанию сообщения об ошибках будут выводится на английском языке. Установить локаль можно через метод locale():

$v = Validate::locale('ru')
    ->length(10, 20)
    ->regex('/^[a-z]+$/i');
$v->validate('O’Reilly'); // output: false

$v->getErrors();
/*
output:
[
  'length' => 'значение должно иметь длину в диапазоне от 10 до 20',
  'regex' => 'значение содержит неверные символы',
]
*/

Кастомизация

Создать свои правила можно в два шага.

Шаг #1. Создаём класс с правилом:

use rockvalidaterulesRule

class CSRF extends Rule
{
    public function __construct($compareTo, $compareIdentical = false, $config = [])
    {
        $this->parentConstruct($config);
        $this->params['compareTo'] = $compareTo;
        $this->params['compareIdentical'] = $compareIdentical;
    }

    public function validate($input)
    {
         if ($this->params['compareIdentical']) {
                return $input === $this->params['compareTo'];
         }
         return $input == $this->params['compareTo'];           
    }    
}

Шаг #2. Создаём класс с сообщениями:

use rockvalidatelocaleLocale;

class CSRF extends Locale
{
    const REQUIRED = 1;
    
    public function defaultTemplates()
    {
        return [
            self::MODE_DEFAULT => [
                self::STANDARD => '{{name}} must be valid',
                self::REQUIRED => '{{name}} must not be empty'
            ],
            self::MODE_NEGATIVE => [
                self::STANDARD => '{{name}} must be invalid',
                self::REQUIRED => '{{name}} must be empty'
            ]
        ];
    }
    
    public function defaultPlaceholders($compareTo)
    {
        if (empty($compareTo)) {
            $this->defaultTemplate = self::REQUIRED;
        }
        return [
            'name' => 'CSRF-token'
        ];
    }
}

Как ранее отмечалось, при использовании правила notOf() будет подставлен шаблон сообщения Locale::MODE_NEGATIVE. Подшаблон же позволяет разнообразить сообщения в зависимости от заданных аргументов в методе правила. По умолчанию Locale::STANDARD.

Профит:

$config = [
    'rules' => [
        'csrf' => [
            'class' => namespacetoCSRF::className(),
            'locales' => [
                'en' => namespacetoenCSRF::className(),
            ]
        ],
    ]
];

$sessionToken = 'foo';
$requestToken = 'bar';
$v = new Validate($config);
$v->csrf($sessionToken)->validate($requestToken); // output: false

$v->getErrors();
/*
output:
[
    'csrf' => 'CSRF-token must be valid',
]
*/

Таким образом, можно осуществить подмену существующих правил.

Дополнительные возможности

Существует сценарий, когда необходимо пропустить «пустые» значения. К примеру, для полей формы необязательных к заполнению. Для этих целей существует свойство skipEmpty — задаётся реакция для каждого правила на «пустые» значения. Для некоторых правил это свойство выставлено в false (не пропускать), а именно: Required, Arr, Bool, String, Int, Float, Numeric, Object, NullValue, Closure, всех ctype-правил. Пример:

$v = Validate::email();
$v->validate(''); // output: true

Данное поведение можно отменить:

$v->isEmpty(false)->validate(''); // output: false

По умолчанию пустыми значениями являются $value === null || $value === [] || $value === ''. Для каждого из правил, существует возможность задать свой обработчик isEmpty:

$config = [
    'rules' => [
        'custom' => [
            'class' => namespacetoCustomRule::className(),
            'locales' => [
                'en' => namespacetoenCustom::className(),
            ],
            'isEmpty' => function($input){
                return $input === '';
            }
        ],
    ]
];

$v = new Validate($config); 

Установка

composer require romeoz/rock-validate:*

А посмотреть?

Существует небольшое демо, которое можно запустить одним из двух способов:

1. Подтянуть из docker registry

docker run -d -p 8080:80 romeoz/vagrant-rock-validate

Демо станет доступно по адресу: http://localhost:8080/

2. Воспользоваться связкой VirtualBox + Vagrant

  • Клонировать проект romeOz/vagrant-rock-validate
    git clone https://github.com/romeOz/vagrant-rock-validate.git
    
  • Опционально. Чтобы Vagrant прописал в hosts фиктивный hostname (http://www.rock-validate/), требуется установить плагин:
    vagrant plugin install vagrant-hostsupdater
    

Демо станет доступно по следующим адресам: www.rock-validate/ или 192.168.33.35/

Автор: romeOz

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js