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

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


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:

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

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é 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)`.

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


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

```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 [#struktury].

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

Lze definovat také indexované pole, známé jako tuple:

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

```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"]
```


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 znaků dlouhý, maximálně 20 znaků
Expect::string()->min(10)->max(20);
```

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

```php
// právě 9 čísel
Expect::string()->pattern('\d{9}');
```


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 tak přidat 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 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:

```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(...)
```


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']
```


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; číselně indexované položky (listy) se místo přepsání připojují jednu za druhou.


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 dostaneš hodně daleko, takže málokdy potřebuješ stavět něco od základu. Když ale chceš znovupoužitelný, samostatný prvek s vlastní logikou validace a slučování, vytvoříš 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íš přes objekt [Context |api:Nette\Schema\Context] pomocí `$context->addError()` a vrátíš `null`. `Processor` všechny chyby posbírá a vyhodí je na konci najednou.

Jako příklad vytvoříme znovupoužitelný prvek, který přijme backing hodnotu enumu (např. řetězec `'hearts'`) a vrátí instanci enumu:

```php
use Nette\Schema\Context;
use Nette\Schema\Schema;

class EnumSchema implements Schema
{
	public function __construct(
		private string $enumClass,
	) {
	}

	public function normalize(mixed $value, Context $context): mixed
	{
		return $value; // není potřeba žádné předzpracování
	}

	public function merge(mixed $value, mixed $base): mixed
	{
		return $value ?? $base; // pozdější hodnota vyhrává
	}

	public function complete(mixed $value, Context $context): mixed
	{
		$enum = is_string($value) ? ($this->enumClass)::tryFrom($value) : null;
		if ($enum === null) {
			$context->addError('The item %path% is not a valid value.', 'enum.value');
			return null;
		}

		return $enum;
	}

	public function completeDefault(Context $context): mixed
	{
		return null; // hodnota použitá, když položka ve vstupu chybí
	}
}
```

Použiješ ho kdekoli, kde se očekává vestavěný prvek – samostatně i jako součást větší struktury:

```php
enum Suit: string
{
	case Hearts = 'hearts';
	case Spades = 'spades';
}

$schema = Expect::structure([
	'suit' => new EnumSchema(Suit::class),
]);

$processor->process($schema, ['suit' => 'hearts']);
// OK, vrátí {'suit' => Suit::Hearts}
```

Protože prvek implementuje celé rozhraní, funguje automaticky i uvnitř `processMultiple()` – `Processor` volá jeho metodu `merge()` stejně jako u kteréhokoli jiného prvku.

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ě 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 = [])
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).

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 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(...)

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 = 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; číselně indexované položky (listy) se místo přepsání připojují jednu za druhou.

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 dostaneš hodně daleko, takže málokdy potřebuješ stavět něco od základu. Když ale chceš znovupoužitelný, samostatný prvek s vlastní logikou validace a slučování, vytvoříš 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íš přes objekt Context pomocí $context->addError() a vrátíš null. Processor všechny chyby posbírá a vyhodí je na konci najednou.

Jako příklad vytvoříme znovupoužitelný prvek, který přijme backing hodnotu enumu (např. řetězec 'hearts') a vrátí instanci enumu:

use Nette\Schema\Context;
use Nette\Schema\Schema;

class EnumSchema implements Schema
{
	public function __construct(
		private string $enumClass,
	) {
	}

	public function normalize(mixed $value, Context $context): mixed
	{
		return $value; // není potřeba žádné předzpracování
	}

	public function merge(mixed $value, mixed $base): mixed
	{
		return $value ?? $base; // pozdější hodnota vyhrává
	}

	public function complete(mixed $value, Context $context): mixed
	{
		$enum = is_string($value) ? ($this->enumClass)::tryFrom($value) : null;
		if ($enum === null) {
			$context->addError('The item %path% is not a valid value.', 'enum.value');
			return null;
		}

		return $enum;
	}

	public function completeDefault(Context $context): mixed
	{
		return null; // hodnota použitá, když položka ve vstupu chybí
	}
}

Použiješ ho kdekoli, kde se očekává vestavěný prvek – samostatně i jako součást větší struktury:

enum Suit: string
{
	case Hearts = 'hearts';
	case Spades = 'spades';
}

$schema = Expect::structure([
	'suit' => new EnumSchema(Suit::class),
]);

$processor->process($schema, ['suit' => 'hearts']);
// OK, vrátí {'suit' => Suit::Hearts}

Protože prvek implementuje celé rozhraní, funguje automaticky i uvnitř processMultiple() – Processor volá jeho metodu merge() stejně jako u kteréhokoli jiného prvku.