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 'Data is invalid: ' . $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
. Нет,
входные данные должны быть булевыми, т. е. только true
или
false
. Нам пришлось бы явно разрешить null
через
Expect::bool()->nullable()
.
Элемент можно сделать обязательным, используя
Expect::bool()->required()
. Мы меняем значение по умолчанию на false
,
используя Expect::bool()->default(false)
или коротко Expect::bool(false)
.
А что если мы захотим принимать 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 = [])
А затем все типы, поддерживаемые валидаторами
через Expect::type('scalar')
или сокращенно Expect::scalar()
. Также
принимаются имена классов или интерфейсов, например:
Expect::type('AddressEntity')
.
Вы также можете использовать нотацию объединения:
Expect::type('bool|string|array')
Значение по умолчанию всегда null
, за исключением array
и
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
Список представляет собой индексированный массив:
$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()
, чтобы сделать первый элемент элементом по
умолчанию:
// default is '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(), // default value is null
]);
Вы также можете определить индексированный массив, известный как кортеж:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Устаревшие элементы
Вы можете объявить свойство устаревшим, используя метод
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()
, вы можете указать регулярное выражение,
которому должна соответствовать вся входная строка (т.е. как если
бы она была завернута в символы ^
a $
):
// только 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']);
// Неудачное утверждение "Четные элементы в массиве" для элемента с массивом значений.
Этот метод можно вызывать многократно для добавления нескольких
ограничений. Его можно смешивать с вызовами transform()
и
castTo()
.
Трансформация: transform()
Успешно подтвержденные данные могут быть изменены с помощью пользовательской функции:
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Этот метод можно вызывать многократно для добавления нескольких
преобразований. Он может перемежаться с вызовами assert()
и
castTo()
. Операции будут выполняться в том порядке, в котором они
объявлены:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase
Метод transform()
может одновременно преобразовывать и проверять
значение. Это часто проще и менее избыточно, чем выстраивание цепочек
transform()
и assert()
. Для этого функция получает объект Context с методом addError()
,
который может быть использован для добавления информации о проблемах
валидации:
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('All characters must be lowercased', '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);
// creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount
Если класс имеет конструктор, то элементы структуры передаются конструктору в качестве именованных параметров:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
Кастинг в сочетании со скалярным параметром создает объект и передает его значение в качестве единственного параметра конструктору:
Expect::string()->castTo(DateTime::class);
// creates 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' => 'jeff',
];
$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'jeff', '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:.*'),
]);