Schema: Adatérvényesítés
Egy praktikus könyvtár az adatstruktúrák érvényesítésére és normalizálására egy adott séma alapján, intelligens és könnyen érthető API-val.
Telepítés:
composer require nette/schema
Alapvető használat
A $schema
változóban van egy érvényesítési séma (hogy ez pontosan mit jelent és hogyan kell létrehozni,
arról később szólunk), a $data
változóban pedig egy adatszerkezet, amelyet érvényesíteni és normalizálni
szeretnénk. Ez lehet például a felhasználó által egy API-n keresztül küldött adat, konfigurációs fájl stb.
A feladatot a Nette\Schema\Processor osztály látja el, amely feldolgozza a bemenetet, és vagy normalizált adatokat ad vissza, vagy hiba esetén egy Nette\Schema\ValidationException kivételt dob.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data is invalid: ' . $e->getMessage();
}
A $e->getMessages()
módszer az összes üzenetsorozat tömbjét adja vissza, a
$e->getMessageObjects()
pedig az összes üzenetet Nette\Schema\Message objektumként adja vissza.
Séma definiálása
És most hozzunk létre egy sémát. Ennek definiálására a Nette\Schema\Expect osztályt használjuk, tulajdonképpen
elvárásokat határozunk meg arra vonatkozóan, hogy az adatoknak hogyan kell kinézniük. Tegyük fel, hogy a bemeneti adatnak
egy struktúrának (pl. egy tömbnek) kell lennie, amely processRefund
típusú bool és refundAmount
típusú int típusú elemeket tartalmaz.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Úgy gondoljuk, hogy a sémadefiníció egyértelműnek tűnik, még akkor is, ha először látjuk.
Küldjük el a következő adatokat érvényesítésre:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, átment.
A kimenet, azaz a $normalized
érték a stdClass
objektum. Ha azt akarjuk, hogy a kimenet egy tömb
legyen, akkor a sémához egy castot adunk hozzá Expect::structure([...])->castTo('array')
.
A struktúra minden eleme opcionális, és alapértelmezett értéke null
. Példa:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, átment.
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Az a tény, hogy az alapértelmezett érték null
, nem jelenti azt, hogy a bemeneti adatokban elfogadható lenne
'processRefund' => null
. Nem, a bemeneti adatnak boolean-nak kell lennie, azaz csak true
vagy
false
. A null
-t kifejezetten engedélyeznünk kellene a Expect::bool()->nullable()
-on
keresztül.
Egy elemet a Expect::bool()->required()
segítségével lehet kötelezővé tenni. Az alapértelmezett
értéket a false
segítségével Expect::bool()->default(false)
vagy röviden a
Expect::bool(false)
segítségével -ra változtatjuk.
És mi lenne, ha a booleans mellett a 1
and 0
-t is el akarnánk fogadni? Akkor felsoroljuk a
megengedett értékeket, amelyeket szintén booleanra 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 ismered az alapokat, hogyan definiálódik a séma, és hogyan viselkednek a struktúra egyes elemei. Most megmutatjuk, hogy a séma definiálásakor milyen egyéb elemeket lehet használni.
Adattípusok: type()
Az összes szabványos PHP adattípus felsorolható a sémában:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
És ezután az összes típus által támogatott
validátorok keresztül Expect::type('scalar')
vagy rövidített Expect::scalar()
. Az osztály- vagy
interfésznevek is elfogadottak, pl. Expect::type('AddressEntity')
.
Használhatjuk az union jelölést is:
Expect::type('bool|string|array')
Az alapértelmezett érték mindig null
, kivéve a array
és list
, ahol ez egy üres
tömb. (A lista a nullától kezdve a numerikus kulcsok növekvő sorrendjében indexelt tömb, azaz nem
asszociatív tömb).
Értékek tömbje: arrayOf() listOf()
A tömb túl általános struktúra, célszerűbb pontosan megadni, hogy milyen elemeket tartalmazhat. Például egy olyan tömb, amelynek elemei csak karakterláncok 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 karakterlánc.
A második paraméterrel megadhatjuk a kulcsokat (az 1.2-es verzió óta):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: '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 karakterlánc.
$processor->process($schema, ['key' => 'a']); // HIBA: nem lista
$processor->process($schema, [1 => 'a', 0 => 'b']); // HIBA: nem lista.
A paraméter lehet egy séma is, így leírhatjuk:
Expect::arrayOf(Expect::bool())
Az alapértelmezett érték egy üres tömb. Ha megadjuk az alapértelmezett értéket, akkor az összeolvad az átadott
adatokkal. Ez kikapcsolható a mergeDefaults(false)
segítségével (az 1.1-es verzió óta).
Felsorolás: anyOf()
anyOf()
azoknak az értékeknek vagy sémáknak a halmaza, amelyek egy érték lehet. Íme, hogyan írjunk egy
olyan elemekből álló tömböt, amely lehet '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]); // ERROR: false nem tartozik oda
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
A anyOf()
módszer a variánsokat egyéni paraméterként fogadja el, nem tömbként. Ha értékekből álló
tömböt akarunk átadni neki, használjuk a anyOf(...$variants)
kicsomagoló operátort.
Az alapértelmezett érték a null
. A firstIsDefault()
módszerrel az első elemet teheti
alapértelmezetté:
// alapértelmezett a 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Struktúrák
A struktúrák meghatározott kulcsokkal rendelkező objektumok. A kulcs ⇒ érték párok mindegyikét „tulajdonságnak“ nevezzük:
A struktúrák tömböket és objektumokat fogadnak, és objektumokat adnak vissza stdClass
.
Alapértelmezés szerint minden tulajdonság opcionális, és alapértelmezett értéke null
. Kötelező
tulajdonságokat a required()
segítségével lehet definiálni:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // az alapértelmezett érték null
]);
$processor->process($schema, ['optional' => '']);
// ERROR: az 'required' opció hiányzik.
$processor->process($schema, ['required' => 'foo']);
// OK, visszatér {'required' => 'foo', 'optional' => null}
Ha nem szeretne csak alapértelmezett értékkel rendelkező tulajdonságokat kiadni, használja a
skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, visszatér {'required' => 'foo'}
Bár a null
a optional
tulajdonság alapértelmezett értéke, a bemeneti adatokban nem megengedett
(az értéknek egy karakterláncnak kell lennie). A null
-t elfogadó tulajdonságok a nullable()
használatával kerülnek definiálásra:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' elvárja, hogy string legyen, null megadva.
$processor->process($schema, ['nullable' => null]);
// OK, visszatér {'optional' => null, 'nullable' => null}
A getShape()
módszer a struktúra összes tulajdonságát tartalmazó tömböt adja vissza.
Alapértelmezés szerint a bemeneti adatokban nem lehetnek extra elemek:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ERROR: Váratlan 'additional' elem
Amit a otherItems()
segítségével megváltoztathatunk. Paraméterként megadjuk az egyes extra elemek
sémáját:
$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ásik struktúrából származtatva a extend()
segítségével:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Array
Meghatározott kulcsokkal rendelkező tömb. Ugyanazok a szabályok vonatkoznak rá, mint a struktúrákra.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Definiálhatunk indexelt tömböt is, amelyet tuple-nek nevezünk:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Deprecations
A tulajdonságokat a deprecated([string $message])
módszerrel. A visszavonásról szóló értesítéseket a
$processor->getWarnings()
adja vissza:
$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"]
Tartományok: min() max()
A min()
és a max()
használatával korlátozhatja a tömbök elemeinek számát:
// tömb, legalább 10 elem, legfeljebb 20 elem
Expect::array()->min(10)->max(20);
A karakterláncok esetében korlátozza a hosszukat:
// legalább 10, legfeljebb 20 karakter hosszúságú karakterlánc
Expect::string()->min(10)->max(20);
Számok esetében korlátozza az értéküket:
// egész szám, 10 és 20 között
Expect::int()->min(10)->max(20);
Természetesen lehetséges, hogy csak a min()
, vagy csak a max()
:
// string, maximum 20 karakter
Expect::string()->max(20);
Szabályos kifejezések: pattern()
A pattern()
segítségével megadhatunk egy olyan reguláris kifejezést, amelynek a összes bemeneti
karakterláncnak meg kell felelnie (azaz mintha a ^
a $
karakterekbe lenne csomagolva):
// csak 9 számjegy
Expect::string()->pattern('\d{9}');
assert()
Bármilyen más korlátozást is hozzáadhat a assert(callable $fn)
segítségével.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // a számlálásnak párosnak kell lennie
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 nem páros
Vagy a
Expect::string()->assert('is_file'); // a fájlnak léteznie kell
Minden egyes állításhoz saját leírást adhat. 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']);
// Sikertelen "Páros elemek a tömbben" állítás a tömb értékű elemre.
A módszer többször is meghívható, hogy több korlátozást adjon hozzá. A transform()
és a
castTo()
hívásokkal keverhető.
Transzformáció: transform()
A sikeresen érvényesített adatok egy egyéni függvény segítségével módosíthatók:
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
A módszer ismételten meghívható több átalakítás hozzáadásához. Ez keverhető a assert()
és a
castTo()
hívásaival. A műveletek a deklaráció sorrendjében kerülnek végrehajtásra:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase
A transform()
módszer egyszerre tudja átalakítani és érvényesíteni az értéket. Ez gyakran egyszerűbb és
kevésbé redundáns, mint a transform()
és a assert()
láncolása. Ehhez a funkció egy Context objektumot kap egy addError()
metódussal, amely az érvényesítési problémákra vonatkozó információk hozzáadására használható:
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);
});
Casting: castTo()
Sikeresen érvényesített adatok önthetők:
Expect::scalar()->castTo('string');
A natív PHP-típusok mellett osztályokba is átvihetők. Megkülönbözteti, hogy egyszerű, konstruktor nélküli osztályról vagy konstruktorral rendelkező osztályról van-e szó. Ha az osztálynak nincs konstruktora, akkor létrejön egy példánya, és a struktúra minden elemét kiírja a tulajdonságaiba:
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
Ha az osztálynak van konstruktora, akkor a struktúra elemeit a konstruktornak nevesített paraméterként adjuk át:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
A skalár paraméterrel kombinált öntött objektum létrehoz egy objektumot, és az értéket adja át egyetlen paraméterként a konstruktornak:
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
Normalizálás: before()
Magát az érvényesítést megelőzően az adatok normalizálhatók a before()
módszerrel. Példaként legyen
egy olyan elemünk, amelynek egy stringekből álló tömbnek kell lennie (pl. ['a', 'b', 'c']
), de a bemenetet egy
karakterlánc formájában kapja meg a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK, visszaadja ['a', 'b', 'c']
Objektumokra való leképezés: from()
Az osztályból struktúra sémát generálhatunk. Példa:
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}
Névtelen osztályok is támogatottak:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
A második paraméterrel egyéni sémát adhat az elemekhez, mivel az osztálydefinícióból kapott információk nem feltétlenül elegendőek:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);