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

Валидация данных является одной из множества практик в разработке безопасного 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 rock\validate\rules\Rule

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 rock\validate\locale\Locale;

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' => \namespace\to\CSRF::className(),
            'locales' => [
                'en' => \namespace\to\en\CSRF::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->skipEmpty(false)->validate(''); // output: false

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

$config = [
    'rules' => [
        'custom' => [
            'class' => \namespace\to\CustomRule::className(),
            'locales' => [
                'en' => \namespace\to\en\Custom::className(),
            ],
            'isEmpty' => function($input){
                return $input === '';
            }
        ],
    ]
];

$v = new Validate($config); 

Установка


composer require romeoz/rock-validate:*

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



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

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

Демо станет доступно по адресу: http://localhost:8080/
Поделиться публикацией

Похожие публикации

Комментарии 16

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


    А на localhost не выложите?
      +1
      Уже, выше.
      Автор указывает адреса, доступные после разворачивания в виртуалке.
        0
        Оу, был невнимателен, как следствие — излишне саркастичен.
        0
        Измените на любой желаемый.

        Когда только постигал мудрость разворачивания проектов в виртуалке, руководствовался сторонним vagrant-конфигом. С тех пор, как-то прижилось. Плюс позволяет избежать конфликта с остальными пет проектами поднятыми в виртуалке. Меняю лишь последний разряд. 192.168.0.0/16 — вроде ничего криминального.

        P.S. Демо скорее в нагрузку, ибо практическая польза минимальна. Примеры представлены здесь и в документации исчерпывающие. Дополнительные зависимости/окружение, которые необходимо как-то особым образом «приготовить» — не требуются.
        0
        А с чем связан такой несколько нестандартный выбор способа инициализации правил валидации? В том плане, по примерам, что каждый метод, по сути, может быть вызван как статически для создания валидатора, так и физически на уже готовом валидаторе? Почему не некая единая точка инициализации?
          0
          • сохранить элегантный синтаксис (сцепной принцип для правил);

          Кроме «элегантности» такого подхода, сохранена совместимость, что даёт прозрачную возможность мигрировать. Можете посмотреть на аналогичную «магическую» инициализацию в Respect/Validation.
          Почему не некая единая точка инициализации?

          Легко. В примере с кастомизацией это видно. А именно:
          // инициализация с дефолтными настройками
          $v = (new Validate)->string()->email();
          $v->validate(7); // output: false
          
          $v->getErrors();
          /*
          output:
          [
             'string' => 'value must be string',
             'email' => 'value must be valid',
          ]
          */
          

          Давайте разнообразим задачу и предположим, что вы используйте некий DI контейнер:
          [
              ...
              'validator' => function(){
                  $config = [
                      'locale' => 'ru' // по дефолту используем русский словарь
                  ];
          
                  return new Validate($config);
              },
              ...
          ]
          

          Возможно, у Вас возникнет вопрос: Почему используется некий конфигурирущий массив с настройками?
          Данная библиотека является standalone модулем моего фреймворка, т.е. используется независимо от него, как и многие другие модули. Инициализация в нём реализована таким образом. Подобную подход Вы могли видеть в yii2
            0
            Судя по коду, Rock PHP Framework — это форк/переосмысление Yii 2.
            Очень интересно было бы увидеть пост о проекте в целом: зачем он, чем отличается от Yii, что планируется добавить, изменить.
              0
              … это форк/переосмысление Yii 2.

              Легенда была несколько иная. Всё началось с написания шаблонизатора, потом появилась некая «экосистема»/фреймворк. Далее, была попытка избавится от множества велосипедов. Плотная работа с Yii 1 и повышенный интерес к Yii 2 ещё в ранней альфе, сделали своё дело. До этого года полтора проработал с Symfony, но взаимности не вышло.

              Фреймворк писался из академических соображений — бесценные опыт и знания. Скорее всего, он в таком статусе и останется. Ряд компонентов пусть даже с небольшими изменениями, являются всё же наследием Yii 2, а потому, неэтично как-то ангажировать последний.

              чем отличается от Yii

              • Роутинг. Можно скормить конфиг с правилами, либо использовать методы, short-метод для создания набора REST-правил, поддержка Action Injection
              • DI. Возможность возвращать инстанс, как синглетон, service locator, Constructor Injection.
              • Шаблонизатор. Использует два движка: нативный php и свой доморощенный на regex-ах (сниппеты, чанки, плейсхолдеры, фильтры). Одновременно можно использовать оба. Изолированные скоупы для плейсхолдеров: $root.name, $parent.name и name соответствует плейсхолдеру/переменной рута, родителя и текущего скоупа), эскейпинг всего, кэширование. HtmlHelper перекачивал из Yii, а также некоторые виджеты. Тот же ActiveForm использует AngularJS на клиенте (в доработке).
              • Валидатор описанный в этом посте
              • Санитизатор
              • Кэширование: тегирование, autolock для конкурирующих запросов
              • Url builder
              • Datatime builder
              • Провайдер для изображений. Связка imagine + flysystem, позволяет хранить отредактированные изображение не только локально, но и на удалённых файловых хранилищах/облаке
              • Абстракция над реализацией markdown парсера от Карстена Брандта. Дополнил специальным синтаксисом для кропа изображений, добавление видео-тегов с возможность указать заглушку с ссылкой на видео (режим оптимизации), существует возможность ограничить набор используемых тегов
              • Абстракция над MQ сервисами
              • и многое другое

              С недавних пор стал модульным, т.е. каждый компонент можно использовать независимо от фреймворка. Yii, к сожалению, таким не является, и неизвестно, будет ли. Тут, как говорится, на вкус и цвет.
              И всё же, к примеру, если Вам нравится реализация AR модели в Yii 2 и при этом очень хочется только её одну где-нибудь задействовать, возможно, в другом фреймворке, то можете воспользоваться Rock DB (актуальна версии Yii2 2.0.3). Существует ряд отличий/дополнений: кэширование Rock Cache, Select Builder — помогает автоматизировать подбор полей при использовании JOIN-ов, иная система пагинации для ActiveDataProvider и ещё по мелочи. Аналогично это касается API/ORM для поискового движка Sphinx и ODM для MongoDB. Для хранения сессий в MongoDB задействован ttl-индекс и упразднён garbage collector. Всё никак не подготовлю PR, чтоб и в Yii это было + некоторые баги.
              Всегда полезно покопаться в чужом белье коде. Благодаря моему взвозу в Yii DB наконец-то появился валидный UNION, но нет возможности задать ORDER BY/LIMIT для всех стейтментов в конце. У себя я это предусмотрел. В общем, всех нюансов сразу так и не вспомнить.

              Повторюсь, у меня нет планов писать обзорные статьи по фреймворку и по тем компонентам, где мой вклад минимален.
                0
                А Rock Validate вы планируете поддерживать и развивать? Не хотелось бы оказаться в ситуации, когда куча кода завязана на компонент, который автор уже не развивает.
                  0
                  Я всё буду поддерживать, ибо львиная доля компонентов/библиотек задействована в проектах компании. С радостью готов принять feedback.
          0
          github.com/romeOz/rock-validate/blob/master/src/rules/Required.php

          Мне кажется, или вы собрали те же грабли, что и Respect/Validation? По поводу этого !empty() столько непоняток. Как, например, без костылей валидировать 0 и 1?
            0
            По поводу этого !empty() столько непоняток.

            Спасибо, что заметили. Добавил выбор режима ($strict). По умолчанию он строгий ($strict = true). В нестрогом режиме, это: '' и null.

            Validate::required(false)->validate(0); // output: true
            
            Validate::required(false)->validate(''); // output: false
            
              0
              Вот так лучше
            0
            Также, можно поподробнее про

            >> известный инструмент Respect/Validation _с множеством вбитых по ходу костылей_
              0
              1. Мне совершенно непонятен вывод стека ошибок через древо exceptions. Если такая возможность кому-то необходима, то несложно абстрагироваться от \rock\validate\Validate с добавлением соответствующих методов, которые, в свою очередь, будут дёргать getErrors() и выбрасывать exceptions.

              2. Логика у Respect/Validation слишком «накручена» — глубокий уровень наследования (жучка за внучку, внучка за бабку, ...). В моей реализации:
              • У Validate вообще отсутствует наследование. При использовании атрибутов задействован класс Attributes.
              • Rule -> Класс с правилом
              • Locale -> Класс с ошибками.

              3. Кстати, товарищи сделали i18n? Тем не менее, такой вызов exception не очевиден — жёсткая привязка к неймспесу, а значит, и к директории.
                0
                Честно говоря, во внутренностях Respect/Validation особо не копался, возможно вы правы. Насчет i18n, я не уверен, что он всем там по дефолту нужен. По крайней мере я сделал свою надстройку и очень доволен (у меня translator переводы из БД берет).

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое