Shema: Potrjevanje podatkov

Praktična knjižnica za potrjevanje in normalizacijo podatkovnih struktur glede na dano shemo s pametnim in razumljivim API-jem.

Namestitev:

composer require nette/schema

Osnovna uporaba

V spremenljivki $schema imamo shemo potrjevanja (kaj točno to pomeni in kako jo ustvariti, bomo povedali pozneje), v spremenljivki $data pa imamo podatkovno strukturo, ki jo želimo potrditi in normalizirati. To so lahko na primer podatki, ki jih uporabnik pošlje prek vmesnika API, konfiguracijske datoteke itd.

Nalogo opravi razred Nette\Schema\Processor, ki obdela vhodne podatke in vrne normalizirane podatke ali pa ob napaki vrže izjemo Nette\Schema\ValidationException.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Data is invalid: ' . $e->getMessage();
}

Metoda $e->getMessages() vrne polje vseh nizov sporočil, metoda $e->getMessageObjects() pa vrne vsa sporočila kot objekte Nette\Schema\Message.

Opredelitev sheme

Zdaj pa ustvarimo shemo. Za njeno opredelitev uporabljamo razred Nette\Schema\Expect, s katerim pravzaprav definiramo pričakovanja, kako naj bi bili podatki videti. Recimo, da morajo biti vhodni podatki struktura (npr. polje), ki vsebuje elemente processRefund tipa bool in refundAmount tipa int.

use Nette\Schema\Expect;

$schema = Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
]);

Verjamemo, da je opredelitev sheme videti jasna, tudi če jo vidite prvič.

V potrditev pošljimo naslednje podatke:

$data = [
	'processRefund' => true,
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // V redu, je uspešno.

Izhodni podatek, tj. vrednost $normalized, je objekt stdClass. Če želimo, da je izhodni podatek polje, dodamo cast v shemo Expect::structure([...])->castTo('array').

Vsi elementi strukture so neobvezni in imajo privzeto vrednost null. Primer:

$data = [
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // V redu, je uspešno.
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

Dejstvo, da je privzeta vrednost null, še ne pomeni, da bi bila sprejeta v vhodnih podatkih 'processRefund' => null. Ne, vhodni podatki morajo biti logične vrednosti, torej samo true ali false. Izrecno bi morali dovoliti null prek Expect::bool()->nullable().

Element lahko postane obvezen z uporabo Expect::bool()->required(). Privzeto vrednost spremenimo v false z uporabo Expect::bool()->default(false) ali kratko s Expect::bool(false).

Kaj pa, če bi poleg boolov želeli sprejeti tudi 1 and 0? Potem naštejemo dovoljene vrednosti, ki jih bomo prav tako normalizirali v 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

Zdaj poznate osnove, kako je shema opredeljena in kako se obnašajo posamezni elementi strukture. Zdaj bomo pokazali, katere vse druge elemente je mogoče uporabiti pri opredeljevanju sheme.

Podatkovni tipi: type()

V shemi so lahko navedeni vsi standardni podatkovni tipi PHP:

Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])

Nato pa še vse tipe, ki jih podpirajo validatorji prek Expect::type('scalar') ali skrajšano Expect::scalar(). Sprejemljiva so tudi imena razredov ali vmesnikov, npr. Expect::type('AddressEntity').

Uporabite lahko tudi zapis union:

Expect::type('bool|string|array')

Privzeta vrednost je vedno null, razen za array in list, kjer je to prazno polje. (Seznam je polje, indeksirano v naraščajočem vrstnem redu številskih ključev od nič, torej neasociativno polje).

Polje vrednosti: arrayOf() listOf()

Polje je preveč splošna struktura, zato je koristneje natančno določiti, katere elemente lahko vsebuje. Na primer polje, katerega elementi so lahko samo nizi:

$schema = Expect::arrayOf('string');

$processor->process($schema, ['hello', 'world']); // V REDU
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERROR: 123 ni niz

Drugi parameter se lahko uporabi za določitev ključev (od različice 1.2):

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

$processor->process($schema, ['hello', 'world']); // V REDU
$processor->process($schema, ['a' => 'hello']); // NAPAKA: 'a' ni int

