Schema: validace dat
Praktická knihovna pro validaci a normalizaci datových struktur oproti danému schématu s chytrým srozumitelným API.
Instalace:
composer require nette/schema
Základní použití
V proměnné $schema
máme validační schéma (co to přesně znamená a jak takové schéma vytvořit si
řekneme vzápětí) a v proměnné $data
datovou strukturu, kterou chceme validovat a normalizovat. Může jít
například o data odeslaná uživatelem skrze rozhraní API, konfigurační soubor, atd.
Úkol obstará třída Nette\Schema\Processor, která zpracuje vstup a buď vrátí normalizovaná data, nebo v případě chyby vyhodí výjimku Nette\Schema\ValidationException.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data nejsou platná: ' . $e->getMessage();
}
Metoda $e->getMessages()
vrací pole všech zpráv jakožto řetězců a
$e->getMessageObjects()
vrací všechny zprávy jako objekty Nette\Schema\Message.
Definování schématu
A nyní vytvoříme schéma. K jeho definování slouží třída Nette\Schema\Expect, definujeme vlastně očekávání,
jak mají data vypadat. Řekněme, že vstupní data musí tvořit strukturu (například pole) obsahující prvky
processRefund
typu bool a refundAmount
typu int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Věříme, že definice schématu vypadá srozumitelně, i když ji vidíte úplně poprvé.
Pošleme k validaci následující data:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, projde validací
Výstupem, tedy hodnotou $normalized
, je objekt stdClass
. Pokud bychom chtěli, aby výstupem bylo
pole, doplníme schéma o přetypování Expect::structure([...])->castTo('array')
.
Všechny prvky struktury jsou volitelné a mají výchozí hodnotu null
. Příklad:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, projde validací
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
To, že výchozí hodnotou je null
, neznamená, že by se akceptovalo ve vstupních datech
'processRefund' => null
. Nikoliv, vstupem musí být boolean, tedy pouze true
nebo
false
. Povolit null
bychom museli explicitně pomoci Expect::bool()->nullable()
.
Položku lze učinit povinnou pomocí Expect::bool()->required()
. Výchozí hodnotu změníme třeba na
false
pomocí Expect::bool()->default(false)
nebo zkráceně Expect::bool(false)
.
A co kdybychom chtěli kromě booleanu akceptovat ještě 1
a 0
? Pak uvedeme výčet hodnot, které
navíc necháme normalizovat na 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
Nyní už znáte základy toho, jak se definuje schéma a jak se chovají jednotlivé prvky struktury. Nyní si ukážeme, jaké všechny další prvky lze při definici schématu použít.
Datové typy: type()
Ve schématu lze uvést všechny standardní datové typy PHP:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
A dále všechny typy, podporované třídou
Validators, například Expect::type('scalar')
nebo zkráceně Expect::scalar()
. Také názvy tříd
či rozhraní, například Expect::type('AddressEntity')
.
Lze použít i union zápis:
Expect::type('bool|string|array')
Výchozí hodnota je vždy null
s výjimkou pro array
a list
, kde je to prázdné pole.
(List je pole indexované podle vzestupné řady numerických klíčů od nuly, tedy neasociativní pole).
Pole hodnot: arrayOf() listOf()
Pole představuje příliš obecnou strukturu, užitečnější je specifikovat, jaké přesně může obsahovat prvky. Například pole, jehož prvky mohou být pouze řetězce:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // CHYBA: 123 není string
Druhým parametrem lze specifikovat klíče (od verze 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // CHYBA: 'a' není int
List je indexované pole:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // CHYBA: 123 není string
$processor->process($schema, ['key' => 'a']); // CHYBA: není list
$processor->process($schema, [1 => 'a', 0 => 'b']); // CHYBA: také není list
Parametrem může být i schéma, můžeme tedy zapsat:
Expect::arrayOf(Expect::bool())
Výchozí hodnota je prázdné pole. Pokud zadáte výchozí hodnotu, bude sloučena s předanými daty. To lze deaktivovat
pomocí mergeDefaults(false)
(od verze 1.1).
Výčet: anyOf()
anyOf()
představuje výčet hodnot nebo schémat, které může hodnota nabývat. Takto zapíšeme pole prvků,
které mohou být buď 'a'
, true
nebo null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // CHYBA: false tam nepatří
Prvky výčtu mohou být i schémata:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // CHYBA
Metoda anyOf()
přijímá varianty jako jednotlivé parametry, nikoliv pole. Pokud jí chcete předat pole hodnot,
použijte unpacking operátor anyOf(...$variants)
.
Výchozí hodnota je null
. Metodou firstIsDefault()
učiníme první prvek výchozí:
// výchozí je 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Struktury
Struktury jsou objekty s definovanými klíči. Každá z dvojic klíč ⇒ hodnota je označována jako „vlastnost“:
Struktury přijímají pole a objekty a vrací objekty stdClass
.
Ve výchozím nastavení jsou všechny vlastnosti volitelné a mají výchozí hodnotu null
. Povinné vlastnosti
můžete definovat pomocí required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // výchozí hodnota je null
]);
$processor->process($schema, ['optional' => '']);
// CHYBA: option 'required' is missing
$processor->process($schema, ['required' => 'foo']);
// OK, vrací {'required' => 'foo', 'optional' => null}
Pokud nechcete mít na výstupu vlastnosti s výchozí hodnotou, použijte skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, vrací {'required' => 'foo'}
Ačkoliv je null
výchozí hodnota vlastnosti optional
, ve vstupních datech povolený není
(hodnotou musí být string). Vlastnosti akceptující null
definujeme pomocí nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// CHYBA: 'optional' expects to be string, null given.
$processor->process($schema, ['nullable' => null]);
// OK, vrací {'optional' => null, 'nullable' => null}
Pole všech vlastností struktury vrací metoda getShape()
.
Ve výchozím nastavení nemohou být ve vstupních datech žádné položky navíc:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// CHYBA: Unexpected item 'additional'
Což můžeme změnit pomocí otherItems()
. Jako parametr uvedeme schéma, podle kterého se budou prvky navíc
validovat:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // CHYBA
Novou strukturu můžete vytvořit odvozením od jiné pomocí extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Pole
Pole s definovanými klíči. Platí pro něj vše co struktury.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // výchozí hodnota je null
]);
Lze definovat také indexované pole, známé jako tuple:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Zastaralé vlastnosti
Můžete označit vlastnost jako deprecated pomocí metody deprecated([string $message])
. Informace o ukončení
podpory jsou vrácena pomocí $processor->getWarnings()
:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('The item %path% is deprecated'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["The item 'old' is deprecated"]
Rozsahy: min() max()
Pomocí min()
a max()
lze u polí omezit počet prvků:
// pole, nejméně 10 položek, maximálně 20 položek
Expect::array()->min(10)->max(20);
U řetězců omezit jejich délku:
// řetězec, nejméně 10 znaků dlouhý, maximálně 20 znaků
Expect::string()->min(10)->max(20);
U čísel omezit jejich hodnotu:
// celé číslo, mezi 10 a 20 včetně
Expect::int()->min(10)->max(20);
Samozřejmě je možné uvést pouze min()
, nebo pouze max()
:
// řetězec maximálně 20 znaků
Expect::string()->max(20);
Regulární výrazy: pattern()
Pomocí pattern()
lze uvést regulární výraz, kterému musí odpovídat celý vstupní řetězec (tj.
jako by byl obalen znaky ^
a $
):
// právě 9 čísel
Expect::string()->pattern('\d{9}');
Vlastní omezení: assert()
Libovolná další omezení zadáme pomocí assert(callable $fn)
.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // počet musí být sudý
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // CHYBA: 3 není sudý počet
Nebo
Expect::string()->assert('is_file'); // soubor musí existovat
Ke každému omezení můžete přidat vlastní popis. Ten bude součástí chybové zprávy.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Even items in array');
$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Even items in array" for item with value array.
Metodu lze volat opakovaně a tak přidat více omezení. Lze ji prokládat s voláním transform()
a
castTo()
.
Transformace: transform()
Úspěšně zvalidovaná data lze upravovat pomocí vlastní funkce:
// převod na velká písmena:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Metodu lze volat opakovaně a tak přidat více transformací. Lze ji prokládat s voláním assert()
a
castTo()
. Operace se provedou v pořadí, v jakém jsou deklarovány:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // převod na velká písmena
Metoda transform()
může současně transformovat i validovat hodnotu. To je často jednodušší a méně
duplicitní než řetězení transform()
a assert()
. K tomuto účelu funkce obdrží objekt Context s metodou addError()
, kterou lze
použít k přidání informací o problémech s validací:
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);
});
Přetypování: castTo()
Úspěšně zvalidovaná data lze přetypovat:
Expect::scalar()->castTo('string');
Kromě nativních PHP typů lze přetypovávat i na třídy. Přitom se rozlišuje, zda jde o jednoduchou třídu bez kontruktoru, nebo třídu s konstruktorem. Pokud třída nemá konstruktor, vytvoří se její instance a všechny prvky struktury se zapíší do properties:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// vytvoří '$obj = new Info' a zapíše do $obj->processRefund a $obj->refundAmount
Pokud třída konstruktor má, prvky struktury se předají jako pojmenované parametry konstruktoru:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// vytvoří $obj = new Info(processRefund: ..., refundAmount: ...)
Přetypování v kombinaci se skalárním parametrem vytvoří objekt a hodnotu předá jako jediný parametr konstrukturu:
Expect::string()->castTo(DateTime::class);
// vytvoří new DateTime(...)
Normalizace: before()
Před samotnou validací lze data normalizovat pomocí metody before()
. Jako příklad uveďme prvek, který musí
být pole řetězců (například ['a', 'b', 'c']
), avšak přijímá vstup ve formě řetězce
a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK a vrátí ['a', 'b', 'c']
Mapování na objekty: from()
Schéma struktury si můžeme nechat vygenerovat ze třídy. Příklad:
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}
Podporovány jsou i anonymní třídy:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Protože informace získané z definice třídy nemusí být dostačující, můžete druhým parametrem doplnit prvkům vlastní schéma:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);