Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

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:.*'),
]);
verzió: 2.0