Seznam je indeksirano polje:

$schema = Expect::listOf('string');

$processor->process($schema, ['a', 'b']); // V REDU
$processor->process($schema, ['a', 123]); // NAPAKA: 123 ni niz
$processor->process($schema, ['key' => 'a']); // NAPAKA: ni seznam
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: ni seznam

Parameter je lahko tudi shema, tako da lahko zapišemo:

Expect::arrayOf(Expect::bool())

Privzeta vrednost je prazno polje. Če določite privzeto vrednost, bo združena s posredovanimi podatki. To lahko onemogočite z uporabo mergeDefaults(false) (od različice 1.1).

Izštevanje: anyOf()

anyOf() je nabor vrednosti ali shem, ki jih lahko ima vrednost. Tukaj je prikazano, kako zapisati polje elementov, ki so lahko 'a', true ali null:

$schema = Expect::listOf(
	Expect::anyOf('a', true, null),
);

$processor->process($schema, ['a', true, null, 'a']); // V REDU
$processor->process($schema, ['a', false]); // ERROR: false ne sodi tja

Elementi naštevanja so lahko tudi sheme:

$schema = Expect::listOf(
	Expect::anyOf(Expect::string(), true, null),
);

$processor->process($schema, ['foo', true, null, 'bar']); // V REDU
$processor->process($schema, [123]); // ERROR

Metoda anyOf() sprejema variante kot posamezne parametre in ne kot polje. Če ji želite posredovati polje vrednosti, uporabite operator razpakiranja anyOf(...$variants).

Privzeta vrednost je null. Če želite, da je prvi element privzet, uporabite metodo firstIsDefault():

// privzeto je 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Strukture

Strukture so predmeti z določenimi ključi. Vsak od teh parov ključ ⇒ vrednost se imenuje “lastnost”:

Strukture sprejemajo polja in predmete ter vračajo predmete stdClass (razen če jih spremenite s castTo('array'), itd.).

Privzeto so vse lastnosti neobvezne in imajo privzeto vrednost null. Obvezne lastnosti lahko določite z uporabo required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // zasebna vrednost je nič.
]);

$processor->process($schema, ['optional' => '']);
// ERROR: manjka možnost 'required'

$processor->process($schema, ['required' => 'foo']);
// V redu, vrne {'required' => 'foo', 'optional' => null}

Če ne želite izpisati lastnosti samo s privzeto vrednostjo, uporabite skipDefaults():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(),
])->skipDefaults();

$processor->process($schema, ['required' => 'foo']);
// V redu, vrne {'required' => 'foo'}

Čeprav je null privzeta vrednost lastnosti optional, je v vhodnih podatkih ni dovoljeno uporabljati (vrednost mora biti niz). Lastnosti, ki sprejemajo null, so opredeljene z uporabo nullable():

$schema = Expect::structure([
	'optional' => Expect::string(),
	'nullable' => Expect::string()->nullable(),
]);

$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' pričakuje, da bo niz, podana je ničla.

$processor->process($schema, ['nullable' => null]);
// V redu, vrne {'optional' => null, 'nullable' => null}

Privzeto je, da v vhodnih podatkih ne sme biti nobenih dodatnih elementov:

$schema = Expect::structure([
	'key' => Expect::string(),
]);

$processor->process($schema, ['additional' => 1]);
// ERROR: Nepričakovan element 'additional'

To lahko spremenimo s otherItems(). Kot parameter bomo določili shemo za vsak dodatni element:

$schema = Expect::structure([
	'key' => Expect::string(),
])->otherItems(Expect::int());

$processor->process($schema, ['additional' => 1]); // V REDU
$processor->process($schema, ['additional' => true]); // ERROR

Deprecations

Lastnost lahko izločite z uporabo deprecated([string $message]) metodo. Obvestila o izločitvi vrne $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"]

Razponi: min() max()

Uporabite min() in max() za omejitev števila elementov za polja:

// polje, vsaj 10 elementov, največ 20 elementov
Expect::array()->min(10)->max(20);

Za nize omejite njihovo dolžino:

// niz, dolg vsaj 10 znakov, največ 20 znakov
Expect::string()->min(10)->max(20);

