Schema: Validarea datelor

O bibliotecă practică pentru validarea și normalizarea structurilor de date în raport cu o schemă dată, cu o API inteligentă și ușor de înțeles.

Instalare:

composer require nette/schema

Utilizare de bază

În variabila $schema avem o schemă de validare (ce înseamnă exact acest lucru și cum să o creăm vom spune mai târziu) și în variabila $data avem o structură de date pe care dorim să o validăm și să o normalizăm. Aceasta poate fi, de exemplu, date trimise de utilizator prin intermediul unui API, fișier de configurare etc.

Sarcina este gestionată de clasa Nette\Schema\Processor, care procesează datele de intrare și fie returnează datele normalizate, fie aruncă o excepție Nette\Schema\ValidationException în caz de eroare.

$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() returnează o matrice cu toate șirurile de mesaje, iar $e->getMessageObjects() returnează toate mesajele ca obiecte Nette\Schema\Message.

Definirea schemei

Și acum să creăm o schemă. Clasa Nette\Schema\Expect este utilizată pentru a o defini, definim de fapt așteptările cu privire la modul în care ar trebui să arate datele. Să spunem că datele de intrare trebuie să fie o structură (de exemplu, o matrice) care conține elementele processRefund de tip bool și refundAmount de tip int.

use Nette\Schema\Expect;

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

Credem că definiția schemei pare clară, chiar dacă o vedeți pentru prima dată.

Să trimitem următoarele date pentru validare:

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

$normalized = $processor->process($schema, $data); // Bine, treisprezece.

Rezultatul, adică valoarea $normalized, este obiectul stdClass. Dacă dorim ca ieșirea să fie o matrice, adăugăm un cast la schema Expect::structure([...])->castTo('array').

Toate elementele structurii sunt opționale și au o valoare implicită null. Exemplu:

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

$normalized = $processor->process($schema, $data); // Bine, Trei.
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

Faptul că valoarea implicită este null nu înseamnă că aceasta ar fi acceptată în datele de intrare 'processRefund' => null. Nu, datele de intrare trebuie să fie booleene, adică numai true sau false. Ar trebui să permitem în mod explicit null prin Expect::bool()->nullable().

Un element poate fi făcut obligatoriu folosind Expect::bool()->required(). Schimbăm valoarea implicită la false folosind Expect::bool()->default(false) sau la scurt timp folosind Expect::bool(false).

Și dacă am dori să acceptăm 1 and 0 în afară de booleeni? Atunci enumerăm valorile permise, pe care le vom normaliza, de asemenea, la 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

Acum cunoașteți elementele de bază ale modului în care este definită schema și cum se comportă elementele individuale ale structurii. Vom arăta acum ce alte elemente pot fi utilizate în definirea unei scheme.

Tipuri de date: type()

Toate tipurile de date PHP standard pot fi enumerate în schemă:

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

Și apoi toate tipurile acceptate de validatoare prin Expect::type('scalar') sau prin abrevierea Expect::scalar(). De asemenea, sunt acceptate și nume de clase sau interfețe, de exemplu Expect::type('AddressEntity').

De asemenea, puteți utiliza notația de uniune:

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

Valoarea implicită este întotdeauna null, cu excepția array și list, unde este o matrice goală. (O listă este o matrice indexată în ordine crescătoare a cheilor numerice de la zero, adică o matrice neasociativă).

Array de valori: arrayOf() listOf()

Matricea este o structură prea generală, fiind mai util să se specifice exact ce elemente poate conține. De exemplu, un array ale cărui elemente pot fi doar șiruri de caractere:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERROR: 123 nu este un șir de caractere

Al doilea parametru poate fi utilizat pentru a specifica cheile (începând cu versiunea 1.2):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' nu este int

Lista este o matrice indexată:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 nu este un șir de caractere
$processor->process($schema, ['key' => 'a']); // ERROR: nu este o listă
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: nu este o listă

Parametrul poate fi, de asemenea, o schemă, astfel încât putem scrie:

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

Valoarea implicită este o matrice goală. Dacă specificați valoarea implicită, aceasta va fi îmbinată cu datele transmise. Acest lucru poate fi dezactivat utilizând mergeDefaults(false) (începând cu versiunea 1.1).

Enumerare: anyOf()

anyOf() este un set de valori sau de scheme care pot reprezenta o valoare. Iată cum se scrie o matrice de elemente care pot fi fie 'a', true, fie null:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERROR: false nu are ce căuta acolo

Elementele enumerării pot fi, de asemenea, scheme:

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

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

Metoda anyOf() acceptă variantele ca parametri individuali, nu ca matrice. Pentru a-i transmite o matrice de valori, utilizați operatorul de despachetare anyOf(...$variants).

Valoarea implicită este null. Utilizați metoda firstIsDefault() pentru a face din primul element valoarea implicită:

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

Structuri

Structurile sunt obiecte cu chei definite. Fiecare dintre aceste perechi cheie ⇒ valoare este denumită “proprietate”:

Structurile acceptă matrici și obiecte și returnează obiecte stdClass.

În mod implicit, toate proprietățile sunt opționale și au o valoare implicită de null. Puteți defini proprietățile obligatorii utilizând required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // valoarea implicită este nulă
]);

$processor->process($schema, ['optional' => '']);
// ERROR: opțiunea 'required' lipsește

$processor->process($schema, ['required' => 'foo']);
// OK, returnează {'required' => 'foo', 'optional' => null}

