Nette Documentation Preview

syntax
Nette Schema
************

.[perex]
Praktická knihovna pro validaci a normalizaci datových struktur oproti danému schématu s chytrým srozumitelným API.

Instalace:

```shell
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 [api:Nette\Schema\Processor], která zpracuje vstup a buď vrátí normalizovaná data, nebo v případě chyby vyhodí výjimku [api:Nette\Schema\ValidationException].

```php
$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":https://api.nette.org/schema/master/Nette/Schema/Message.html.


Definování schématu
-------------------

A nyní vytvoříme schéma. K jeho definování slouží třída [api: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.

```php
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:

```php
$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:

```php
$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:

```php
$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:

```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 |utils:validators#Očekávané typy], 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:

```php
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())`. .{data-version:1.4.0}


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:

```php
$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):

```php
$schema = Expect::arrayOf('string', 'int');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // CHYBA: 'a' není int
```

List je indexované pole:

```php
$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: .{data-version:1.4.0}

```php
$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:

```php
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`:

```php
$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:

```php
$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í:

```php
// výchozí je 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
```


Výčtové typy: enum() .{data-version:1.4.0}
------------------------------------------

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:

```php
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 |#mapovani-na-objekty-from] 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()`:

```php
$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í:

```php
$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()`:

```php
$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()`:

```php
$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:

```php
$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:

```php
$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()`:

```php
$dog = Expect::structure([
	'name' => Expect::string(),
	'age' => Expect::int(),
]);

$dogWithBreed = $dog->extend([
	'breed' => Expect::string(),
]);
```


Pole .{data-version:1.3.2}
--------------------------

Pole s definovanými klíči. Platí pro něj vše, co pro [#struktury].

```php
$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // výchozí hodnota je null
]);
```


Tuple: tuple() .{data-version:1.4.0}
------------------------------------

Tuple je pole pevné velikosti, v němž má každá pozice své vlastní schéma:

```php
$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()`:

```php
$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() .{data-version:1.4.0}
------------------------------------------

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 |#export-do-json-schema]:

```php
$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ů:

```php
// pole, nejméně 10 položek, maximálně 20 položek
Expect::array()->min(10)->max(20);
```

U řetězců omezit jejich délku:

```php
// ř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:

```php
// 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()`:

```php
// ř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() |#vycet-anyof]: .{data-version:1.4.0}

```php
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 `$`):

```php
// 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í. .{data-version:1.4.0}


Vlastní omezení: assert()
-------------------------

Libovolná další omezení zadáme pomocí `assert(callable $fn)`.

```php
$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

```php
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.

```php
$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() .{data-version:1.2.5}
-----------------------------------------------

Úspěšně zvalidovaná data lze upravovat pomocí vlastní funkce:

```php
// 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:

```php
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 |api:Nette\Schema\Context] s metodou `addError()`, kterou lze použít k přidání informací o problémech s validací:

```php
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:

```php
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:

```php
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:

```php
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:

```php
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á: .{data-version:1.4.0}

```php
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. .{data-version:1.4.0}


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`:

```php
$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í. .{data-version:1.4.0}


Mapování na objekty: from()
---------------------------

Schéma struktury si můžeme nechat vygenerovat ze třídy. Příklad:

```php
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:

```php
$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:

```php
$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:

```php
$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()`: .{data-version:1.4.0}

```php
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ší: .{data-version:1.4.0}

```php
Expect::bool()->mergeWith(fn($value, $base) => $value || $base); // jednou zapnuto, zůstane zapnuto
```


Export do JSON Schema .{data-version:1.4.0}
-------------------------------------------

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":https://json-schema.org, tedy formát, který čtou editory, aby rovnou v JSON nebo YAML souboru nabízely doplňování a podtrhávaly chyby:

```php
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);
```

```json
{
	"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:

1. **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.
2. **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.
3. **complete()** - provede samotnou validaci, doplní výchozí hodnoty za chybějící položky a aplikuje `assert()`, `transform()` a `castTo()`. 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í [api:Nette\Schema\Schema]. Má přesně ty čtyři metody popsané výše:

```php
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 |api:Nette\Schema\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ů:

```php
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:

```php
$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`.

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:

  1. 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.
  2. 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.
  3. complete() – provede samotnou validaci, doplní výchozí hodnoty za chybějící položky a aplikuje assert(), transform() a castTo(). 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.