26 июля 2016 в 17:19

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

Не секрет, что пользовательским данным доверять нельзя. Поэтому однажды человек и придумал валидацию данных. Ну а я, интереса ради и пользы для, написал свою реализацию валидатора на 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();

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


Для более сложного вывода ошибок можно использовать метод 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>


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


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

Благодарю за прочтение!
Ян Иванов @franzose
карма
38,5
рейтинг 0,0
Веб-разработчик
Самое читаемое Разработка

Комментарии (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'])),
                                ])  
                            ]
                        ]),
                    ]),
    

    • 0
      Да. На самом деле, это упущение как статьи, так и документации. Валидация многомерных данных есть:

      $rules = ['attribute.nested.nested' => function() { ... }]
      $messages = ['attribute.nested.nested' => 'Атрибут не может быть таким-секим']
      
      • +1

        как-то это дико неудобно выглядит.

  • +1
    Неделя банальных статеек открыта!
  • –2
    может быть представлено объектом класса правила или замыканием

    Это называется по-русски «анонимная функция» или «лямбда-функция». Использование термина «Closure» в PHP — ошибка разработчиков, в которой они не раз уже каялись.
    • +2
      Если бы пост был на английском и автор использовал термин «Closure», то вы сообщили, что по-английски это называется «anonymous function» или «lamba function»?
      • –3
        Да.
        Closure — это замыкание контекста на Lambda. А никак не сама Lambda.
        • +1

          Во-первых, не замыкание контекста на анонимную функцию, а замыкание анонимной функции на контекст.
          Во-вторых анонимные функции в php, хотя и не замыкаются на контекст исполнения, тем не менее могут сменить внутренний контекст (ссылку) $this в ручном режиме (Closure::bind(), $func->bindTo()), и быть в этом смысле замкнуты на переданный объект.
          Таким образом, однозначно утверждать, являются ли анонимные функции в php замыканиями или нет — нельзя. И уж тем более сложно говорить о том, хорошо это или плохо. Это просто особенность языка и ее нужно знать. А Ваши выкрики в духе "ололо! Closure — не Clousre!", просто неуместны. Ибо статья о разработке на PHP написана разработчиком PHP для разработчиков PHP. И мы — разработчики PHP прекрасно понимаем, что значит замыкание в контексте PHP. Так что, в принципе, носом нас в это можно и не тыкать.

          • –3
            А Ваши выкрики в духе «ололо! Closure — не Clousre!

            О, выкрики нашли и „ололо“. Не процитируете ли?
            С каких это пор вежливое указание на терминологическую неточность стало вдруг „выкриками“?

            Если автор сочтет замечание существенным — исправит. Нет — ну и на здоровье. Вы-то что так волнуетесь?
  • +2
    я планирую написать статью, где попытаюсь сравнить свою библиотеку с другими решениями

    жду сравнения с этим — https://github.com/auraphp/Aura.Filter
  • +1
    А что насчет многоязычности? Вручную все сообщения прописывать? И опять же, каков смысл еще одной библиотеки если есть дургие? Какие отличия?
    • 0
      Сейчас «из коробки» сообщений нет, поэтому да, пока прописывать вручную. И я не уверен, стоит ли реализовывать многоязычность, ведь это можно сделать самостоятельно (псевдокод):

      $messages = ['attribute' => i18n('validation.attribute.not_empty')]
      
      • 0

        Я думаю, было бы отличным решением, дать пользователю возможность, самому определить методику перевода.


        Например так (псевдокод):


        $validator = $serviceLocator->get('validator');
        $translator = $serviceLocator->get('lang');
        
        $validatror->setTranlator(function($phrase) use ($translator){
           return $translator->translate(phrase);
        });
        • 0
          Т.е. чтобы можно было просто писать ключи вместо конкретных фраз и потом их переводить через кастомный сервис?
          • +1

            именно так

            • +1
              Надо подумать)
        • 0
          По моему скромному мнению, в библиотеку для валидации не нужно добавлять это. Валидатор возвращает массив ошибок, его и нужно переводить.
          • +1
            еще одна обязательная строчка в коде для мультиязычных и иноязычных приложений после каждой валидации? Не думаю что это хорошее решение
          • 0

            1) Это всего лишь возможность. Никто не заставляет ей пользоваться.
            2) Это декларативно. Единожды объявил переводчик, и забыл.
            3) Это позволит не городить "беготню" по массивам самому.
            4) Это ооп в конце-то концов.


            И я действительно не понимаю, какую проблему может доставить делегирование дополнительного функционала любому объекту на Ваш выбор.

            • 0
              две проблемы:
              1) время
              2) поддержка
              Которые могут стать не такими обременительными, если стать контрибьютером и дополнить код на гитхабе. Но опять таки — зачем? ведь есть же другие либы где все это уже есть…
              • 0

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

                • 0
                  я подразумевал не обертку, а законченный рабочий код
              • 0

                Ну а вопрос "зачем" — это к автору.

                • 0
                  Ну, грубо говоря она рассчитана на тех, кто собирает собственные фреймворки из разнородных компонентов вместо использования одного «полноценного» и большого. При этом я согласен, что некоторых функций пока может не хватать в сравнении с аналогами. А остальное — дело вкуса)
      • 0
        вы не ответили на вопрос
        И опять же, каков смысл еще одной библиотеки если есть другие? Какие отличия?
        • 0
          Отличия в использовании я хочу разобрать в одной из следующих статей. В частности, если не брать валидаторы от Laravel и Symfony, остальные используют так называемый fluent интерфейс или цепочки методов. Мне такой подход показался не очень удобным и я решил попробовать создать альтернативу) По поводу смысла: вначале это был спортивный интерес, но сейчас на работе идёт процесс рефакторинга, и я хочу в итоге внедрить эту библиотеку.
          • 0
            сейчас на работе идёт процесс рефакторинга, и я хочу в итоге внедрить эту библиотеку.

            Вот этого делать не стоит. Так как у вас нет веских причин
            Мне такой подход показался не очень удобным и я решил попробовать создать альтернативу

            Тоже так себе аргумент. достаточно было бы написать обертку как вам нравится. Но опять таки — зачем делать тоже самое, просто в другой оболочке без значительных изменений? Людям гораздо проще работать с вещами которые им уже знакомы, пусть и сделанные с неудобной архитектурой.
            И еще у вас нет поддержки ICU в сообщениях
          • +2
            если не брать валидаторы от Laravel и Symfony

            а почему их не брать? Оба валидатора хорошие и оба можно использовать как самостоятельные пакеты к любому проэкту. B обеих больше функционала, чем в вашем валидаторе и за обеими стоят изестные разработчики и комньюнити.
            • 0
              Я имел в виду, что они не используют цепочки методов как способ создания правил.
              • 0

                Это тип недостаток или что? Вместо того что бы писать свой валидатор проще сделать отдельный пакет, который добавляет fluent-интерфейс для построения правил и реюзает все возможности этих валидаторов.

                • 0
                  Я не говорил, что это недостаток :) Целью я себе ставил сделать валидатор «почти как в Laravel», но с возможностью указывать непосредственно в массиве правил классы и Closure.
                  • 0

                    Повторюсь — "реализовать другой способ задания правил" и "написать свой валидатор" это чуть разные задачи. Просто меня ситуация с валидаторами слегка удручает) Я пока использую symfony/serializer но мне приходится писать кастомные валидаторы что бы пофиксить баги и "неудобства" при использовании оного без форм.

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