Валидация сложных структур с PHPixie Validate
Сегодня вышел еще один компонент 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).