Schema: validacija podatkov
Praktična knjižnica za validacijo in normalizacijo podatkovnih struktur glede na dano shemo s pametnim razumljivim API-jem.
Namestitev:
composer require nette/schema
Osnovna uporaba
V spremenljivki $schema
imamo validacijsko shemo (kaj točno to pomeni in kako tako shemo ustvariti, si bomo
povedali kmalu) in v spremenljivki $data
podatkovno strukturo, ki jo želimo validirati in normalizirati. Lahko gre
na primer za podatke, ki jih je uporabnik poslal prek vmesnika API, konfiguracijsko datoteko itd.
Nalogo opravi razred Nette\Schema\Processor, ki obdela vhod in bodisi vrne normalizirane podatke, bodisi v primeru napake vrže izjemo Nette\Schema\ValidationException.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Podatki niso veljavni: ' . $e->getMessage();
}
Metoda $e->getMessages()
vrne polje vseh sporočil kot nize in $e->getMessageObjects()
vrne vsa
sporočila kot objekte Nette\Schema\Message.
Definiranje sheme
In zdaj ustvarimo shemo. Za njeno definiranje služi razred Nette\Schema\Expect, definiramo pravzaprav pričakovanja,
kako naj podatki izgledajo. Recimo, da morajo vhodni podatki tvoriti strukturo (na primer polje), ki vsebuje elementa
processRefund
tipa bool in refundAmount
tipa int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Verjamemo, da definicija sheme izgleda razumljivo, tudi če jo vidite prvič.
Pošljimo k validaciji naslednje podatke:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, gre skozi validacijo
Izhod, torej vrednost $normalized
, je objekt stdClass
. Če bi želeli, da bi bil izhod polje,
dopolnimo shemo s pretvorbo Expect::structure([...])->castTo('array')
.
Vsi elementi strukture so neobvezni in imajo privzeto vrednost null
. Primer:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, gre skozi validacijo
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
To, da je privzeta vrednost null
, ne pomeni, da bi se sprejelo v vhodnih podatkih
'processRefund' => null
. Ne, vhod mora biti boolean, torej samo true
ali false
. Dovoliti
null
bi morali eksplicitno s pomočjo Expect::bool()->nullable()
.
Element lahko naredimo obvezen s pomočjo Expect::bool()->required()
. Privzeto vrednost spremenimo na primer
na false
s pomočjo Expect::bool()->default(false)
ali skrajšano
Expect::bool(false)
.
In kaj, če bi poleg booleana želeli sprejeti še 1
in 0
? Potem navedemo naštevne vrednosti, ki jih
poleg tega pustimo normalizirati 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
Zdaj že poznate osnove tega, kako se definira shema in kako se obnašajo posamezni elementi strukture. Zdaj si bomo pokazali, katere vse druge elemente lahko uporabimo pri definiciji sheme.
Podatkovni tipi: type()
V shemi lahko navedete vse standardne podatkovne tipe PHP:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
In nadalje vse tipe, podprte s strani razreda
Validators, na primer Expect::type('scalar')
ali skrajšano Expect::scalar()
. Tudi imena razredov
ali vmesnikov, na primer Expect::type('AddressEntity')
.
Lahko uporabite tudi union zapis:
Expect::type('bool|string|array')
Privzeta vrednost je vedno null
z izjemo za array
in list
, kjer je to prazno polje.
(List je polje, indeksirano po naraščajočem zaporedju numeričnih ključev od nič, torej neasociativno polje).
Polja vrednosti: arrayOf() listOf()
Polje predstavlja preveč splošno strukturo, bolj uporabno je specificirati, katere točno elemente lahko vsebuje. Na primer polje, katerega elementi so lahko samo nizi:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // NAPAKA: 123 ni niz
Z drugim parametrom lahko specificirate ključe (od različice 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // NAPAKA: 'a' ni int
List je indeksirano polje:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // NAPAKA: 123 ni niz
$processor->process($schema, ['key' => 'a']); // NAPAKA: ni list
$processor->process($schema, [1 => 'a', 0 => 'b']); // NAPAKA: tudi ni list
Parameter je lahko tudi shema, torej lahko zapišemo:
Expect::arrayOf(Expect::bool())
Privzeta vrednost je prazno polje. Če podate privzeto vrednost, bo združena s predanimi podatki. To lahko deaktivirate z
mergeDefaults(false)
(od različice 1.1).
Naštevanje: anyOf()
anyOf()
predstavlja naštevanje vrednosti ali shem, ki jih lahko vrednost prevzame. Tako zapišemo polje
elementov, ki so lahko bodisi 'a'
, true
ali null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // NAPAKA: false ne spada sem
Elementi naštevanja so lahko tudi sheme:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // NAPAKA
Metoda anyOf()
sprejema variante kot posamezne parametre, ne kot polje. Če ji želite predati polje vrednosti,
uporabite unpacking operator anyOf(...$variants)
.
Privzeta vrednost je null
. Z metodo firstIsDefault()
naredimo prvi element privzetega:
// privzeto je 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Strukture
Strukture so objekti z definiranimi ključi. Vsak od parov ključ ⇒ vrednost je označen kot „lastnost“:
Strukture sprejemajo polja in objekte ter vračajo objekte stdClass
.
Privzeto so vse lastnosti neobvezne in imajo privzeto vrednost null
. Obvezne lastnosti lahko definirate z
required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // privzeta vrednost je null
]);
$processor->process($schema, ['optional' => '']);
// NAPAKA: možnost 'required' manjka
$processor->process($schema, ['required' => 'foo']);
// OK, vrne {'required' => 'foo', 'optional' => null}
Če na izhodu ne želite imeti lastnosti s privzeto vrednostjo, uporabite skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, vrne {'required' => 'foo'}
Čeprav je null
privzeta vrednost lastnosti optional
, v vhodnih podatkih ni dovoljen (vrednost mora
biti niz). Lastnosti, ki sprejemajo null
, definiramo z nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// NAPAKA: 'optional' pričakuje niz, podan null.
$processor->process($schema, ['nullable' => null]);
// OK, vrne {'optional' => null, 'nullable' => null}
Polje vseh lastnosti strukture vrne metoda getShape()
.
Privzeto v vhodnih podatkih ne sme biti nobenih dodatnih elementov:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// NAPAKA: Nepričakovan element 'additional'
Kar lahko spremenimo z otherItems()
. Kot parameter navedemo shemo, po kateri se bodo dodatni elementi
validirali:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // NAPAKA
Novo strukturo lahko ustvarite z izpeljavo iz druge z uporabo extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Polja
Polja z definiranimi ključi. Zanje velja vse kot za strukture.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // privzeta vrednost je null
]);
Lahko definirate tudi indeksirano polje, znano kot tuple:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Zastarele lastnosti
Lastnost lahko označite kot zastarelo z metodo deprecated([string $message])
. Informacije o prenehanju podpore
so vrnjene z $processor->getWarnings()
:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('Element %path% je zastarel'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Element 'old' je zastarel"]
Obsegi: min() max()
Z min()
in max()
lahko pri poljih omejite število elementov:
// polje, najmanj 10 elementov, največ 20 elementov
Expect::array()->min(10)->max(20);
Pri nizih omejite njihovo dolžino:
// niz, najmanj 10 znakov dolg, največ 20 znakov
Expect::string()->min(10)->max(20);
Pri številih omejite njihovo vrednost:
// celo število, med 10 in 20 vključno
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 pattern()
lahko navedete regularni izraz, ki mu mora ustrezati celoten vhodni niz (tj. kot da bi bil ovit
z znaki ^
in $
):
// natanko 9 številk
Expect::string()->pattern('\d{9}');
Lastne omejitve: assert()
Poljubne dodatne omejitve podamo z 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']); // OK
$processor->process($schema, ['a', 'b', 'c']); // NAPAKA: 3 ni sodo število
Ali
Expect::string()->assert('is_file'); // datoteka mora obstajati
Vsaki omejitvi lahko dodate lasten opis. Ta bo del sporočila o napaki.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Sodi elementi v polju');
$processor->process($schema, ['a', 'b', 'c']);
// Neuspela asercija "Sodi elementi v polju" za element z vrednostjo array.
Metodo lahko kličete večkrat in tako dodate več omejitev. Lahko jo prepletate s klici transform()
in
castTo()
.
Transformacije: transform()
Uspešno validirane podatke lahko urejate z lastno funkcijo:
// pretvorba v velike črke:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Metodo lahko kličete večkrat in tako dodate več transformacij. Lahko jo prepletate s klici assert()
in
castTo()
. Operacije se izvedejo v vrstnem redu, v katerem so deklarirane:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'Vsi znaki morajo biti male črke')
->transform(fn(string $s) => strtoupper($s)); // pretvorba v velike črke
Metoda transform()
lahko hkrati transformira in validira vrednost. To je pogosto enostavnejše in manj podvojeno
kot veriženje transform()
in assert()
. V ta namen funkcija prejme objekt Context z metodo addError()
, ki jo lahko
uporabite za dodajanje informacij o težavah z validacijo:
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('Vsi znaki morajo biti male črke', 'my.case.error');
return null;
}
return strtoupper($s);
});
Pretvorba: castTo()
Uspešno validirane podatke lahko pretvorite:
Expect::scalar()->castTo('string');
Poleg nativnih PHP tipov lahko pretvarjate tudi v razrede. Pri tem se razlikuje, ali gre za preprost razred brez konstruktorja ali razred s konstruktorjem. Če razred nima konstruktorja, se ustvari njegova instanca in vsi elementi strukture se zapišejo v lastnosti:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// ustvari '$obj = new Info' in zapiše v $obj->processRefund in $obj->refundAmount
Če razred ima konstruktor, se elementi strukture predajo kot imenovani parametri konstruktorju:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// ustvari $obj = new Info(processRefund: ..., refundAmount: ...)
Pretvorba v kombinaciji s skalarnim parametrom ustvari objekt in vrednost preda kot edini parameter konstruktorju:
Expect::string()->castTo(DateTime::class);
// ustvari new DateTime(...)
Normalizacija: before()
Pred samo validacijo lahko podatke normalizirate z metodo before()
. Kot primer navedimo element, ki mora biti
polje nizov (na primer ['a', 'b', 'c']
), vendar sprejema vhod v obliki niza a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK in vrne ['a', 'b', 'c']
Preslikava na objekte: from()
Shemo strukture si lahko pustimo generirati iz razreda. Primer:
class Config
{
public string $name;
public string|null $password;
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}
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 niso zadostne, lahko z drugim parametrom dopolnite elementom lastno shemo:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);