Vlastní prvky

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í, můžete si ho napsat. Tato stránka vysvětluje:

  • jak procesor s daty krok za krokem zachází
  • rozhraní, které implementuje každý prvek
  • jak napsat a použít vlastní prvek

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.

Rozhraní Schema

Vlastní prvek vytvoříte 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.

Příklad: velikosti v bajtech

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.

verze: 2.x 1.x