image
Сегодня вышел еще один компонент PHPixie 3, в этот раз для валидации данных. Библиотек для PHP которые занимаются валидацией уже достаточно, зачем тогда писать еще один? На самом деле у большинства из них есть большой недостаток — они работают только с одномерными массивами данных ориентируясь в первую очередь на работу с формами. Такой подход неизбежно устарел в мире API и REST, все чаще приходиться работать с документообразными запросами со сложной структурой. Validate с самого начала был спроектирован как раз чтобы справляться с такими задачами. И даже если вы не используете PHPixie этот компонент может вам очень пригодиться.

Начнем с простого примера, простого одномерного массива:

// Собственно сами данные

$data = array(
    'name' => 'Pixie',
    'home' => 'Oak',
    'age'  => 200,
    'type' => 'fairy'
);

$validate = new \PHPixie\Validate();

// Создаем валидатор
$validator = $validate->validator();

// По сути одномерный массив это простой документ
$document = $validator->rule()->addDocument();

// Для задания самых правил поддерживаются несколько
// вариантов синтаксиса. Сначала попробуем стандартный

// Обязательное поле с фильтрами
$document->valueField('name')
    ->required()
    ->addFilter()
        ->alpha()
        ->minLength(3);

// Фильтры также можно задать массивом
$document->valueField('home')
    ->required()
    ->addFilter()
        ->filters(array(
            'alpha',
            'minLength' => array(3)
        ));

// Или в случае одного фильтра
// просто передать его сразу
$document->valueField('age')
    ->required()
    ->filter('numeric');

// свой колбек для конкретного поля
$document->valueField('type')
    ->required()
    ->callback(function($result, $value) {
        if(!in_array($value, array('fairy', 'pixie'))) {
            // Задаем свою ошибку
            $result->addMessageError("Type can be either 'fairy' or 'pixie'");
        }
    });

// По умолчанию валидатор не пропустит поля
// для которых нет правил валидации.
// Но эту проверку можно отключить
$document->allowExtraFields();

// свой колбек для всего документа
$validator->rule()->callback(function($result, $value) {
    if($value['type'] === 'fairy' && $value['home'] !== 'Oak') {
        $result->addMessageError("Fairies live only inside oaks");
    }
});


То же самое но с альтернативным синтаксисом
$validator = $validate->validator(function($value) {
    $value->document(function($document) {
        $document
            ->allowExtraFields()
            ->field('name', function($name) {
                $name
                    ->required()
                    ->filter(function($filter) {
                        $filter
                            ->alpha()
                            ->minLength(3);
                    });
            })
            ->field('home', function($home) {
                $home
                    ->required()
                    ->filter(array(
                        'alpha',
                        'minLength' => array(3)
                    ));
            })
            ->field('age', function($age) {
                $age
                    ->required()
                    ->filter('numeric');
            })
            ->field('type', function($home) {
                $home
                    ->required()
                    ->callback(function($result, $value) {
                        if(!in_array($value, array('fairy', 'pixie'))) {
                            $result->addMessageError("Type can be either 'fairy' or 'pixie'");
                        }
                    });
            });
    })
    ->callback(function($result, $value) {
        if($value['type'] === 'fairy' && $value['home'] !== 'Oak') {
            $result->addMessageError("Fairies live only inside oaks");
        }
    });
});



И сама валидация:

$result = $validator->validate($data);
var_dump($result->isValid());

// Добавим немного ошибок
$data['name'] = 'Pi';
$data['home'] = 'Maple';
$result = $validator->validate($data);
var_dump($result->isValid());

// Выведем ошибки
foreach($result->errors() as $error) {
    echo $error."\n";
}
foreach($result->invalidFields() as $fieldResult) {
    echo $fieldResult->path().":\n";
    foreach($fieldResult->errors() as $error) {
        echo $error."\n";
    }
}

/*
bool(true)
bool(false)
Fairies live only inside oaks
name:
Value did not pass filter 'minLength'
*/


Работа с результатами



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

if($error->type() === 'filter') {
    if($error->filter() === 'minLength') {
       $params = $error->parameters();
       echo "Please enter at least {$params[0]} characters";
    }
}


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

Структуры данных


Ну вот собственно «киллер фича», попробуем провалидировать вот такую структуру:

$data = array(
    'name' => 'Pixie',
    
    // 'home' это просто субдокумент
    'home' => array(
        'location' => 'forest',
        'name'     => 'Oak'
    ),
    
    // 'spells' массив субдокументов одного типа,
    // и текстовым ключом (его тоже надо проверить)
    // of the same type
    'spells' => array(
        'charm' => array(
            'name' => 'Charm Person',
            'type' => 'illusion'
        ),
        'blast' => array(
            'name' => 'Fire Blast',
            'type' => 'evocation'
        ),
        // ....
    )
);

