Nette Schema
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ě pomocí 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 = [])
Expect::list($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).
Ve verzi 1.x znamená typ '?int[]' pole, jehož položky jsou celá čísla nebo
null. Verze 2.0 ho bude číst stejně jako phpDoc, tedy jako pole celých čísel, nebo null místo
celého pole, a proto jeho použití vyvolá varování E_USER_DEPRECATED. Pokud chcete zachovat současný význam,
napište Expect::arrayOf(Expect::type('int')->nullable()).
Pole hodnot: arrayOf() listOf()
Pole představuje příliš obecnou strukturu, užitečnější je určit, jaké prvky přesně může obsahovat. 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 určit 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
S parametrem wrap se přijme i samostatná hodnota a zabalí se do listu o jedné
položce:
$schema = Expect::listOf('string', wrap: true);
$processor->process($schema, 'hello'); // OK, vrátí ['hello']
$processor->process($schema, ['a', 'b']); // OK
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ých 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).
Varianty se zkoušejí v pořadí, v jakém jsou zapsané, a vyhraje první, která vyhoví. Jedinou výjimkou je
null: pokud ho výčet přijímá, vstup null dá vždy null, i když by ho dříve
uvedená varianta se strukturou nebo polem jinak převedla na prázdnou strukturu či pole.
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();
Výčtové typy: enum()
Konfigurační soubor obsahuje jen prostý řetězec jako 'hearts', jenže v kódu byste mnohem raději pracovali
s enumem. Metoda enum() jedno převede na druhé: předáte jí enum, jehož case mají přiřazené hodnoty
(enum Suit: string), a ona přijme buď tuto hodnotu, nebo přímo case, a vždy vám vrátí case:
enum Suit: string
{
case Hearts = 'hearts';
case Spades = 'spades';
}
$schema = Expect::enum(Suit::class);
$processor->process($schema, 'hearts'); // OK, vrátí Suit::Hearts
$processor->process($schema, Suit::Spades); // OK, vrátí Suit::Spades
$processor->process($schema, 'clubs'); // CHYBA: expects to be Suit
Enumy s celočíselnými hodnotami fungují úplně stejně a platí pro ně vše, co znáte z ostatních prvků, třeba
Expect::enum(Suit::class)->required() nebo ->default(Suit::Hearts).
Při generování schématu ze třídy vám vlastnost
public Suit $suit dá právě tento prvek, takže tam po enum() sahat nemusíte.
Expect::type(Suit::class) je něco jiného: jako u každého názvu třídy přijme jen instanci, tedy
samotný case.
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: the mandatory item 'required' is missing
$processor->process($schema, ['required' => 'foo']);
// OK, vrací {'required' => 'foo', 'optional' => null}
Samotná struktura je povinná. Pokud je tedy vnořená v jiné struktuře a vstup ji neobsahuje, přesto se vytvoří – a
ohlásí chybu ve chvíli, kdy obsahuje povinnou vlastnost. Pomocí required(false) učiníte celou vnořenou
strukturu volitelnou. Když ve vstupu chybí, na výstupu bude null, ale pokud je přítomna, její povinné
vlastnosti se vyžadují:
$schema = Expect::structure([
'db' => Expect::structure([
'dsn' => Expect::string()->required(),
])->required(false),
]);
$processor->process($schema, []);
// OK, vrací {'db' => null}
$processor->process($schema, ['db' => []]);
// CHYBA: chybí 'db › dsn'
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 pro struktury.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // výchozí hodnota je null
]);
Tuple: tuple()
Tuple je pole pevné velikosti, v němž má každá pozice své vlastní schéma:
$schema = Expect::tuple([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Vstupem musí být list, takže pozice zadané klíči nebo v jiném pořadí se odmítnou. Když je vstup kratší,
chybějící pozice se chovají jako chybějící položky struktury: dostanou výchozí hodnotu, nebo nahlásí chybu, pokud jsou
required(). Metoda otherItems() dovolí, aby za definovanými pozicemi následovaly další položky, a
určí schéma, kterému musí odpovídat, kdežto extend() přidá na konec další pozice.
Když se konfigurace skládají ve vrstvách, pozdější tuple nahradí ten dřívější celý, protože smíchat pozice dvou různých tuplů by dalo jen nesmysl.
Na rozdíl od struktury nepřijímá tuple null jako prázdnou hodnotu; pokud je null platnou
hodnotou, zavolejte nullable().
Zastaralé vlastnosti
Můžete označit vlastnost jako deprecated pomocí metody deprecated([string $message]). Informace o ukončení
podpory jsou vráceny 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"]
Popis: description()
Každému prvku můžete přidat lidsky čitelný popis. Na validaci nemá vliv, jenže schéma bývá jediné místo, kde je
význam konfigurační volby vůbec zapsaný, a description() vám ho dovolí napsat přímo tam. Popis se také
propíše do exportu do JSON Schema:
$schema = Expect::structure([
'host' => Expect::string('localhost')->description('Database server hostname'),
'port' => Expect::int(3306)->description('Port the server listens on'),
])->description('Database connection');
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 bajtů dlouhý, maximálně 20 bajtů
Expect::string()->min(10)->max(20);
Délka řetězce se měří v bajtech. Pokud chcete u řetězce v UTF-8 počítat znaky, použijte
Expect::unicode().
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 bajtů
Expect::string()->max(20);
U unionu jako Expect::type('int|string') není jasné, zda mají min() a
max() omezovat číslo, nebo délku řetězce, proto tam vyvolají varování E_USER_DEPRECATED.
Omezení místo toho dejte jednotlivým variantám anyOf():
Expect::anyOf(Expect::int()->min(1), Expect::string()->min(1));
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}');
Regulární výraz má smysl jen u řetězce. Proto Expect::string() vrací prvek třídy
StringType, zatímco Expect::int() a Expect::float() vracejí NumberType a
Expect::listOf() i ostatní schémata polí ArrayType, a každý z nich nabízí metody svého druhu.
Volání pattern() u čísla nebo pole vyvolá varování E_USER_DEPRECATED a editor vás na zastaralé
volání upozorní hned při psaní.
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 přidat tak 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 přidat tak 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 konstruktoru, 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 konstruktoru:
Expect::string()->castTo(DateTime::class);
// vytvoří new DateTime(...)
Z hodnoty null se objekt nikdy nevytvoří. Pokud je cílem třída nebo enum,
null zůstane null, takže nevyplněná nepovinná položka zůstane prázdná:
Expect::string()->nullable()->castTo(DateTime::class);
// z '2026-01-01' bude DateTime, null zůstane null
U nativních typů přidejte otazník, pokud má null projít beze změny:
castTo('?string') hodnotu null zachová, kdežto castTo('string') z ní udělá prázdný
řetězec.
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']
Prvek má jedinou obslužnou funkci, takže druhé volání before() tu první nahradí. Celou normalizaci proto
umístěte do jedné funkce.
Druhé volání navíc vyvolá varování E_USER_DEPRECATED, protože ve verzi 2.0 se
obslužné funkce budou řetězit a každá dostane výsledek té předchozí.
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 = null;
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 = null;
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:.*'),
]);
Slučování více konfigurací
Aplikace často skládají konfiguraci ve vrstvách: existují vestavěné výchozí hodnoty a nad nimi uživatel dodává
vlastní nastavení, které má přepsat jen ty položky, jež skutečně uvede. Přesně to dělá
processMultiple() – vezme několik datových sad, sloučí je v pořadí tak, že pozdější mají přednost, a
výsledek zvaliduje jako celek:
$schema = Expect::structure([
'host' => Expect::string(),
'port' => Expect::int(),
'logging' => Expect::bool(),
]);
$defaults = ['host' => 'localhost', 'port' => 3306, 'logging' => false];
$userConfig = ['port' => 5432, 'logging' => true];
$config = $processor->processMultiple($schema, [$defaults, $userConfig]);
// $config = {'host' => 'localhost', 'port' => 5432, 'logging' => true}
Položka host si ponechá výchozí hodnotu, protože ji uživatel nenastavil, zatímco port a
logging přepíše pozdější datová sada. Hodnoty pod řetězcovými klíči se slučují tímto způsobem;
položky listu (listOf(), arrayOf()) se místo přepsání připojují jedna za druhou.
Pokud připojování nebo slučování podle klíčů není to, co chcete, mohou si pole a struktury
zvolit jinou strategii metodou mergeMode():
use Nette\Schema\MergeMode;
Expect::arrayOf('string')->mergeMode(MergeMode::Replace); // pozdější pole nahradí to dřívější
MergeMode::AppendKeys slučuje podle klíčů a položky listu připojuje, OverwriteKeys také
slučuje podle klíčů, ale položky listu přepisuje podle jejich pozice, a Replace ponechá jen pozdější
hodnotu. Bez mergeMode() se pole, jehož položky nejsou popsané, třeba Expect::array(), slučuje do
hloubky.
Pro cokoliv jiného je tu mergeWith(), kterému předáte vlastní funkci, jež pozdější
hodnotu zkombinuje s tou dřívější:
Expect::bool()->mergeWith(fn($value, $base) => $value || $base); // jednou zapnuto, zůstane zapnuto
Export do JSON Schema
Vaše schéma už je přesným popisem toho, co smí konfigurace obsahovat, a ten popis se hodí i mimo PHP. Metoda
JsonSchema::export() z něj vytvoří dokument JSON Schema, tedy formát,
který čtou editory, aby rovnou v JSON nebo YAML souboru nabízely doplňování a podtrhávaly chyby:
use Nette\Schema\JsonSchema;
$schema = Expect::structure([
'host' => Expect::string('localhost')->description('Database server hostname'),
'port' => Expect::int(3306)->min(1)->max(65535),
'user' => Expect::string()->required(),
]);
echo json_encode(JsonSchema::export($schema), JSON_PRETTY_PRINT);
{
"type": "object",
"properties": {
"host": {"type": "string", "description": "Database server hostname"},
"port": {"type": "integer", "minimum": 1, "maximum": 65535},
"user": {"type": "string"}
},
"required": ["user"],
"additionalProperties": false
}
Export popisuje tvar dat: typy, rozsahy, regulární výrazy, povinné položky, hodnoty výčtů a texty, které jste
přidali metodou description(). Výchozí hodnoty, castTo() a transform() v něm
záměrně nejsou, protože popisují, co se s daty děje až po validaci, a to JSON Schema vyjádřit neumí.
Typ, který existuje jen v PHP, třeba třídu (Expect::type(Address::class)), callable nebo resource, exportovat
vůbec nelze. Místo dokumentu, který by potichu popisoval něco jiného, export vyhodí
Nette\NotSupportedException.
Mezi exportem a validací zůstávají dva rozdíly. JSON Schema měří délku řetězce ve znacích, kdežto
Expect::string() v bajtech, proto pro text, který není čisté ASCII, použijte Expect::unicode(). A
pattern() se exportuje tak, jak je, ale validátor JSON Schema ho čte jako regulární výraz ECMAScriptu, takže
konstrukce specifické pro PCRE tam mohou být pochopeny jinak.
Jak zpracování probíhá
Každý prvek schématu – ať vestavěný, nebo vlastní – implementuje čtyři metody, které dohromady určují, jak nakládá s daty. Tři z nich tvoří zpracovatelskou pipeline:
- normalize() – připraví surový vstup. Tady běží háčky
before()a tady se například objekt převede na pole. Spouští se jako první, zvlášť pro každou datovou sadu. - merge() – sloučí dvě už normalizované datové sady, přičemž pozdější má přednost. Tento krok využívá
jen
processMultiple();process()ho přeskakuje, protože má jen jednu datovou sadu. - complete() – provede samotnou validaci, doplní výchozí hodnoty za chybějící položky a aplikuje
assert(),transform()acastTo(). Běží jako poslední, nad sloučeným výsledkem.
Čtvrtou metodu, completeDefault(), volá nadřazený prvek pro položku, jež ve vstupu úplně chybí – buď dodá
výchozí hodnotu, nebo ohlásí, že chybí required() položka.
process() tedy provádí normalize → complete, zatímco processMultiple() provádí
normalize (každou sadu) → merge → complete. Kvůli tomuto pořadí vidí before() surový vstup,
kdežto transform() už zvalidovanou hodnotu.
Vlastní prvky schématu
S assert(), transform() a before() se dostanete hodně daleko, takže málokdy
potřebujete stavět něco od základu. Když ale chcete znovupoužitelný, samostatný prvek s vlastní logikou validace a
slučování, vytvoříte ho implementací rozhraní Nette\Schema\Schema. Má přesně ty čtyři metody
popsané výše:
interface Schema
{
function normalize(mixed $value, Context $context);
function merge(mixed $value, mixed $base);
function complete(mixed $value, Context $context);
function completeDefault(Context $context);
}
Chyby se nevyhazují; místo toho je hlásíte přes objekt Context pomocí $context->addError() a
vrátíte null. Processor všechny chyby posbírá a vyhodí je na konci najednou.
Jako příklad vytvoříme znovupoužitelný prvek pro velikosti zapsané tak, jak je lidé v konfiguraci píší nejraději,
třeba '512kB' nebo '2 MB'. Prvek je zvaliduje a vrátí počet bajtů:
use Nette\Schema\Context;
use Nette\Schema\Schema;
class ByteSizeSchema implements Schema
{
private const Units = ['b' => 1, 'kb' => 1024, 'mb' => 1024 ** 2, 'gb' => 1024 ** 3];
public function normalize(mixed $value, Context $context): mixed
{
return is_int($value) ? "$value B" : $value; // sjednotíme oba zápisy
}
public function merge(mixed $value, mixed $base): mixed
{
return $value ?? $base; // pozdější hodnota vyhrává
}
public function complete(mixed $value, Context $context): mixed
{
if (!is_string($value) || !preg_match('~^(\d+)\s*([a-zA-Z]+)$~', $value, $m)) {
$context->addError("The item %path% expects to be a size like '512kB'.", 'size.format');
return null;
}
$unit = self::Units[strtolower($m[2])] ?? null;
if ($unit === null) {
$context->addError("The item %path% uses an unknown unit '$m[2]'.", 'size.unit');
return null;
}
return (int) $m[1] * $unit;
}
public function completeDefault(Context $context): mixed
{
return null; // hodnota použitá, když položka ve vstupu chybí
}
}
Použijete ho kdekoli, kde se očekává vestavěný prvek – samostatně i jako součást větší struktury:
$schema = Expect::structure([
'maxUpload' => new ByteSizeSchema,
'diskQuota' => new ByteSizeSchema,
]);
$processor->process($schema, ['maxUpload' => '2 MB', 'diskQuota' => 512]);
// OK, vrátí {'maxUpload' => 2097152, 'diskQuota' => 512}
$processor->process($schema, ['maxUpload' => '5 TB']);
// CHYBA: The item 'maxUpload' uses an unknown unit 'TB'.
Protože prvek implementuje celé rozhraní, funguje automaticky i uvnitř processMultiple() –
Processor volá jeho metodu merge() stejně jako u kteréhokoli jiného prvku. Jen
JsonSchema::export() nemůže vědět, co takový prvek přijímá, a proto vyhodí
Nette\NotSupportedException.