Schema: валидация данных
Практичная библиотека для валидации и нормализации структур данных по заданной схеме с умным и понятным API.
Установка:
composer require nette/schema
Основное использование
В переменной $schema
у нас есть схема валидации (что это точно
означает и как создать такую схему, мы расскажем вскоре), а в переменной
$data
— структура данных, которую мы хотим валидировать и
нормализовать. Это могут быть, например, данные, отправленные
пользователем через API, конфигурационный файл и т.д.
Задачу выполнит класс Nette\Schema\Processor, который обработает входные данные и либо вернет нормализованные данные, либо в случае ошибки выбросит исключение Nette\Schema\ValidationException.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Данные недействительны: ' . $e->getMessage();
}
Метод $e->getMessages()
возвращает массив всех сообщений в виде
строк, а $e->getMessageObjects()
возвращает все сообщения как объекты Nette\Schema\Message.
Определение схемы
А теперь создадим схему. Для ее определения служит класс Nette\Schema\Expect, мы фактически
определяем ожидания, как должны выглядеть данные. Скажем, входные
данные должны образовывать структуру (например, массив), содержащую
элементы processRefund
типа bool и refundAmount
типа int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Мы верим, что определение схемы выглядит понятно, даже если вы видите его впервые.
Отправим на валидацию следующие данные:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, пройдет валидацию
Выводом, то есть значением $normalized
, является объект stdClass
.
Если бы мы хотели, чтобы выводом был массив, мы бы дополнили схему
приведением типов Expect::structure([...])->castTo('array')
.
Все элементы структуры являются необязательными и имеют значение по
умолчанию null
. Пример:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, пройдет валидацию
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
То, что значением по умолчанию является null
, не означает, что во
входных данных будет принято 'processRefund' => null
. Нет, входными
данными должен быть boolean, то есть только true
или false
.
Разрешить null
нам пришлось бы явно с помощью
Expect::bool()->nullable()
.
Элемент можно сделать обязательным с помощью Expect::bool()->required()
.
Значение по умолчанию изменим, например, на false
с помощью
Expect::bool()->default(false)
или сокращенно Expect::bool(false)
.
А что, если бы мы хотели помимо boolean принимать еще 1
и 0
?
Тогда мы укажем перечисление значений, которые к тому же нормализуем в
boolean:
$schema = Expect::structure([
'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'),
'refundAmount' => Expect::int(),
]);
$normalized = $processor->process($schema, $data);
is_bool($normalized->processRefund); // true
Теперь вы знаете основы того, как определяется схема и как ведут себя отдельные элементы структуры. Теперь мы покажем, какие еще элементы можно использовать при определении схемы.
Типы данных: type()
В схеме можно указать все стандартные типы данных PHP:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
А также все типы, поддерживаемые классом Validators,
например Expect::type('scalar')
или сокращенно Expect::scalar()
. Также
имена классов или интерфейсов, например Expect::type('AddressEntity')
.
Можно использовать и union-запись:
Expect::type('bool|string|array')
Значение по умолчанию всегда null
, за исключением array
и
list
, где это пустой массив. (List — это массив, индексированный по
возрастающей последовательности числовых ключей от нуля, то есть
неассоциативный массив).
Массив значений: arrayOf() listOf()
Массив представляет собой слишком общую структуру, полезнее указать, какие именно элементы он может содержать. Например, массив, элементы которого могут быть только строками:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ОШИБКА: 123 не является строкой
Вторым параметром можно указать ключи (с версии 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ОШИБКА: 'a' не является int
List — это индексированный массив:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ОШИБКА: 123 не является строкой
$processor->process($schema, ['key' => 'a']); // ОШИБКА: не является списком
$processor->process($schema, [1 => 'a', 0 => 'b']); // ОШИБКА: также не является списком
Параметром может быть и схема, таким образом, мы можем записать:
Expect::arrayOf(Expect::bool())
Значение по умолчанию — пустой массив. Если вы зададите значение по
умолчанию, оно будет объединено с переданными данными. Это можно
деактивировать с помощью mergeDefaults(false)
(с версии 1.1).
Перечисление: anyOf()
anyOf()
представляет собой перечисление значений или схем,
которые может принимать значение. Так мы запишем массив элементов,
которые могут быть либо 'a'
, true
или null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ОШИБКА: false здесь недопустимо
Элементы перечисления могут быть и схемами:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ОШИБКА
Метод anyOf()
принимает варианты как отдельные параметры, а не
массив. Если вы хотите передать ему массив значений, используйте
оператор распаковки anyOf(...$variants)
.
Значение по умолчанию — null
. Методом firstIsDefault()
мы
сделаем первый элемент значением по умолчанию:
// значение по умолчанию 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Структуры
Структуры — это объекты с определенными ключами. Каждая из пар ключ ⇒ значение называется «свойством»:
Структуры принимают массивы и объекты и возвращают объекты
stdClass
.
По умолчанию все свойства являются необязательными и имеют значение
по умолчанию null
. Обязательные свойства можно определить с
помощью required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // значение по умолчанию null
]);
$processor->process($schema, ['optional' => '']);
// ОШИБКА: опция 'required' отсутствует
$processor->process($schema, ['required' => 'foo']);
// OK, возвращает {'required' => 'foo', 'optional' => null}
Если вы не хотите иметь в выводе свойства со значением по умолчанию,
используйте skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, возвращает {'required' => 'foo'}
Хотя null
является значением по умолчанию для свойства
optional
, во входных данных он не разрешен (значением должна быть
строка). Свойства, принимающие null
, определяем с помощью
nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// ОШИБКА: 'optional' ожидает строку, получен null.
$processor->process($schema, ['nullable' => null]);
// OK, возвращает {'optional' => null, 'nullable' => null}
Массив всех свойств структуры возвращает метод getShape()
.
По умолчанию во входных данных не может быть лишних элементов:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ОШИБКА: Неожиданный элемент 'additional'
Что мы можем изменить с помощью otherItems()
. В качестве параметра
укажем схему, по которой будут валидироваться дополнительные
элементы:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ОШИБКА
Новую структуру можно создать, унаследовав от другой с помощью
extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Массив
Массив с определенными ключами. К нему применимо все то же, что и к структурам.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // значение по умолчанию null
]);
Можно определить также индексированный массив, известный как кортеж (tuple):
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Устаревшие свойства
Вы можете пометить свойство как устаревшее (deprecated) с помощью метода
deprecated([string $message])
. Информация о прекращении поддержки
возвращается с помощью $processor->getWarnings()
:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('Элемент %path% устарел'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Элемент 'old' устарел"]
Диапазоны: min() max()
С помощью min()
и max()
можно у массивов ограничить
количество элементов:
// массив, минимум 10 элементов, максимум 20 элементов
Expect::array()->min(10)->max(20);
У строк ограничить их длину:
// строка длиной не менее 10 символов, не более 20 символов
Expect::string()->min(10)->max(20);
У чисел ограничить их значение:
// целое число, от 10 до 20 включительно
Expect::int()->min(10)->max(20);
Конечно, можно указать только min()
, или только max()
:
// строка не более 20 символов
Expect::string()->max(20);
Регулярные выражения: pattern()
С помощью pattern()
можно указать регулярное выражение, которому
должна соответствовать вся входная строка (то есть, как если бы она
была заключена в символы ^
и $
):
// ровно 9 цифр
Expect::string()->pattern('\d{9}');
Пользовательские ограничения: assert()
Любые другие ограничения зададим с помощью assert(callable $fn)
.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // количество должно быть четным
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ОШИБКА: 3 - нечетное количество
Или
Expect::string()->assert('is_file'); // файл должен существовать
К каждому ограничению можно добавить собственное описание. Оно будет частью сообщения об ошибке.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Четное количество элементов в массиве');
$processor->process($schema, ['a', 'b', 'c']);
// Не пройдено утверждение "Четное количество элементов в массиве" для элемента со значением array.
Метод можно вызывать повторно и так добавить несколько ограничений.
Его можно чередовать с вызовами transform()
и castTo()
.
Трансформации: transform()
Успешно провалидированные данные можно изменять с помощью пользовательской функции:
// преобразование в верхний регистр:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Метод можно вызывать повторно и так добавить несколько
трансформаций. Его можно чередовать с вызовами assert()
и
castTo()
. Операции выполняются в том порядке, в котором они
объявлены:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'Все символы должны быть в нижнем регистре')
->transform(fn(string $s) => strtoupper($s)); // преобразование в верхний регистр
Метод transform()
может одновременно трансформировать и
валидировать значение. Это часто проще и менее избыточно, чем цепочка
вызовов transform()
и assert()
. Для этой цели функция получает
объект Context с методом
addError()
, который можно использовать для добавления информации о
проблемах валидации:
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('Все символы должны быть в нижнем регистре', 'my.case.error');
return null;
}
return strtoupper($s);
});
Приведение типов: castTo()
Успешно провалидированные данные можно привести к типу:
Expect::scalar()->castTo('string');
Помимо встроенных типов PHP, можно приводить и к классам. При этом различается, является ли это простым классом без конструктора, или классом с конструктором. Если у класса нет конструктора, создается его экземпляр и все элементы структуры записываются в свойства:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// создаст '$obj = new Info' и запишет в $obj->processRefund и $obj->refundAmount
Если у класса есть конструктор, элементы структуры передаются как именованные параметры конструктору:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// создаст $obj = new Info(processRefund: ..., refundAmount: ...)
Приведение типов в сочетании со скалярным параметром создает объект и передает значение как единственный параметр конструктору:
Expect::string()->castTo(DateTime::class);
// создаст new DateTime(...)
Нормализация: before()
Перед самой валидацией данные можно нормализовать с помощью метода
before()
. В качестве примера приведем элемент, который должен быть
массивом строк (например, ['a', 'b', 'c']
), но принимает ввод в виде
строки a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK и вернет ['a', 'b', 'c']
Отображение на объекты: from()
Схему структуры можно сгенерировать из класса. Пример:
class Config
{
public string $name;
public string|null $password;
public bool $admin = false;
}
$schema = Expect::from(new Config);
$data = [
'name' => 'franta',
];
$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'franta', 'password' => null, 'admin' => false}
Поддерживаются и анонимные классы:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Поскольку информации, полученной из определения класса, может быть недостаточно, вы можете вторым параметром дополнить элементы собственной схемой:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);