Schema: zatwierdzanie danych
Poręczna biblioteka do walidacji i normalizacji struktur danych względem danego schematu z inteligentnym, łatwym do zrozumienia API.
Instalacja:
composer require nette/schema
Zastosowanie podstawowe
W zmiennej $schema
mamy schemat walidacji (o tym, co to dokładnie znaczy i jak stworzyć taki schemat powiemy
później), a w zmiennej $data
mamy strukturę danych, którą chcemy zwalidować i znormalizować. Mogą to być
np. dane przesłane przez użytkownika poprzez API, plik konfiguracyjny itp.
Zadanie to jest obsługiwane przez klasę Nette\Schema\Processor, która przetwarza dane wejściowe 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 'Data nejsou platná: ' . $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\Message.
Definiowanie schematu
Teraz stwórzmy schemat. Do jego zdefiniowania służy klasa Nette\Schema\Expect, tak naprawdę definiujemy oczekiwania
co do tego, jak powinny wyglądać dane. Powiedzmy, że dane wejściowe muszą być strukturą (na przykład tablicą)
zawierającą elementy processRefund
typu bool oraz refundAmount
typu int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Wierzymy, że definicja schematu wygląda na łatwą do zrozumienia, nawet jeśli widzisz ją po raz pierwszy.
Prześlemy następujące dane do walidacji:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, projde validací
Wyjściem, czyli wartością $normalized
, jest obiekt stdClass
. Jeśli chcielibyśmy, aby wyjście
było tablicą, dodamy nadpisanie schematu 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, projde validací
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Fakt, że wartością domyślną jest null
, nie oznacza, że 'processRefund' => null
zostałaby
zaakceptowana w danych wejściowych. Nie, dane wejściowe muszą być booleanem, czyli tylko true
lub
false
. Aby dopuścić null
musielibyśmy wyraźnie użyć
Expect::bool()->nullable()
.
Wpis może być obowiązkowy za pomocą Expect::bool()->required()
. Możemy zmienić wartość domyślną na
false
za pomocą Expect::bool()->default(false)
lub w skrócie Expect::bool(false)
.
A co by było gdybyśmy chcieli oprócz boolean przyjąć 1
a 0
? Następnie wymieniamy wartości,
które dodatkowo pozwalamy znormalizować do 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 znasz już podstawy definiowania schematu i tego, jak zachowują się elementy struktury. Teraz zobaczymy, jakie pozostałe elementy mogą być użyte podczas definiowania schematu.
Typy danych: type()
Wszystkie standardowe typy danych PHP mogą być wymienione w schemacie:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Jak również wszystkie typy obsługiwane przez klasę
Validators, na przykład Expect::type('scalar')
lub w skrócie Expect::scalar()
. Również nazwy
klas lub interfejsów, na przykład Expect::type('AddressEntity')
.
Można również zastosować notację unijną:
Expect::type('bool|string|array')
Domyślnie zawsze jest to null
z wyjątkiem array
i list
, gdzie jest to puste pole.
(Lista to tablica indeksowana przez rosnący szereg kluczy numerycznych od zera, czyli tablica nieasocjacyjna).
Tablica wartości: arrayOf() listOf()
Tablica jest zbyt ogólną strukturą; bardziej przydatne jest określenie dokładnie, jakie elementy może zawierać. Na przykład tablica, której elementami mogą być tylko ciągi 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]); // CHYBA: 123 není string
Drugi parametr może być użyty do określenia kluczy (od wersji 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // CHYBA: 'a' není int
Lista jest tablicą indeksowaną:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 nie jest ciągiem znaków
$processor->process($schema, ['key' => 'a']); // ERROR: not a list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: również nie lista
Parametr może być również schematem, więc możemy napisać:
Expect::arrayOf(Expect::bool())
Domyślną wartością jest puste pole. Jeśli określisz wartość domyślną, zostanie ona połączona z przekazanymi
danymi. Można to wyłączyć za pomocą mergeDefaults(false)
(od wersji 1.1).
Wyliczenie: anyOf()
anyOf()
reprezentuje wyliczenie wartości lub schematów, które może przyjąć dana wartość. W ten sposób
piszemy tablicę z elementami, które mogą być albo 'a'
, true
, albo null
:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // CHYBA: false tam nepatří
Elementami wyliczenia mogą być również schematy:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // CHYBA
Metoda anyOf()
przyjmuje warianty jako pojedyncze parametry, a nie tablice. Aby przekazać mu tablicę wartości,
użyj operatora rozpakowywania anyOf(...$variants)
.
Wartość domyślna to null
. Użyj metody firstIsDefault()
, aby pierwszy element był domyślny:
// domyślnie jest to "hello
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Struktury
Struktury to obiekty o zdefiniowanych kluczach. Każda z par klucz ⇒ wartość jest określana jako “właściwość”:
Struktury akceptują tablice i obiekty oraz zwracają obiekty stdClass
.
Domyślnie wszystkie właściwości są opcjonalne i domyślnie ustawione na null
. Możesz zdefiniować
właściwości obowiązkowe używając required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // výchozí hodnota je null
]);
$processor->process($schema, ['optional' => '']);
// CHYBA: brakuje opcji 'required'
$processor->process($schema, ['required' => 'foo']);
// OK, vrací {'required' => 'foo', 'optional' => null}
Jeśli nie chcesz, aby właściwości z wartością domyślną znalazły się na wyjściu, użyj
skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, vrací {'required' => 'foo'}
Chociaż null
jest domyślną wartością właściwości optional
, nie jest dozwolone w danych
wejściowych (wartość musi być łańcuchem). Właściwości akceptujące null
są definiowane za pomocą
nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// CHYBA: 'optional' expects to be string, null given.
$processor->process($schema, ['nullable' => null]);
// OK, vrací {'optional' => null, 'nullable' => null}
Tablica wszystkich właściwości struktury jest zwracana przez metodę getShape()
.
Domyślnie w danych wejściowych nie może być żadnych dodatkowych elementów:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// CHYBA: Nieoczekiwany element 'additional'
Które można zmienić za pomocą otherItems()
. Jako parametr podajemy 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]); // CHYBA
Nową strukturę można utworzyć poprzez wyprowadzenie jej z innej za pomocą extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Tablica
Tablica ze zdefiniowanymi kluczami. Obowiązują te same zasady, co w przypadku struktur.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Można również zdefiniować tablicę indeksowaną, znaną jako krotka:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Właściwości przestarzałe
Możesz oznaczyć właściwość jako przestarzałą używając metody deprecated([string $message])
. Informacje
o deprecjacji są zwracane za pomocą $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"]
Zakresy: min() max()
Użyj min()
i max()
, aby ograniczyć liczbę elementów dla tablic:
// pole, minimum 10 pozycji, maksimum 20 pozycji
Expect::array()->min(10)->max(20);
W przypadku ciągów, ogranicz ich długość:
// ciąg znaków o długości co najmniej 10 znaków, maksymalnie 20 znaków
Expect::string()->min(10)->max(20);
W przypadku liczb, ogranicz ich wartość:
// liczba całkowita, między 10 a 20 włącznie
Expect::int()->min(10)->max(20);
Oczywiście można wymienić tylko min()
, lub tylko max()
:
// ciąg o długości maksymalnie 20 znaków
Expect::string()->max(20);
Wyrażenia regularne: pattern()
Możesz użyć pattern()
do określenia wyrażenia regularnego, do którego musi pasować cały łańcuch
wejściowy (tj. tak jakby zawinięty przez ^
a $
):
// tylko 9 liczb
Expect::string()->pattern('\d{9}');
Niestandardowe ograniczenie: assert()
Wszelkie dodatkowe ograniczenia można określić 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']); // ERROR: 3 nie jest liczbą parzystą
Albo
Expect::string()->assert('is_file'); // plik musi istnieć
Do każdego ograniczenia można dodać własny opis. Zostanie to uwzględnione w komunikacie 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.
Metoda ta może być wywoływana wielokrotnie w celu dodania wielu ograniczeń. Można ją łączyć z wywołaniami
transform()
i castTo()
.
Transformacja: transform()
Pomyślnie zweryfikowane dane można modyfikować za pomocą funkcji niestandardowej:
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Metoda ta może być wywoływana wielokrotnie w celu dodania wielu przekształceń. Można ją mieszać z wywołaniami
assert()
i castTo()
. Operacje będą wykonywane w kolejności, w jakiej zostały zadeklarowane:
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()
może jednocześnie przekształcać i weryfikować wartość. Jest to często prostsze
i mniej zbędne niż łączenie metod transform()
i assert()
. W tym celu funkcja otrzymuje obiekt Context z metodą addError()
, która może
być użyta do dodania informacji o kwestiach walidacji:
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()
Pomyślnie zweryfikowane dane mogą być rzutowane:
Expect::scalar()->castTo('string');
Oprócz natywnych typów PHP, można również rzutować na klasy. Rozróżnia to, czy jest to prosta klasa bez konstruktora, czy klasa z konstruktorem. Jeśli klasa nie ma konstruktora, tworzona jest jej instancja, a wszystkie elementy struktury są zapisywane w jej właściwościach:
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
Jeśli klasa ma konstruktor, elementy struktury są przekazywane jako nazwane parametry do konstruktora:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
Rzutowanie w połączeniu z parametrem skalarnym tworzy obiekt i przekazuje wartość jako jedyny parametr do konstruktora:
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
Normalizacja: before()
Przed walidacją dane mogą zostać znormalizowane przy użyciu metody before()
. Jako przykład rozważmy element,
który musi być tablicą łańcuchów (np. ['a', 'b', 'c']
), ale otrzymuje dane wejściowe w postaci ciągu
a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK a vrátí ['a', 'b', 'c']
Mapowanie do obiektów: from()
Można wygenerować schemat struktury z klasy. Przykład:
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}
Obsługiwane 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ą nie być wystarczające, można dodać niestandardowy schemat dla elementów za pomocą drugiego parametru:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);