Schema: Validierung von Daten
Eine praktische Bibliothek zur Validierung und Normalisierung von Datenstrukturen anhand eines vorgegebenen Schemas mit einer intelligenten und leicht verständlichen API.
Installation:
composer require nette/schema
Grundlegende Verwendung
In der Variablen $schema
haben wir ein Validierungsschema (was genau das bedeutet und wie man es erstellt, wird
später erklärt) und in der Variablen $data
haben wir eine Datenstruktur, die wir validieren und normalisieren
wollen. Dies können z. B. Daten sein, die vom Benutzer über eine API, eine Konfigurationsdatei usw. gesendet werden.
Die Aufgabe wird von der Klasse Nette\Schema\Processor übernommen, die die Eingabe verarbeitet und entweder normalisierte Daten zurückgibt oder im Fehlerfall eine Nette\Schema\ValidationException Exception wirft.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data is invalid: ' . $e->getMessage();
}
Die Methode $e->getMessages()
liefert ein Array mit allen Nachrichtenstrings und
$e->getMessageObjects()
liefert alle Nachrichten als „Nette\Schema\Message“-Objekte:https://api.nette.org/…Message.html zurück.
Schema definieren
Und nun erstellen wir ein Schema. Die Klasse Nette\Schema\Expect wird dazu verwendet, um es zu definieren,
d.h. wir definieren Erwartungen, wie die Daten aussehen sollen. Sagen wir, dass die Eingabedaten eine Struktur (z. B. ein Array)
sein müssen, die Elemente processRefund
vom Typ bool und refundAmount
vom Typ int enthält.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Wir glauben, dass die Schemadefinition klar aussieht, auch wenn Sie sie zum ersten Mal sehen.
Lassen Sie uns die folgenden Daten zur Validierung senden:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, es funktioniert
Die Ausgabe, d. h. der Wert $normalized
, ist das Objekt stdClass
. Wenn wir wollen, dass die Ausgabe
ein Array ist, fügen wir einen Cast zu schema Expect::structure([...])->castTo('array')
.
Alle Elemente der Struktur sind optional und haben einen Standardwert null
. Beispiel:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, es funktioniert
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Die Tatsache, dass der Standardwert null
ist, bedeutet nicht, dass er in den Eingabedaten
'processRefund' => null
akzeptiert würde. Nein, die Eingabe muss boolesch sein, d. h. nur true
oder
false
. Wir müssten null
über Expect::bool()->nullable()
ausdrücklich zulassen.
Ein Element kann mit Expect::bool()->required()
obligatorisch gemacht werden. Wir ändern den Standardwert auf
false
mit Expect::bool()->default(false)
oder kurz mit Expect::bool(false)
.
Und was, wenn wir neben Booleschen Werten auch 1
and 0
akzeptieren wollen? Dann listen wir die
zulässigen Werte auf, die wir ebenfalls zu booleschen Werten normalisieren werden:
$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
Jetzt kennen Sie die Grundlagen, wie das Schema definiert ist und wie sich die einzelnen Elemente der Struktur verhalten. Wir werden nun zeigen, was alle anderen Elemente bei der Definition eines Schemas verwendet werden können.
Datentypen: type()
Alle Standard-PHP-Datentypen können im Schema aufgeführt werden:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Und dann alle Typen , die von den Validatoren über
Expect::type('scalar')
oder abgekürzt Expect::scalar()
unterstützt werden. Auch Klassen- oder Schnittstellennamen
werden akzeptiert, z.B. Expect::type('AddressEntity')
.
Sie können auch die Union-Notation verwenden:
Expect::type('bool|string|array')
Der Standardwert ist immer null
, außer bei array
und list
, wo es sich um ein leeres
Array handelt. (Eine Liste ist ein Array, das in aufsteigender Reihenfolge der numerischen Schlüssel von Null an indiziert ist,
also ein nicht-assoziatives Array).
Array von Werten: arrayOf() listOf()
Das Array ist eine zu allgemeine Struktur, es ist sinnvoller, genau anzugeben, welche Elemente es enthalten kann. Zum Beispiel, ein Array, dessen Elemente nur Strings sein können:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hallo', 'welt']); // OK
$processor->process($schema, ['a' => 'hallo', 'b' => 'welt']); // OK
$processor->process($schema, ['key' => 123]); // FEHLER: 123 ist kein String
Der zweite Parameter kann zur Angabe von Schlüsseln verwendet werden (seit Version 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hallo', 'welt']); // OK
$processor->process($schema, ['a' => 'hallo']); // ERROR: 'a' ist nicht int
Die Liste ist ein indiziertes Array:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // FEHLER: 123 ist keine Zeichenkette
$processor->process($schema, ['key' => 'a']); // FEHLER: ist keine Liste
$processor->process($schema, [1 => 'a', 0 => 'b']); // FEHLER: ist keine Liste
Der Parameter kann auch ein Schema sein, so dass wir schreiben können:
Expect::arrayOf(Expect::bool())
Der Standardwert ist ein leeres Array. Wenn Sie einen Standardwert angeben, wird dieser mit den übergebenen Daten
zusammengeführt. Dies kann mit mergeDefaults(false)
deaktiviert werden (seit Version 1.1).
Aufzählung: anyOf()
anyOf()
ist eine Menge von Werten oder Schemata, die ein Wert sein kann. So schreiben Sie ein Array von Elementen,
die entweder 'a'
, true
oder null
sein können:
$schema = Expect::listOf(
Expect::anyOf('a', true, null),
);
$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERROR: false gehört nicht dazu
Die Aufzählungselemente können auch Schemata sein:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // FEHLER
Die Methode anyOf()
akzeptiert Varianten als einzelne Parameter, nicht als Array. Um ihr ein Array von Werten zu
übergeben, verwenden Sie den Entpackungsoperator anyOf(...$variants)
.
Der Standardwert ist null
. Verwenden Sie die Methode firstIsDefault()
, um das erste Element zum
Standardwert zu machen:
// Standard ist 'hallo'
Expect::anyOf(Expect::string('hallo'), true, null)->firstIsDefault();
Strukturen
Strukturen sind Objekte mit definierten Schlüsseln. Jedes dieser Schlüssel ⇒ Wert-Paare wird als „Eigenschaft“ bezeichnet:
Strukturen akzeptieren Arrays und Objekte und geben Objekte zurück stdClass
.
Standardmäßig sind alle Eigenschaften optional und haben einen Standardwert von null
. Obligatorische
Eigenschaften können Sie mit required()
definieren:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // der Standardwert ist null
]);
$processor->process($schema, ['optional' => '']);
// ERROR: option 'required' is missing
$processor->process($schema, ['required' => 'foo']);
// OK, liefert {'required' => 'foo', 'optional' => null}
Wenn Sie keine Eigenschaften mit nur einem Standardwert ausgeben wollen, verwenden Sie skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, liefert {'required' => 'foo'}
Obwohl null
der Standardwert der Eigenschaft optional
ist, ist er in den Eingabedaten nicht erlaubt
(der Wert muss ein String sein). Eigenschaften, die null
akzeptieren, werden mit nullable()
definiert:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// FEHLER: 'optional' wird als String erwartet, null gegeben.
$processor->process($schema, ['nullable' => null]);
// OK, liefert {'optional' => null, 'nullable' => null}
Das Array mit allen Struktureigenschaften wird von der Methode getShape()
zurückgegeben.
Standardmäßig können keine zusätzlichen Elemente in den Eingabedaten enthalten sein:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ERROR: Unexpected item 'additional'
Das können wir mit otherItems()
ändern. Als Parameter wird das Schema für jedes zusätzliche Element
angegeben:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERROR
Sie können eine neue Struktur erstellen, indem Sie mit extend()
von einer anderen ableiten:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Array
Ein Array mit definierten Schlüsseln. Es gelten die gleichen Regeln wie für Strukturen.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Sie können auch ein indiziertes Array, ein sogenanntes Tupel, definieren:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Verwerfungen
Sie können eine Eigenschaft mit der deprecated([string $message])
Methode. Verwerfungshinweise werden von
$processor->getWarnings()
zurückgegeben:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('Das Element %path% ist veraltet'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Das Element 'old' ist veraltet"]
Bereiche: min() max()
Verwenden Sie min()
und max()
, um die Anzahl der Elemente für Arrays zu begrenzen:
// Array, mindestens 10 Elemente, maximal 20 Elemente
Expect::array()->min(10)->max(20);
Bei Zeichenketten begrenzen Sie deren Länge:
// String, mindestens 10 Zeichen lang, maximal 20 Zeichen
Expect::string()->min(10)->max(20);
Bei Zahlen begrenzen Sie ihren Wert:
// Ganzzahl, zwischen 10 und 20 einschließlich
Expect::int()->min(10)->max(20);
Es ist natürlich möglich, nur min()
oder nur max()
zu nennen:
// Zeichenkette, maximal 20 Zeichen
Expect::string()->max(20);
Reguläre Ausdrücke: pattern()
Mit pattern()
können Sie einen regulären Ausdruck angeben, mit dem die gesamte Eingabezeichenkette
übereinstimmen muss (d.h. als ob sie in Zeichen eingeschlossen wäre ^
a $
):
// nur 9 Ziffern
Expect::string()->pattern('\d{9}');
Benutzerdefinierte Assertions: assert()
Sie können weitere Einschränkungen mit assert(callable $fn)
hinzufügen.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // die Anzahl muss gerade sein
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 ist nicht gerade
Oder
Expect::string()->assert('is_file'); // die Datei muss existieren
Sie können Ihre eigene Beschreibung für jede Assertion hinzufügen. Sie wird Teil der Fehlermeldung sein.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Gerade Elemente in Array');
$processor->process($schema, ['a', 'b', 'c']);
// Fehlgeschlagene Assertion "Gerade Elemente in Array" für Element mit Wert array.
Die Methode kann wiederholt aufgerufen werden, um mehrere Beschränkungen hinzuzufügen. Sie kann mit Aufrufen von
transform()
und castTo()
vermischt werden.
Umwandlung: transform()
Erfolgreich überprüfte Daten können mit einer benutzerdefinierten Funktion geändert werden:
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Die Methode kann wiederholt aufgerufen werden, um mehrere Transformationen hinzuzufügen. Sie kann mit Aufrufen von
assert()
und castTo()
vermischt werden. Die Operationen werden in der Reihenfolge ausgeführt, in der
sie deklariert sind:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase
Die Methode transform()
kann den Wert gleichzeitig transformieren und validieren. Dies ist oft einfacher und
weniger redundant als die Verkettung von transform()
und assert()
. Zu diesem Zweck erhält die Funktion
ein Context-Objekt mit einer addError()
-Methode, die dazu verwendet werden kann, Informationen über Validierungsprobleme hinzuzufügen:
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);
});
Gießen: castTo()
Erfolgreich überprüfte Daten können gecastet werden:
Expect::scalar()->castTo('string');
Zusätzlich zu den nativen PHP-Typen können Sie auch auf Klassen casten. Es wird unterschieden, ob es sich um eine einfache Klasse ohne Konstruktor oder um eine Klasse mit Konstruktor handelt. Wenn die Klasse keinen Konstruktor hat, wird eine Instanz der Klasse erstellt und alle Elemente der Struktur werden in ihre Eigenschaften geschrieben:
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
Wenn die Klasse einen Konstruktor hat, werden die Elemente der Struktur als benannte Parameter an den Konstruktor übergeben:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
Casting in Verbindung mit einem skalaren Parameter erzeugt ein Objekt und übergibt den Wert als einzigen Parameter an den Konstruktor:
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
Normalisierung: before()
Vor der eigentlichen Validierung können die Daten mit der Methode before()
normalisiert werden. Als Beispiel
nehmen wir ein Element an, das ein Array von Strings sein muss (z.B. ['a', 'b', 'c']
), aber eine Eingabe in Form
einer Zeichenkette erhält a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK, liefert ['a', 'b', 'c']
Zuordnung zu Objekten: from()
Sie können aus der Klasse ein Strukturschema erzeugen. Beispiel:
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}
Anonyme Klassen werden ebenfalls unterstützt:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Da die aus der Klassendefinition gewonnenen Informationen möglicherweise nicht ausreichen, können Sie mit dem zweiten Parameter ein benutzerdefiniertes Schema für die Elemente hinzufügen:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);