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

Schema: validarea datelor

Bibliotecă practică pentru validarea și normalizarea structurilor de date față de 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 schema de validare (ce înseamnă exact și cum să creăm o astfel de schemă vom discuta imediat) și în variabila $data structura de date pe care dorim să o validăm și să o normalizăm. Poate fi, de exemplu, date trimise de utilizator prin interfața API, un fișier de configurare etc.

Sarcina este gestionată de clasa Nette\Schema\Processor, care procesează intrarea și fie returnează date normalizate, fie, în caz de eroare, aruncă excepția Nette\Schema\ValidationException.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Datele nu sunt valide: ' . $e->getMessage();
}

Metoda $e->getMessages() returnează un array al tuturor mesajelor ca șiruri și $e->getMessageObjects() returnează toate mesajele ca obiecte Nette\Schema\Message.

Definirea schemei

Și acum creăm schema. Pentru definirea ei se utilizează clasa Nette\Schema\Expect, definim de fapt așteptările despre cum ar trebui să arate datele. Să spunem că datele de intrare trebuie să formeze o structură (de exemplu, un array) 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 ușor de înțeles, chiar dacă o vedeți pentru prima dată.

Trimitem spre validare următoarele date:

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

$normalized = $processor->process($schema, $data); // OK, trece validarea

Ieșirea, adică valoarea $normalized, este un obiect stdClass. Dacă am dori ca ieșirea să fie un array, completăm schema cu conversia Expect::structure([...])->castTo('array').

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

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

$normalized = $processor->process($schema, $data); // OK, trece validarea
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

Faptul că valoarea implicită este null nu înseamnă că s-ar accepta în datele de intrare 'processRefund' => null. Nu, intrarea trebuie să fie un boolean, adică doar true sau false. Pentru a permite null, ar trebui să o facem explicit folosind Expect::bool()->nullable().

Elementul poate fi făcut obligatoriu folosind Expect::bool()->required(). Valoarea implicită o schimbăm, de exemplu, la false folosind Expect::bool()->default(false) sau prescurtat Expect::bool(false).

Și ce dacă am dori să acceptăm, pe lângă boolean, și 1 și 0? Atunci specificăm o enumerare a valorilor, pe care le lăsăm, în plus, să fie normalizate 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 deja elementele de bază ale modului în care se definește o schemă și cum se comportă elementele individuale ale structurii. Acum vom arăta ce alte elemente pot fi utilizate la definirea schemei.

Tipuri de date: type()

În schemă se pot specifica toate tipurile de date standard PHP:

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

Și, în plus, toate tipurile suportate de clasa Validators, de exemplu Expect::type('scalar') sau prescurtat Expect::scalar(). De asemenea, numele claselor sau interfețelor, de exemplu Expect::type('AddressEntity').

Se poate utiliza și notația union:

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

Valoarea implicită este întotdeauna null, cu excepția array și list, unde este un array gol. (List este un array indexat conform unei serii ascendente de chei numerice începând de la zero, adică un array neasociativ).

Array-uri de valori: arrayOf() listOf()

Array-ul reprezintă o structură prea generală, este mai util să specificăm exact ce elemente poate conține. De exemplu, un array ale cărui elemente pot fi doar șiruri:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // EROARE: 123 nu este string

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

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

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

List este un array indexat:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // EROARE: 123 nu este string
$processor->process($schema, ['key' => 'a']); // EROARE: nu este list
$processor->process($schema, [1 => 'a', 0 => 'b']); // EROARE: de asemenea, nu este list

Parametrul poate fi și o schemă, deci putem scrie:

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

Valoarea implicită este un array gol. Dacă specificați o valoare implicită, aceasta va fi combinată cu datele transmise. Acest lucru poate fi dezactivat folosind mergeDefaults(false) (începând cu versiunea 1.1).

Enumerare: anyOf()

anyOf() reprezintă o enumerare a valorilor sau schemelor pe care le poate lua o valoare. Astfel scriem un array de elemente care pot fi fie 'a', true sau null:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // EROARE: false nu aparține aici

Elementele enumerării pot fi și scheme:

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

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

Metoda anyOf() acceptă variantele ca parametri individuali, nu un array. Dacă doriți să îi transmiteți un array de valori, utilizați operatorul unpacking anyOf(...$variants).

Valoarea implicită este null. Prin metoda firstIsDefault() facem primul element implicit:

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

Structuri

Structurile sunt obiecte cu chei definite. Fiecare pereche cheie ⇒ valoare este denumită „proprietate”:

Structurile acceptă array-uri și obiecte și returnează obiecte stdClass.