Dacă nu doriți să afișați proprietăți care au doar o valoare implicită, utilizați skipDefaults():

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

$processor->process($schema, ['required' => 'foo']);
// OK, returnează {'required' => 'foo'}

Deși null este valoarea implicită a proprietății optional, nu este permisă în datele de intrare (valoarea trebuie să fie un șir de caractere). Proprietățile care acceptă null sunt definite utilizând nullable():

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

$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' se așteaptă să fie un șir de caractere, dar este nul.

$processor->process($schema, ['nullable' => null]);
// OK, returnează {'optional' => null, 'nullable' => null}

Matricea tuturor proprietăților structurii este returnată de metoda getShape().

În mod implicit, nu pot exista elemente suplimentare în datele de intrare:

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

$processor->process($schema, ['additional' => 1]);
// ERROR: Element neașteptat "suplimentar

Ceea ce se poate schimba cu otherItems(). Ca parametru, vom specifica schema pentru fiecare element suplimentar:

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

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

Puteți crea o structură nouă derivând din alta folosind extend():

$dog = Expect::structure([
	'name' => Expect::string(),
	'age' => Expect::int(),
]);

$dogWithBreed = $dog->extend([
	'breed' => Expect::string(),
]);

Array

Un array cu chei definite. Se aplică aceleași reguli ca pentru structuri.

$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // default value is null
]);

De asemenea, puteți defini un array indexat, cunoscut sub numele de tuple:

$schema = Expect::array([
	Expect::int(),
	Expect::string(),
	Expect::bool(),
]);

$processor->process($schema, [1, 'hello', true]); // OK

Deprecieri

Puteți deprecia o proprietate folosind opțiunea deprecated([string $message]) metoda . Notificările de depreciere sunt returnate de $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"]

Domenii: min() max()

Utilizați min() și max() pentru a limita numărul de elemente pentru array-uri:

// matrice, cel puțin 10 elemente, maximum 20 elemente
Expect::array()->min(10)->max(20);

Pentru șirurile de caractere, limitați lungimea acestora:

// șir de cel puțin 10 caractere, maxim 20 de caractere
Expect::string()->min(10)->max(20);

Pentru numere, limitați valoarea acestora:

// număr întreg, între 10 și 20 inclusiv
Expect::int()->min(10)->max(20);

Desigur, este posibil să se menționeze doar min(), sau doar max():

// șir de caractere, maximum 20 de caractere
Expect::string()->max(20);

Expresii regulate: pattern()

Cu ajutorul pattern(), puteți specifica o expresie regulată cu care trebuie să se potrivească întregul șir de intrare (adică ca și cum ar fi înfășurat în caractere ^ a $):

// doar 9 cifre
Expect::string()->pattern('\d{9}');

Aserțiuni personalizate: assert()

Puteți adăuga orice alte restricții folosind assert(callable $fn).

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // numărul trebuie să fie par

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 nu este par

Sau

Expect::string()->assert('is_file'); // fișierul trebuie să existe

Puteți adăuga propria descriere pentru fiecare afirmație. Aceasta va face parte din mesajul de eroare.

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

$processor->process($schema, ['a', 'b', 'c']);
// A eșuat aserțiunea "Even items in array" pentru elementul cu valoarea array.

Metoda poate fi apelată în mod repetat pentru a adăuga mai multe constrângeri. Ea poate fi amestecată cu apelurile la transform() și castTo().

Transformare: transform()

Datele validate cu succes pot fi modificate cu ajutorul unei funcții personalizate:

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

Metoda poate fi apelată în mod repetat pentru a adăuga mai multe transformări. Ea poate fi amestecată cu apeluri la assert() și castTo(). Operațiile vor fi executate în ordinea în care sunt declarate:

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() poate transforma și valida valoarea simultan. Acest lucru este adesea mai simplu și mai puțin redundant decât înlănțuirea transform() și assert(). În acest scop, funcția primește un obiect Context cu o metodă addError(), care poate fi utilizată pentru a adăuga informații despre problemele de validare:

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()

Datele validate cu succes pot fi turnate:

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

În plus față de tipurile native PHP, puteți, de asemenea, să faceți cast la clase. Se distinge dacă este vorba de o clasă simplă fără constructor sau de o clasă cu constructor. În cazul în care clasa nu are constructor, se creează o instanță a acesteia și toate elementele structurii sunt scrise în proprietățile sale:

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

În cazul în care clasa are un constructor, elementele structurii sunt transmise constructorului ca parametri numiți:

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

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

Castingul combinat cu un parametru scalar creează un obiect și transmite valoarea acestuia ca unic parametru către constructor:

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

Normalizare: before()

Înainte de validarea propriu-zisă, datele pot fi normalizate cu ajutorul metodei before(). Ca exemplu, să avem un element care trebuie să fie o matrice de șiruri de caractere (de exemplu ['a', 'b', 'c']), dar care primește datele de intrare sub forma unui șir de caractere a b c:

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

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

$normalized = $processor->process($schema, 'a b c');
// OK, returnează ['a', 'b', 'c']

Maparea în obiecte: from()

Puteți genera o schemă de structură din clasă. Exemplu:

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}

Sunt acceptate și clasele anonime:

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

Deoarece este posibil ca informațiile obținute din definiția clasei să nu fie suficiente, puteți adăuga o schemă personalizată pentru elemente cu ajutorul celui de-al doilea parametru:

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