Валидация сложных структур с PHPixie Validate

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).

© Habrahabr.ru