Держите данные под контролем

Не секрет, что пользовательским данным доверять нельзя. Поэтому однажды человек и придумал валидацию данных. Ну, а я, интереса ради и пользы для, написал свою реализацию валидатора на PHP.

Kontrolio — «очередная библиотека валидации данных», спроектированная независимой от фреймворков, расширяемой и дружественной контейнерам сервисов. Альтернативы: Respect, Sirius Validation, Valitron и многие другие.

В идеале предполагается, что вы используете некую реализацию контейнера сервисов (напр., PHP-DI, PHP League Container и др.), поэтому для начала необходимо зарегистрировать Kontrolio в нём:

use Kontrolio\Factory;

// Регистрируем
$container->singleton('validation', function() {
    return new Factory;
});

// Используем
$validator = $container->get('validation')
    ->make($data, $rules, $messages)
    ->validate();

В тех ситуациях, когда внедрить контейнер в проект затруднительно, вы можете использовать старый-добрый (на самом деле нет) синглтон:

use Kontrolio\Factory;

$validator = Factory::getInstance()
    ->make($data, $rules, $messages)
    ->validate();

Возможно вы заметите, что процесс валидации похож на оный из Laravel. Действительно, мне понравилось то, как там это реализовано, поэтому я решил использовать подобное решение. $data, $rules и $messages — ассоциативные массивы, где $data — это просто массив из пар ключ-значение (может быть многомерным), в котором ключ это имя атрибута, который необходимо провалидировать. Самое интересное — в правилах валидации и сообщениях об ошибках.

Правила валидации и сообщения об ошибках


Правило валидации в Kontrolio может быть представлено объектом класса правила или замыканием. Замыкания — самый простой способ описания правила валидации:
$rules = [
    'attribute' => function($value) {
        return $value === 'foo';
    }
];

$valid = $validator
    ->make(['attribute' => 'bar'], $rules)
    ->validate();

var_dump($valid); // false

Правила-замыкания при обработке валидатором оборачиваются в объект класса Kontrolio\Rules\CallbackRuleWrapper, поэтому они располагают всеми теми же опциями, что и классы-правила, и вы можете написать замыкание в таком виде:

'attribute' => function($value) {
    return [
        'valid' => $value === 'foo',
        'name' => 'value_is_foo_rule',
        'empty_allowed' => true,
        'skip' => false,
        'violations' => []
    ];
}

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

use Kontrolio\Rules\AbstractRule;

class FooRule extends AbstractRule
{
    public function isValid($input = null)
    {
        return $input === 'foo';
    }
}

$rules = ['attribute' => new FooRule];

На заметку: Kontrolio поставляется с множеством правил «из коробки».

Опции правил валидации


В записи правил в виде замыканий вы заметили несколько опций, которые поддерживает любое правило. Немного о каждой опции далее.

valid. Это непосредственно условие. Эквивалент для класса правила — метод isValid, принимающий один аргумент, валидируемое значение атрибута. Чтобы стало понятнее, я покажу, как можно задать правило валидации для некого атрибута:

// Самый простой способ, где возвращаемое значение есть условие правила.
'attribute' => function($value) {
    return $value === 'foo';
}

// Если вы хотите использовать опции, вам необходимо возвращать массив из замыкания,
// при этом в массиве должен быть обязательно задан ключ 'valid',
// который определяет условие правила.
'attribute' => function($value) {
    return [
        'valid' => $value === 'foo'
        // другие опции...
    ];
}

// При использовании отдельного класса вы наследуете свое правило от базового класса
// и определяете метод isValid, в котором возвращаете условие.
use Kontrolio\Rules\AbstractRule;

class FooRule extends AbstractRule
{
    public function isValid($input = null)
    {
        return $input === 'foo';
    }
}

Это самые простые способы задания правила к атрибуту.

name. Это имя или идентификатор правила. Главным образом, используется для формирования сообщений об ошибках валидации:

$data = ['attribute' => 'invalid'];
$rules = ['attribute' => new Email];
$messages = ['attribute.email' => 'The attribute must be an email'];

$validator = $container->get('validation')
    ->make($data, $rules, $messages);
if ($validator->validate()) {
    //
} else {
    $messages = $validator->getErrors();
}

Если вы создаете правило на основе класса, то вам нет необходимости задавать имя/идентификатор правила вручную, потому что наследуясь от Kontrolio\Rules\AbstractRule вы получаете данную функциональность по умолчанию в методе getName. Тем не менее вы можете свободно менять имя правила просто переопределив этот метод.

empty_allowed. Эта опция полезна в ситуациях, когда вам необходимо применить правило валидации только в том случае, если значение атрибута присутствует. С замыканиями это выглядит так:

'attribute' => function($value) {
    return [
        'valid' => $value === 'foo',
        'empty_allowed' => true
    ];
}

Используя класс-правило с данной опцией, вы можете применить его к атрибуту двумя способами:

// Именованный конструктор
'attribute' => FooRule::allowingEmptyValue()

// Или соответствующий не статический метод
'attribute' => (new FooRule)->allowEmptyValue()

В данном случае, валидатор ответит положительно, если в значении атрибута будет значение 'foo' или он будет пуст.

skip. Эта опция не похожа на предыдущую. Она позволяет вам указать такое условие, которое прикажет валидатору пропустить проверку атрибута так, как будто данного правила и не было вовсе.

'confirmed' => function($value) {
    return [
        'valid' => (bool)$value === true,
        'skip' => is_admin()
    ];
}

Эквивалент для правила-класса — метод canSkipValidation, и работает он абсолютно так же:

