Schema: walidacja danych
Praktyczna biblioteka do walidacji i normalizacji struktur danych względem danego schematu z inteligentnym, zrozumiałym API.
Instalacja:
composer require nette/schema
Podstawowe użycie
W zmiennej $schema
mamy schemat walidacji (co to dokładnie oznacza i jak taki schemat utworzyć, powiemy za
chwilę), a w zmiennej $data
strukturę danych, którą chcemy walidować i normalizować. Mogą to być na
przykład dane przesłane przez użytkownika przez interfejs API, plik konfiguracyjny itp.
Zadanie wykonuje klasa Nette\Schema\Processor, która przetwarza wejście i albo zwraca znormalizowane dane, albo w przypadku błędu rzuca wyjątek Nette\Schema\ValidationException.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Dane są nieprawidłowe: ' . $e->getMessage();
}
Metoda $e->getMessages()
zwraca tablicę wszystkich wiadomości jako ciągi znaków, a
$e->getMessageObjects()
zwraca wszystkie wiadomości jako obiekty Nette\Schema\Message.
Definiowanie schematu
A teraz utworzymy schemat. Do jego definiowania służy klasa Nette\Schema\Expect, definiujemy właściwie oczekiwania,
jak mają wyglądać dane. Powiedzmy, że dane wejściowe muszą tworzyć strukturę (na przykład tablicę) zawierającą
elementy processRefund
typu bool i refundAmount
typu int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Wierzymy, że definicja schematu wygląda zrozumiale, nawet jeśli widzisz ją po raz pierwszy.
Prześlemy do walidacji następujące dane:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, przejdzie walidację
Wyjściem, czyli wartością $normalized
, jest obiekt stdClass
. Jeśli chcielibyśmy, aby wyjściem
była tablica, uzupełnimy schemat o rzutowanie Expect::structure([...])->castTo('array')
.
Wszystkie elementy struktury są opcjonalne i mają domyślną wartość null
. Przykład:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, przejdzie walidację
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
To, że domyślną wartością jest null
, nie oznacza, że akceptowałoby się w danych wejściowych
'processRefund' => null
. Nie, wejściem musi być boolean, czyli tylko true
lub false
.
Pozwolenie na null
musielibyśmy jawnie określić za pomocą Expect::bool()->nullable()
.
Element można uczynić obowiązkowym za pomocą Expect::bool()->required()
. Wartość domyślną zmienimy na
przykład na false
za pomocą Expect::bool()->default(false)
lub skrótowo
Expect::bool(false)
.
A co gdybyśmy chcieli oprócz boolean akceptować jeszcze 1
i 0
? Wtedy podamy wyliczenie wartości,
które dodatkowo pozwolimy znormalizować na 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
Teraz już znasz podstawy tego, jak definiuje się schemat i jak zachowują się poszczególne elementy struktury. Teraz pokażemy, jakie wszystkie inne elementy można użyć przy definicji schematu.
Typy danych: type()
W schemacie można podać wszystkie standardowe typy danych PHP:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
A dalej wszystkie typy, wspierane przez klasę
Validators, na przykład Expect::type('scalar')
lub skrótowo Expect::scalar()
. Także nazwy klas
czy interfejsów, na przykład Expect::type('AddressEntity')
.
Można użyć również zapisu union:
Expect::type('bool|string|array')
Wartość domyślna to zawsze null
z wyjątkiem dla array
i list
, gdzie jest to pusta
tablica. (List to tablica indeksowana według rosnącej serii kluczy numerycznych od zera, czyli tablica nieasocjacyjna).
Tablice wartości: arrayOf() listOf()
Tablica reprezentuje zbyt ogólną strukturę, bardziej użyteczne jest określenie, jakie dokładnie może zawierać elementy. Na przykład tablica, której elementy mogą być tylko ciągami znaków:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // BŁĄD: 123 nie jest stringiem
Drugim parametrem można określić klucze (od wersji 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // BŁĄD: 'a' nie jest int
List to tablica indeksowana:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // BŁĄD: 123 nie jest stringiem
$processor->process($schema, ['key' => 'a']); // BŁĄD: nie jest listą
$processor->process($schema, [1 => 'a', 0 => 'b']); // BŁĄD: również nie jest listą
Parametrem może być również schemat, możemy więc zapisać:
Expect::arrayOf(Expect::bool())
Wartość domyślna to pusta tablica. Jeśli podasz wartość domyślną, zostanie ona połączona z przekazanymi danymi.
Można to zdezaktywować za pomocą mergeDefaults(false)
(od wersji 1.1).
Wyliczenie: anyOf()
anyOf()
reprezentuje wyliczenie wartości lub schematów, które może przyjmować wartość. W ten sposób
zapiszemy tablicę elementów, które mogą być albo 'a'
, true
lub null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // BŁĄD: false tu nie pasuje
Elementy wyliczenia mogą być również schematami:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // BŁĄD
Metoda anyOf()
przyjmuje warianty jako pojedyncze parametry, a nie tablicę. Jeśli chcesz przekazać jej tablicę
wartości, użyj operatora unpacking anyOf(...$variants)
.
Wartość domyślna to null
. Metodą firstIsDefault()
uczynimy pierwszy element domyślnym:
// domyślnie 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Struktury
Struktury to obiekty z zdefiniowanymi kluczami. Każda z par klucz ⇒ wartość jest określana jako „właściwość”:
Struktury przyjmują tablice i obiekty i zwracają obiekty stdClass
.
Domyślnie wszystkie właściwości są opcjonalne i mają domyślną wartość null
. Obowiązkowe właściwości
można zdefiniować za pomocą required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // wartość domyślna to null
]);
$processor->process($schema, ['optional' => '']);
// BŁĄD: brakuje opcji 'required'
$processor->process($schema, ['required' => 'foo']);
// OK, zwraca {'required' => 'foo', 'optional' => null}
Jeśli nie chcesz mieć na wyjściu właściwości z wartością domyślną, użyj skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, zwraca {'required' => 'foo'}
Chociaż null
jest domyślną wartością właściwości optional
, w danych wejściowych nie jest
dozwolony (wartością musi być string). Właściwości akceptujące null
definiujemy za pomocą
nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// BŁĄD: 'optional' oczekuje stringa, podano null.
$processor->process($schema, ['nullable' => null]);
// OK, zwraca {'optional' => null, 'nullable' => null}
Tablicę wszystkich właściwości struktury zwraca metoda getShape()
.
Domyślnie w danych wejściowych nie mogą znajdować się żadne dodatkowe elementy:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// BŁĄD: Nieoczekiwany element 'additional'
Co możemy zmienić za pomocą otherItems()
. Jako parametr podamy schemat, według którego będą walidowane
dodatkowe elementy:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // BŁĄD
Nową strukturę możesz utworzyć, dziedzicząc po innej za pomocą extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Tablice
Tablice z zdefiniowanymi kluczami. Dotyczy ich wszystko, co struktury.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // wartość domyślna to null
]);
Można zdefiniować również tablicę indeksowaną, znaną jako tuple:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Przestarzałe właściwości
Możesz oznaczyć właściwość jako przestarzałą za pomocą metody deprecated([string $message])
. Informacje
o zakończeniu wsparcia są zwracane za pomocą $processor->getWarnings()
:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('Element %path% jest przestarzały'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Element 'old' jest przestarzały"]
Zakresy: min() max()
Za pomocą min()
i max()
można w przypadku tablic ograniczyć liczbę elementów:
// tablica, co najmniej 10 elementów, maksymalnie 20 elementów
Expect::array()->min(10)->max(20);
W przypadku ciągów znaków ograniczyć ich długość:
// ciąg znaków, co najmniej 10 znaków długości, maksymalnie 20 znaków
Expect::string()->min(10)->max(20);
W przypadku liczb ograniczyć ich wartość:
// liczba całkowita, od 10 do 20 włącznie
Expect::int()->min(10)->max(20);
Oczywiście można podać tylko min()
lub tylko max()
:
// ciąg znaków o maksymalnej długości 20 znaków
Expect::string()->max(20);
Wyrażenia regularne: pattern()
Za pomocą pattern()
można podać wyrażenie regularne, któremu musi odpowiadać cały wejściowy ciąg
znaków (tj. jakby był otoczony znakami ^
i $
):
// dokładnie 9 cyfr
Expect::string()->pattern('\d{9}');
Własne ograniczenia: assert()
Dowolne inne ograniczenia podajemy za pomocą assert(callable $fn)
.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // liczba musi być parzysta
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // BŁĄD: 3 nie jest liczbą parzystą
Lub
Expect::string()->assert('is_file'); // plik musi istnieć
Do każdego ograniczenia możesz dodać własny opis. Będzie on częścią komunikatu o błędzie.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Even items in array');
$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Even items in array" for item with value array.
Metodę można wywoływać wielokrotnie, aby dodać więcej ograniczeń. Można ją przeplatać z wywołaniami
transform()
i castTo()
.
Transformacje: transform()
Pomyślnie zwalidowane dane można modyfikować za pomocą własnej funkcji:
// konwersja na wielkie litery:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Metodę można wywoływać wielokrotnie, aby dodać więcej transformacji. Można ją przeplatać z wywołaniami
assert()
i castTo()
. Operacje zostaną wykonane w kolejności, w jakiej są zadeklarowane:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // konwersja na wielkie litery
Metoda transform()
może jednocześnie transformować i walidować wartość. Jest to często prostsze i mniej
powtarzalne niż łączenie transform()
i assert()
. W tym celu funkcja otrzymuje obiekt Context z metodą addError()
, której
można użyć do dodania informacji o problemach z walidacją:
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);
});
Rzutowanie: castTo()
Pomyślnie zwalidowane dane można rzutować:
Expect::scalar()->castTo('string');
Oprócz natywnych typów PHP można rzutować również na klasy. Rozróżnia się przy tym, czy chodzi o prostą klasę bez konstruktora, czy klasę z konstruktorem. Jeśli klasa nie ma konstruktora, tworzy się jej instancja, a wszystkie elementy struktury zapisuje się do właściwości:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// tworzy '$obj = new Info' i zapisuje do $obj->processRefund i $obj->refundAmount
Jeśli klasa ma konstruktor, elementy struktury przekazuje się jako nazwane parametry konstruktora:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// tworzy $obj = new Info(processRefund: ..., refundAmount: ...)
Rzutowanie w połączeniu z parametrem skalarnym tworzy obiekt i przekazuje wartość jako jedyny parametr konstruktora:
Expect::string()->castTo(DateTime::class);
// tworzy new DateTime(...)
Normalizacja: before()
Przed samą walidacją dane można znormalizować za pomocą metody before()
. Jako przykład podajmy element,
który musi być tablicą ciągów znaków (na przykład ['a', 'b', 'c']
), ale przyjmuje wejście w formie ciągu
a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK i zwraca ['a', 'b', 'c']
Mapowanie na obiekty: from()
Schemat struktury możemy wygenerować na podstawie klasy. Przykład:
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}
Wspierane są również klasy anonimowe:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Ponieważ informacje uzyskane z definicji klasy mogą być niewystarczające, możesz drugim parametrem uzupełnić elementy o własny schemat:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);