În mod implicit, toate proprietățile sunt opționale și au valoarea implicită null. Puteți defini proprietăți obligatorii folosind required():

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

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

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

Dacă nu doriți să aveți în ieșire proprietăți cu 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). Proprietățile care acceptă null le definim folosind nullable():

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

$processor->process($schema, ['optional' => null]);
// EROARE: 'optional' se așteaptă să fie string, null dat.

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

Array-ul 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]);
// EROARE: Element neașteptat 'additional'

Ceea ce putem schimba folosind otherItems(). Ca parametru specificăm schema conform căreia se vor valida elementele suplimentare:

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

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

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

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

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

Array-uri

Array cu chei definite. Pentru el se aplică tot ce structuri.

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

Se poate defini și un array indexat, cunoscut sub numele de tuple:

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

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

Proprietăți învechite

Puteți marca o proprietate ca învechită folosind metoda deprecated([string $message]). Informațiile despre încetarea suportului sunt returnate folosind $processor->getWarnings():

$schema = Expect::structure([
	'old' => Expect::int()->deprecated('Elementul %path% este învechit'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Elementul 'old' este învechit"]

Intervale: min() max()

Folosind min() și max() se poate limita numărul de elemente la array-uri:

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

La șiruri se poate limita lungimea lor:

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

La numere se poate limita valoarea lor:

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

Desigur, este posibil să specificați doar min(), sau doar max():

// șir maxim 20 caractere
Expect::string()->max(20);

Expresii regulate: pattern()

Folosind pattern(), puteți specifica o expresie regulată căreia trebuie să îi corespundă întregul șir de intrare (adică, ca și cum ar fi încadrat de caracterele ^ și $):

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

Restricții personalizate: assert()

Orice alte restricții le specificăm 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']); // EROARE: 3 nu este un număr par

Sau

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

La fiecare restricție puteți adăuga o descriere personalizată. Aceasta va face parte din mesajul de eroare.

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Elemente pare în array');

$processor->process($schema, ['a', 'b', 'c']);
// Aserțiune eșuată "Elemente pare în array" pentru elementul cu valoarea array.

Metoda poate fi apelată în mod repetat pentru a adăuga mai multe restricții. Poate fi intercalată cu apeluri transform() și castTo().

Transformări: transform()

Datele validate cu succes pot fi modificate folosind o funcție personalizată:

// conversie la majuscule:
Expect::string()->transform(fn(string $s) => strtoupper($s));

Metoda poate fi apelată în mod repetat pentru a adăuga mai multe transformări. Poate fi intercalată cu apeluri assert() și castTo(). Operațiile se efectuează în ordinea în care sunt declarate:

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'Toate caracterele trebuie să fie minuscule')
	->transform(fn(string $s) => strtoupper($s)); // conversie la majuscule

Metoda transform() poate transforma și valida simultan valoarea. Acest lucru este adesea mai simplu și mai puțin duplicat decât înlănțuirea transform() și assert(). În acest scop, funcția primește un obiect Context cu metoda 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('Toate caracterele trebuie să fie minuscule', 'my.case.error');
			return null;
		}

		return strtoupper($s);
	});

Conversie: castTo()

Datele validate cu succes pot fi convertite:

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

Pe lângă tipurile native PHP, se poate converti și la clase. Se face distincție dacă este o clasă simplă fără constructor sau o clasă cu constructor. Dacă clasa nu are constructor, se creează instanța sa și toate elementele structurii se scriu în proprietăți:

class Info
{
	public bool $processRefund;
	public int $refundAmount;
}

Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
])->castTo(Info::class);

// creează '$obj = new Info' și scrie în $obj->processRefund și $obj->refundAmount

Dacă clasa are constructor, elementele structurii se transmit ca parametri numiți constructorului:

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

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

Conversia în combinație cu un parametru scalar creează un obiect și transmite valoarea ca singurul parametru al constructorului:

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

Normalizare: before()

Înainte de validarea propriu-zisă, datele pot fi normalizate folosind metoda before(). Ca exemplu, să luăm un element care trebuie să fie un array de șiruri (de exemplu, ['a', 'b', 'c']), dar acceptă intrarea sub forma șirului a b c:

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

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

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

Mapare la obiecte: from()

Schema structurii o putem lăsa să fie generată dintr-o clasă. Exemplu:

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}

Sunt suportate și clasele anonime:

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

Deoarece informațiile obținute din definiția clasei pot să nu fie suficiente, puteți completa elementele cu o schemă proprie folosind al doilea parametru:

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