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