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
в
допълнение към булевите числа? Нека изброим валидните стойности, които
също нормализираме в булеви:
$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()
, за да направите първия елемент по подразбиране:
// по подразбиране е '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']);
// ОК, връща {'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]);
// ОК, връща {'optional' => null, 'nullable' => null}
Масивът от всички свойства на структурата се връща от метода
getShape()
.
По подразбиране във входните данни не може да има допълнителни елементи:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ERROR: Неочакван елемент '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']); // ERROR: 3 - нечетно число
Или
Expect::string()->assert('is_file'); // файлът трябва да съществува
Можете да добавите собствено описание за всяко твърдение. Това ще бъде част от съобщението за грешка.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Even elements in array');
$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: ...)
Casting в комбинация със скаларен параметър създава обект и предава стойността като единствен параметър на конструктора:
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
Нормализация: преди()
Преди самото валидиране данните могат да бъдат нормализирани с
помощта на метода 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:.*'),
]);