Schema: adat validáció
Praktikus könyvtár adatszerkezetek validálására és normalizálására egy adott séma alapján, okos, érthető API-val.
Telepítés:
composer require nette/schema
Alapvető használat
A $schema
változóban van a validációs séma (hogy ez pontosan mit jelent, és hogyan hozzunk létre ilyen
sémát, azt hamarosan elmondjuk), a $data
változóban pedig az az adatszerkezet, amelyet validálni és
normalizálni szeretnénk. Ez lehet például egy felhasználó által API interfészen keresztül küldött adat, konfigurációs
fájl stb.
A feladatot a Nette\Schema\Processor osztály végzi el, amely feldolgozza a bemenetet, és vagy visszaadja a normalizált adatokat, vagy hiba esetén Nette\Schema\ValidationException kivételt dob.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Az adatok érvénytelenek: ' . $e->getMessage();
}
Az $e->getMessages()
metódus visszaadja az összes üzenetet stringként tartalmazó tömböt, az
$e->getMessageObjects()
pedig az összes üzenetet Message.html
objektumként.
Séma definiálása
És most hozzuk létre a sémát. A definiálására a Nette\Schema\Expect osztály szolgál, tulajdonképpen
elvárásokat definiálunk, hogy hogyan kell kinézniük az adatoknak. Tegyük fel, hogy a bemeneti adatoknak egy struktúrát
(például tömböt) kell alkotniuk, amely processRefund
(bool típusú) és refundAmount
(int típusú)
elemeket tartalmaz.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Bízunk benne, hogy a séma definíciója érthetőnek tűnik, még akkor is, ha most látja először.
Küldjük validálásra a következő adatokat:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, átmegy a validáción
A kimenet, azaz a $normalized
érték, egy stdClass
objektum. Ha azt szeretnénk, hogy a kimenet
tömb legyen, kiegészítjük a sémát egy Expect::structure([...])->castTo('array')
típuskonverzióval.
A struktúra minden eleme opcionális, és alapértelmezett értéke null
. Példa:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, átmegy a validáción
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Az, hogy az alapértelmezett érték null
, nem jelenti azt, hogy a bemeneti adatokban elfogadná a
'processRefund' => null
-t. Nem, a bemenetnek boolean-nek kell lennie, tehát csak true
vagy
false
. A null
engedélyezéséhez explicit módon kellene használni a
Expect::bool()->nullable()
-t.
Egy elemet kötelezővé tehetünk a Expect::bool()->required()
segítségével. Az alapértelmezett értéket
megváltoztathatjuk például false
-ra a Expect::bool()->default(false)
vagy röviden
Expect::bool(false)
segítségével.
És mi van, ha a boolean mellett még az 1
-et és 0
-t is el akarjuk fogadni? Akkor megadjuk az
értékek felsorolását, amelyeket ráadásul boolean-re normalizálunk:
$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
Most már ismeri az alapokat arról, hogyan definiáljunk sémát, és hogyan viselkednek a struktúra egyes elemei. Most megmutatjuk, milyen további elemeket lehet használni a séma definiálásakor.
Adattípusok: type()
A sémában megadhatók az összes standard PHP adattípus:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Továbbá minden típus, amelyet a Validators osztály
által támogatott, például Expect::type('scalar')
vagy röviden Expect::scalar()
. Továbbá
osztály- vagy interfésznevek, például Expect::type('AddressEntity')
.
Használható union jelölés is:
Expect::type('bool|string|array')
Az alapértelmezett érték mindig null
, kivéve az array
és list
esetében, ahol ez
egy üres tömb. (A lista egy nullától kezdődő, növekvő sorrendű numerikus kulcsokkal indexelt tömb, azaz nem
asszociatív tömb).
Értékek tömbje: arrayOf() listOf()
A tömb túl általános struktúra, hasznosabb megadni, hogy pontosan milyen elemeket tartalmazhat. Például egy tömb, amelynek elemei csak stringek lehetnek:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // HIBA: 123 nem string
A második paraméterrel megadhatók a kulcsok (1.2 verziótól):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // HIBA: 'a' nem int
A lista egy indexelt tömb:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // HIBA: 123 nem string
$processor->process($schema, ['key' => 'a']); // HIBA: nem lista
$processor->process($schema, [1 => 'a', 0 => 'b']); // HIBA: szintén nem lista
A paraméter lehet séma is, tehát írhatjuk:
Expect::arrayOf(Expect::bool())
Az alapértelmezett érték egy üres tömb. Ha alapértelmezett értéket ad meg, az összevonásra kerül az átadott
adatokkal. Ezt a mergeDefaults(false)
segítségével lehet letiltani (1.1 verziótól).
Felsorolás: anyOf()
Az anyOf()
értékek vagy sémák felsorolását jelenti, amelyeket az érték felvehet. Így írjuk le egy olyan
elemekből álló tömböt, amelyek lehetnek 'a'
, true
vagy null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // HIBA: a false nem tartozik ide
A felsorolás elemei lehetnek sémák is:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // HIBA
Az anyOf()
metódus a változatokat különálló paraméterekként fogadja, nem tömbként. Ha értékek
tömbjét szeretné átadni neki, használja az unpacking operátort anyOf(...$variants)
.
Az alapértelmezett érték null
. A firstIsDefault()
metódussal az első elemet tesszük
alapértelmezetté:
// az alapértelmezett 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Struktúrák
A struktúrák definiált kulcsokkal rendelkező objektumok. Minden kulcs ⇒ érték párt „tulajdonságnak” (property) nevezünk:
A struktúrák tömböket és objektumokat fogadnak el, és stdClass
objektumokat adnak vissza.
Alapértelmezés szerint minden tulajdonság opcionális, és alapértelmezett értéke null
. Kötelező
tulajdonságokat definiálhat a required()
segítségével:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // alapértelmezett érték null
]);
$processor->process($schema, ['optional' => '']);
// HIBA: az 'required' opció hiányzik
$processor->process($schema, ['required' => 'foo']);
// OK, visszaadja {'required' => 'foo', 'optional' => null}
Ha nem szeretné, hogy a kimenetben alapértelmezett értékkel rendelkező tulajdonságok legyenek, használja a
skipDefaults()
-t:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, visszaadja {'required' => 'foo'}
Bár a null
az optional
tulajdonság alapértelmezett értéke, a bemeneti adatokban nem
engedélyezett (az értéknek stringnek kell lennie). A null
-t elfogadó tulajdonságokat a nullable()
segítségével definiáljuk:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// HIBA: az 'optional' stringet vár, null-t kapott.
$processor->process($schema, ['nullable' => null]);
// OK, visszaadja {'optional' => null, 'nullable' => null}
A struktúra összes tulajdonságának tömbjét a getShape()
metódus adja vissza.
Alapértelmezés szerint a bemeneti adatokban nem lehetnek extra elemek:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// HIBA: Váratlan elem 'additional'
Ezt megváltoztathatjuk az otherItems()
segítségével. Paraméterként adjunk meg egy sémát, amely szerint az
extra elemek validálva lesznek:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // HIBA
Új struktúrát hozhat létre egy másikból származtatva a extend()
segítségével:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Tömbök
Definiált kulcsokkal rendelkező tömbök. Minden vonatkozik rá, ami a struktúrák-ra.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // alapértelmezett érték null
]);
Definiálható indexelt tömb is, amelyet tuple-ként ismerünk:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Elavult propertyk
Egy propertyt elavultként jelölhet meg a deprecated([string $message])
metódussal. A támogatás
megszűnéséről szóló információkat a $processor->getWarnings()
adja vissza:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('Az elem %path% elavult'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Az elem 'old' elavult"]
Tartományok: min() max()
A min()
és max()
segítségével korlátozhatjuk a tömbök elemeinek számát:
// tömb, legalább 10 elem, legfeljebb 20 elem
Expect::array()->min(10)->max(20);
Stringeknél korlátozhatjuk a hosszukat:
// string, legalább 10 karakter hosszú, legfeljebb 20 karakter
Expect::string()->min(10)->max(20);
Számoknál korlátozhatjuk az értéküket:
// egész szám, 10 és 20 között, beleértve
Expect::int()->min(10)->max(20);
Természetesen megadhatjuk csak a min()
-t vagy csak a max()
-ot:
// string legfeljebb 20 karakter
Expect::string()->max(20);
Reguláris kifejezések: pattern()
A pattern()
segítségével megadhatunk egy reguláris kifejezést, amelynek a teljes bemeneti stringnek
meg kell felelnie (azaz mintha ^
és $
karakterekkel lenne körülvéve):
// pontosan 9 számjegy
Expect::string()->pattern('\d{9}');
Egyéni megszorítások: assert()
Bármilyen további korlátozást megadhatunk az assert(callable $fn)
segítségével.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // a számnak párosnak kell lennie
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // HIBA: 3 nem páros szám
Vagy
Expect::string()->assert('is_file'); // a fájlnak léteznie kell
Minden korlátozáshoz hozzáadhat saját leírást. Ez a hibaüzenet része lesz.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Páros elemek a tömbben');
$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Páros elemek a tömbben" for item with value array.
A metódust ismételten hívhatjuk, hogy több korlátozást adjunk hozzá. Váltakozhat a transform()
és
castTo()
hívásokkal.
Átalakítások: transform()
A sikeresen validált adatokat módosíthatjuk egy saját függvénnyel:
// átalakítás nagybetűsre:
Expect::string()->transform(fn(string $s) => strtoupper($s));
A metódust ismételten hívhatjuk, hogy több átalakítást adjunk hozzá. Váltakozhat az assert()
és
castTo()
hívásokkal. A műveletek abban a sorrendben hajtódnak végre, ahogyan deklarálva vannak:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'Minden karakternek kisbetűsnek kell lennie')
->transform(fn(string $s) => strtoupper($s)); // átalakítás nagybetűsre
A transform()
metódus egyszerre átalakíthatja és validálhatja az értéket. Ez gyakran egyszerűbb és
kevésbé duplikált, mint a transform()
és assert()
láncolása. Ebből a célból a függvény
megkapja a Context objektumot a
addError()
metódussal, amelyet a validációs problémákkal kapcsolatos információk hozzáadására lehet
használni:
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('Minden karakternek kisbetűsnek kell lennie', 'my.case.error');
return null;
}
return strtoupper($s);
});
Típuskonverzió: castTo()
A sikeresen validált adatokat át lehet típusolni:
Expect::scalar()->castTo('string');
A natív PHP típusokon kívül osztályokra is át lehet típusolni. Itt különbséget teszünk, hogy egyszerű osztályról van-e szó konstruktor nélkül, vagy konstruktorral rendelkező osztályról. Ha az osztálynak nincs konstruktora, létrejön az példánya, és a struktúra minden eleme beíródik a propertykbe:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// létrehozza '$obj = new Info'-t és beírja a $obj->processRefund és $obj->refundAmount-ba
Ha az osztálynak van konstruktora, a struktúra elemei névvel ellátott paraméterként kerülnek átadásra a konstruktornak:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// létrehozza $obj = new Info(processRefund: ..., refundAmount: ...)
A típuskonverzió skaláris paraméterrel kombinálva létrehoz egy objektumot, és az értéket egyetlen paraméterként adja át a konstruktornak:
Expect::string()->castTo(DateTime::class);
// létrehozza new DateTime(...)
Normalizálás: before()
Maga a validáció előtt az adatokat normalizálhatjuk a before()
metódussal. Példaként említsünk egy
elemet, amelynek stringek tömbjének kell lennie (például ['a', 'b', 'c']
), de bemenetként a b c
string formátumot fogad el:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK és visszaadja ['a', 'b', 'c']
Leképezés objektumokra: from()
A struktúra sémáját legeneráltathatjuk egy osztályból. Példa:
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}
Támogatottak a névtelen osztályok is:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Mivel az osztálydefinícióból nyert információk nem feltétlenül elegendőek, a második paraméterrel kiegészítheti az elemeket saját sémával:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);