Pri številkah omejite njihovo vrednost:

// celo število, med 10 in vključno 20
Expect::int()->min(10)->max(20);

Seveda je mogoče navesti samo min() ali samo max():

// niz, največ 20 znakov
Expect::string()->max(20);

Regularni izrazi: pattern()

Z uporabo pattern() lahko določite regularni izraz, ki mu mora ustrezati celoten vhodni niz (tj. kot da bi bil ovit v znake ^ a $):

// samo 9 številk
Expect::string()->pattern('\d{9}');

Trditve po meri: assert()

Vse druge omejitve lahko dodate z uporabo spletne strani assert(callable $fn).

$countIsEven = fn($v) => count($v) % 2 === 0;

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // število mora biti sodo.

$processor->process($schema, ['a', 'b']); // V REDU
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 ni sodo

Ali .

Expect::string()->assert('is_file'); // datoteka mora obstajati

Za vsako trditev lahko dodate svoj opis. Ta bo del sporočila o napaki.

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Even items in array');

$processor->process($schema, ['a', 'b', 'c']);
// Neuspešna trditev "Even items in array" za element z vrednostjo array.

Metodo lahko večkrat pokličete, če želite dodati več omejitev. Lahko se meša s klici na transform() in castTo().

Transformacija: transform()

Uspešno potrjene podatke lahko spremenite s funkcijo po meri:

// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));

Metodo je mogoče poklicati večkrat, da se doda več preoblikovanj. Lahko se meša s klici na assert() in castTo(). Operacije se izvedejo v vrstnem redu, v katerem so deklarirane:

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'All characters must be lowercased')
	->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase

Metoda transform() lahko hkrati preoblikuje in potrdi vrednost. To je pogosto preprostejše in manj odvečno kot veriženje metod transform() in assert(). V ta namen funkcija prejme objekt Context z metodo addError(), ki jo lahko uporabimo za dodajanje informacij o težavah pri potrjevanju:

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);
	});

Oddajanje: castTo()

Uspešno potrjene podatke je mogoče oddati:

Expect::scalar()->castTo('string');

Poleg izvornih tipov PHP lahko podatke prelijete tudi v razrede. Razlikuje, ali gre za preprost razred brez konstruktorja ali razred s konstruktorjem. Če razred nima konstruktorja, se ustvari njegov primerek, vsi elementi strukture pa se zapišejo v njegove lastnosti:

class Info
{
	public bool $processRefund;
	public int $refundAmount;
}

Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
])->castTo(Info::class);

// creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount

Če ima razred konstruktor, se elementi strukture konstruktorju posredujejo kot poimenovani parametri:

class Info
{
	public function __construct(
		public bool $processRefund,
		public int $refundAmount,
	) {
	}
}

// creates $obj = new Info(processRefund: ..., refundAmount: ...)

Casting v kombinaciji s skalarnim parametrom ustvari objekt in konstruktorju posreduje vrednost kot edini parameter:

Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)

Normalizacija: before()

Pred samim preverjanjem lahko podatke normaliziramo z metodo before(). Kot primer imejmo element, ki mora biti polje nizov (npr. ['a', 'b', 'c']), vendar prejme vhodne podatke v obliki niza a b c:

$explode = fn($v) => explode(' ', $v);

$schema = Expect::arrayOf('string')
	->before($explode);

$normalized = $processor->process($schema, 'a b c');
// V redu, vrne ['a', 'b', 'c']

preslikava v predmete: from()

Iz razreda lahko ustvarite strukturno shemo. Primer:

class Config
{
	public string $name;
	public string|null $password;
	public bool $admin = false;
}

$schema = Expect::from(new Config);

$data = [
	'name' => 'jeff',
];

$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false}

Podprti so tudi anonimni razredi:

$schema = Expect::from(new class {
	public string $name;
	public ?string $password;
	public bool $admin = false;
});

Ker informacije, pridobljene iz definicije razreda, morda ne zadostujejo, lahko z drugim parametrom dodate shemo po meri za elemente:

$schema = Expect::from(new Config, [
	'name' => Expect::string()->pattern('\w:.*'),
]);
različica: 2.0