$validator = $validate->validator();
$document = $validator->rule()->addDocument();

$document->valueField('name')
    ->required()
    ->addFilter()
        ->alpha()
        ->minLength(3);

// Субдокумент
$homeDocument = $document->valueField('home')
    ->required()
    ->addDocument();

$homeDocument->valueField('location')
    ->required()
    ->addFilter()
        ->in(array('forest', 'meadow'));

$homeDocument->valueField('name')
    ->required()
    ->addFilter()
        ->alpha();

// Массив субдокументов
$spellsArray = $document->valueField('spells')
    ->required()
    ->addArrayOf()
    ->minCount(1);

// Правила для ключа
$spellDocument = $spellsArray
    ->valueKey()
    ->filter('alpha');

// Правила для элемента массива        
$spellDocument = $spellsArray
    ->valueItem()
    ->addDocument();

$spellDocument->valueField('name')
    ->required()
    ->addFilter()
        ->minLength(3);

$spellDocument->valueField('type')
    ->required()
    ->addFilter()
        ->alpha();


То же самое используя альтернативный синтаксис
$validator = $validate->validator(function($value) {
    $value->document(function($document) {
        $document
            ->field('name', function($name) {
                $name
                    ->required()
                    ->filter(array(
                        'alpha',
                        'minLength' => array(3)
                    ));
            })
            ->field('home', function($home) {
                $home
                    ->required()
                    ->document(function($home) {
                        
                        $home->field('location', function($location) {
                            $location
                                ->required()
                                ->addFilter()
                                    ->in(array('forest', 'meadow'));
                            });
                        
                        $home->field('name', function($name) {
                            $name
                                ->required()
                                ->filter('alpha');
                        });
                    });
            })
            ->field('spells', function($spells) {
                $spells->required()->arrayOf(function($spells){
                    $spells
                        ->minCount(1)
                        ->key(function($key) {
                            $key->filter('alpha');
                        })
                        ->item(function($spell) {
                            $spell->required()->document(function($spell) {
                                $spell->field('name', function($name) {
                                    $name
                                        ->required()
                                        ->addFilter()
                                            ->minLength(3);
                                });
                                    
                                $spell->field('type', function($type) {
                                    $type
                                        ->required()
                                        ->filter('alpha');
                                });
                            });
                    });
                });
            });
    });
});



Альтернативний синтаксис на мой взгляд гораздо читабельнее в таком случае, так как табуляция кода совпадает с табуляцией документа.

Посмотрим на результаты

$result = $validator->validate($data);

var_dump($result->isValid());
//bool(true)

// Добавим ошибок
$data['name'] = '';
$data['spells']['charm']['name'] = '1';

// Невалидный чисельный ключ
$data['spells'][3] = $data['spells']['blast'];

$result = $validator->validate($data);

var_dump($result->isValid());
//bool(false)

// рекурсивная функция для вывода ошибок
function printErrors($result) {
    foreach($result->errors() as $error) {
        echo $result->path().': '.$error."\n";
    }
    
    foreach($result->invalidFields() as $result) {
        printErrors($result);
    }
}
printErrors($result);

/*
name: Value is empty
spells.charm.name: Value did not pass filter 'minLength'
spells.3: Value did not pass filter 'alpha'
*/


Демо


Чтобы попробовать Validate своими руками достаточно:

git clone https://github.com/phpixie/validate
cd validate/examples
 
#если у вас еще нет Композера
curl -sS https://getcomposer.org/installer | php
 
php composer.phar install
php simple.php
php document.php


