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); // ОК, проходить
Вихід, тобто значенням $normalized
, є об'єкт stdClass
. Якщо ми
хочемо, щоб результатом був масив, ми додаємо приведення до схеми
Expect::structure([...])->castTo('array')
.
Усі елементи структури є необов'язковими і мають значення за
замовчуванням null
. Приклад:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // ОК, проходить
// $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']); // ОК
$processor->process($schema, ['a' => 'hello']); // ПОМИЛКА: 'a' не int
Список являє собою індексований масив:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 не є рядком
$processor->process($schema, ['key' => 'a']); // ERROR: не список
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: не список
Параметр також може бути схемою, тому ми можемо написати:
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']); // ОК
$processor->process($schema, ['a', false]); // ПОМИЛКА: false тут не місце
Елементи перерахування також можуть бути схемами:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // ОК
$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(), // 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:.*'),
]);