Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

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:.*'),
]);
версия: 2.0