И кстати как и у всех других библиотеках от PHPixie вас ждет 100% покрытие кода тестами и работа под любой версией PHP старше 5.3 (включая новую 7 и HHVM).

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


  1. KAndy
    28.10.2015 22:09
    +2

    Очень приятно что метод validate возвращает обьект а не изменяет состояние валидатора.


  1. AlexLeonov
    28.10.2015 23:45
    -5

    Очередная хрень в стиле PHP4. Почему бы не использовать нормальную концепцию MultiException?


    1. jigpuzzled
      29.10.2015 00:10
      +3

      А можно поподробнее?


      1. AlexLeonov
        29.10.2015 12:57
        -3

        Исключение, которое является коллекцией исключений. В PHP это делается двумя пальцами.
        Вы избавляетесь от безумия вроде ->errors() и получаете полный контроль над ошибками.


        1. jigpuzzled
          29.10.2015 13:19
          +1

          Во первых чисто логически результат валидации это не исключительное событие. То что вы говорите это то же самое что если бы strpos() бросал исключение если субстрока не найдена.

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


          1. AlexLeonov
            29.10.2015 13:26
            -5

            Господь с вами. Какое «прожорливее»? Какое «медленнее»? Когда вы последний раз упирались по производительности именно в PHP-код, а не в базу, память, свои кривые руки?

            Результат валидации — это или true или false. А вот что вызвало этот результат — вполне себе исключение. То есть ОЖИДАЕМОЕ и ПРЕДВИДИМОЕ нами событие.


            1. VolCh
              29.10.2015 14:14
              +1

              Результат валидации данных на наборе правил — это список (возможно пустой) невыполняющихся правил. $validator->isDataValid() лишь обёртка для empty($validator->getErrors()).

              Разработчик валидатора точно знает, что его основная задача вернуть список ошибок. И исключения он бросает, когда встречается с ситуацией, которую он предвидел, но не знает как с ней поступать — например, синтаксически некорректные или логически противоречивые правила, тип данных, которые он не знает как валидировать и т.п. Исключения должны бросаться в общем случае, когда не знаешь как поступать в той или иной ситуации. Разработчик валидатора должен знать как поступать в случае нарушения правила — добавить этот случай в список. Если какого-то клиента не интересует список, интересует только факт, если он знает только как быть, если ошибок нет, но не знает как быть, если ошибки есть, то он может сам бросить исключение.


              1. AlexLeonov
                29.10.2015 14:20
                -3

                исключения он бросает, когда встречается с ситуацией, которую он предвидел, но не знает как с ней поступать


                Разумеется, вы ошибаетесь.
                Но это, видимо, тема отдельной статьи.


    1. VolCh
      29.10.2015 11:58
      +2

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


      1. AlexLeonov
        29.10.2015 12:59
        -3

        Список ошибок — это коллекция исключений, возникших при валидации. Зачем же придумывать велосипед в стиле ->errors(), если есть нормальная объектная модель для этого?


        1. VolCh
          29.10.2015 14:17
          +3

          Это не нормальная модель, как минимум она избыточна — зачем мне в результате валидации данных трейсы с указанием в какой строке валидатора обнаружилась ошибка?


  1. postgree
    29.10.2015 11:56

    Такой подход неизбежно устарел в мире API и REST, все чаще приходиться работать с документообразными запросами со сложной структурой. Validate с самого начала был спроектирован как раз чтобы справляться с такими задачами.

    Имха со шпагой на перевес:
    1: Validate с самого начала был спроектирован для работы только с FullREST.
    2: Километр бизнес логики и 2 километра ФЛК убьет насмерть всю красоту и простоту.
    Я работал с моделью, на которую было наложено 114 требований (отличных от размера или нахождения в списке) по валидации данных. + были требования по оптимизации производительности всего этого добра, т.к. данные могли импортироваться, и в немалом количестве. И требования менялись регулярно. Это был мой маленький персональный ад.


    1. VolCh
      29.10.2015 12:15
      +2

      При количестве не структурируемых требований порядка сотни и больше (даже если они касаются только валидируемого объекта, не требуют, например, проверки в хранилище, что число членов агрегата не должно быть больше 10) и жесткими требованиями к производительности, красоты достичь обычно очень сложно. Или код — «плоский», с кучей вложенных ветвлений, но быстрый, или красивый, но медленный. Под красотой понимается, прежде всего, простота и скорость внесения изменений (включая добавление и удаление) в правила валидации.

      Исключение — декларативное описание правил на DSL (вплоть до описания самим бизнесом без привлечения разработчиков) и генерация страшного, но быстрого кода, самое позднее, при первом запросе на проверку соответствия данному набору правил, а лучше при деплое новой версии набора правил.


      1. postgree
        29.10.2015 15:29

        Исключение — декларативное описание правил на DSL (вплоть до описания самим бизнесом без привлечения разработчиков)

        Если честно, не встречал, но быстрое ознакомление с темой не до конца раскрыло проверки в хранилище. Да и отдавать на откуп бизнесу(? представителю заказчика) как? Ведь в идеальном мире мы должны проверить все входные параметры и недопустить исключения из-за ограничения проверки внешнего ключа. И т.д. и т.п. И выдать на все это нормальные предупреждения. Что требует как минимум знаний о всех связанных данных, помимо требований.
        Или код — «плоский», с кучей вложенных ветвлений, но быстрый, или красивый, но медленный.

        Т.е. варианта получается 3:
        1. Проверок мало или требований по производительности не выставляется — пользуемся разными валидаторами.
        2. Проверок много и есть требования по производительности — пишем быстрое спагетти.
        3. Проверок очень много и пишем свои велосипеды с преферансом и гимназистками. И пофиг на потерю производительности по сравнению со спагетти, в этих экскрементах хоть как то надо разбираться.


  1. KIVagant
    30.10.2015 18:32

    Я в своё время писал для этих целей либу: AMatch (часть 2) с более коротким синтаксисом. Но код там очень несовременный.