class FooRule
{
    public function isValid($input = null)
    {
        return (bool)$value === true;
    }
    public function canSkipValidation()
    {
        return is_admin();
    }
}
$rules = ['confirmed' => new FooRule];

violations. Я любезно позаимствовал данный термин из Symfony. С использованием «нарушений» пользователь может получить более точное сообщение об ошибке (которое вам необходимо задать), хотя сам валидатор, так же как и прежде, просто вернёт false:

$data = 'nonsense';
$rules = ['attribute' => new Email(true, true)];
$messages = [
    'attribute' => [
        'email' => "Something's wrong with your email.",
        'email.mx' => 'MX record is wrong.',
        'email.host' => 'Unknown email host.'
    ]
];

Вы можете задавать столько «нарушений», сколько пожелаете, и каждое из них затем может быть использовано для более детального описания ошибок валидации: от самого общего сообщения до самого детализированного. Посмотрите, как пример, класс Kontrolio\Rules\Core\Email.

Применяем несколько правил к атрибутам


До этого все примеры показывали описание одного правила к одному атрибуту. Но, естественно, вы можете добавлять сколь угодно много правил к сколь угодно многим атрибутам :) Более того, вы можете совмещать использование замыканий и классов:
$rules = [
    'some' => function($value) {
         return $value === 'foo';
    },
    'another' => [
        function($value) {
            return $value !== 'foo';
        },
        new FooBarRule,
        // и так далее...
    ]
];

Всё круто, конечно, но есть еще один интересный способ записи целого набора правил — в виде строки:

'attribute' => 'not_empty|length:5,15'

Здесь каждое правило отделяется вертикальной чертой. Эту идею я позаимствовал из Laravel, но разница в том, что любая такая строка «распаковывается» в обычный массив правил, который вы видели уже не раз в статье. Так что строка выше в данном случае — всего лишь сахарок для вот такого массива:

'attribute' => [
    new NotEmpty,
    new Length(5, 15)
]

Обратите внимание, что всё, что вы пишете после двоеточия, прямиком попадает в аргументы конструктора правила-класса:

'length:5, 15' -> new Length(5, 15)

Так что тут надо быть осторожным.

Пропускаем валидацию атрибута целиком


Пропуска отдельного правила или позволения пустых значений было бы недостаточно, поэтому Kontrolio содержит специальное правило, названное по аналогии с Laravel — 'sometimes' и представленное классом Kontrolio\Rules\Core\Sometimes. Когда вы добавите это правило к атрибуту, оно укажет валидатору пропустить проверку атрибута, если он отсутствует в массиве данных, переданных в валидатор, или если его значение пусто. Данное правило необходимо всегда ставить первым в списке.
$data = [];
$rules = ['attribute' => 'sometimes|length:5,15'];

$valid = $container
    ->get('validator')
    ->make($data, $rules)
    ->validate();

var_dump($valid); // true

По аналогии с предыдущими примерами данный может быть записан и так:

$data = [];
$rules = [
    'attribute' => [
        new Sometimes,
        new Length(5, 15)
    ]
];

$valid = $container
    ->get('validator')
    ->make($data, $rules)
    ->validate();

var_dump($valid); // true

Вывод ошибок валидации


Ошибки валидации хранятся в виде ассоциативного массива, где ключи это названия атрибутов, а значения — массивы с самими сообщениями:
$data = ['attribute' => ''];
$rules = [
    'attribute' => [
        new NotBlank,
        new Length(5, 15)
    ]
];

$messages = [
    'attribute.not_blank'  => 'The attr. is required.',
    'attribute.length.min' => 'It must contain at lest 5 chars'
];

$valid = $container
    ->get('validator')
    ->make($data, $rules)
    ->validate();

$errors = $validator->getErrors();

Дамп ошибок будет выглядить следующим образом:

[
    'attribute' => [
        0 => 'The attr. is required.',
        1 => 'It must contain at lest 5 chars'
    ]
]

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

$errors = $validator->getErrorsList();

Для более сложного вывода ошибок можно использовать метод getErrors. Он возвращает сообщения, сгруппированные по названиям атрибутов:

    $messages):

Завершая сей рассказ


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

Благодарю за прочтение!

Комментарии (2)

  • 26 июля 2016 в 17:32 (комментарий был изменён)

    0

    Возможно я не увидел этого, но где валидация многомерных данных? Например, с помощью Symfony валидатора я могу делать такую валидацию:
    Код
    'rules' => new Assert\Optional([
                        new Assert\Type(['type' => 'array']),
                        new Assert\All([
                            'constraints' => [
                                new Assert\Collection([
                                    'week_days' => [
                                        new Assert\Type(['type' => 'array']),
                                        new Assert\Count([
                                            'min' => 1,
                                            'max' => 7,
                                        ]),
                                        new Assert\All([
                                            'constraints' => [
                                                new Assert\Choice(['choices' => range(0, 6)]),
                                            ]
                                        ]),
                                    ],
                                    'hours' => [
                                        new Assert\Type(['type' => 'array']),
                                        new Assert\Count([
                                            'min' => 1,
                                            'max' => 24,
                                        ]),
                                        new Assert\All([
                                            'constraints' => [
                                                new Assert\Choice(['choices' => range(0, 23)]),
                                            ]
                                        ]),
                                    ],
                                    'group_id' => new Assert\Optional(new Assert\Type(['type' => 'integer'])),
                                ])  
                            ]
                        ]),
                    ]),
    

  • 26 июля 2016 в 17:33

    +1

    Неделя банальных статеек открыта!

© Habrahabr.ru