Schema: Datenvalidierung
Praktische Bibliothek zur Validierung und Normalisierung von Datenstrukturen anhand eines gegebenen Schemas mit einer cleveren, verständlichen API.
Installation:
composer require nette/schema
Grundlegende Verwendung
In der Variablen $schema
haben wir das Validierungsschema (was das genau bedeutet und wie man ein solches Schema
erstellt, erklären wir gleich) und in der Variablen $data
die Datenstruktur, die wir validieren und normalisieren
möchten. Dies können beispielsweise Daten sein, die vom Benutzer über eine API-Schnittstelle gesendet wurden, eine
Konfigurationsdatei usw.
Die Aufgabe übernimmt die Klasse Nette\Schema\Processor, die die Eingabe verarbeitet und entweder normalisierte Daten zurückgibt oder im Fehlerfall eine Ausnahme Nette\Schema\ValidationException auslöst.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Daten sind ungültig: ' . $e->getMessage();
}
Die Methode $e->getMessages()
gibt ein Array aller Nachrichten als Strings zurück und
$e->getMessageObjects()
gibt alle Nachrichten als “Nette\Schema\Message”-Objekte zurück:https://api.nette.org/…Message.html.
Schema definieren
Und nun erstellen wir das Schema. Zu seiner Definition dient die Klasse Nette\Schema\Expect, wir definieren eigentlich die
Erwartungen, wie die Daten aussehen sollen. Nehmen wir an, die Eingabedaten müssen eine Struktur (z. B. ein Array) bilden, die
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 verständlich aussieht, auch wenn Sie sie zum ersten Mal sehen.
Senden wir folgende Daten zur Validierung:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, besteht die Validierung
Die Ausgabe, also der Wert $normalized
, ist ein stdClass
-Objekt. Wenn wir möchten, dass die Ausgabe
ein Array ist, ergänzen wir das Schema um die Typumwandlung Expect::structure([...])->castTo('array')
.
Alle Elemente der Struktur sind optional und haben den Standardwert null
. Beispiel:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, besteht die Validierung
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Dass der Standardwert null
ist, bedeutet nicht, dass 'processRefund' => null
in den Eingabedaten
akzeptiert würde. Nein, die Eingabe muss ein Boolean sein, also nur true
oder false
. Um
null
zu erlauben, müssten wir dies explizit mit Expect::bool()->nullable()
tun.
Ein Element kann mit Expect::bool()->required()
als erforderlich markiert werden. Den Standardwert ändern wir
z. B. auf false
mit Expect::bool()->default(false)
oder kurz Expect::bool(false)
.
Und was wäre, wenn wir neben Boolean auch 1
und 0
akzeptieren wollten? Dann geben wir eine
Aufzählung von Werten an, die wir zusätzlich in Boolean normalisieren lassen:
$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 ein Schema definiert wird und wie sich die einzelnen Elemente der Struktur verhalten. Nun zeigen wir Ihnen, welche weiteren Elemente bei der Definition eines Schemas verwendet werden können.
Datentypen: type()
Im Schema können alle Standard-PHP-Datentypen angegeben werden:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Und weiterhin alle Typen, die von der Klasse Validators
unterstützt werden, zum Beispiel Expect::type('scalar')
oder kurz Expect::scalar()
. Auch Klassen-
oder Schnittstellennamen, zum Beispiel Expect::type('AddressEntity')
.
Es kann auch eine Union-Notation verwendet werden:
Expect::type('bool|string|array')
Der Standardwert ist immer null
, mit Ausnahme von array
und list
, wo es ein leeres Array
ist. (Ein List ist ein Array, das nach aufsteigender Reihe numerischer Schlüssel ab Null indiziert ist, also ein
nicht-assoziatives Array).
Wert-Arrays: arrayOf() listOf()
Ein Array stellt eine zu allgemeine Struktur dar, es ist nützlicher anzugeben, welche Elemente es genau enthalten darf. Zum Beispiel ein Array, dessen Elemente nur Strings sein dürfen:
$schema = Expect::arrayOf('string');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // FEHLER: 123 ist kein String
Mit dem zweiten Parameter können Schlüssel spezifiziert werden (ab Version 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // FEHLER: 'a' ist kein Int
Ein List ist ein indiziertes Array:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // FEHLER: 123 ist kein String
$processor->process($schema, ['key' => 'a']); // FEHLER: kein List
$processor->process($schema, [1 => 'a', 0 => 'b']); // FEHLER: auch kein List
Der Parameter kann auch ein Schema sein, wir können also schreiben:
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 (ab Version 1.1).
Aufzählung: anyOf()
anyOf()
stellt eine Aufzählung von Werten oder Schemata dar, die der Wert annehmen kann. So schreiben wir 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]); // FEHLER: false gehört nicht dazu
Die Elemente der Aufzählung 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. Wenn Sie ihr ein Array von
Werten übergeben möchten, verwenden Sie den Unpacking-Operator anyOf(...$variants)
.
Der Standardwert ist null
. Mit der Methode firstIsDefault()
machen wir das erste Element zum
Standard:
// Standard ist 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Strukturen
Strukturen sind Objekte mit definierten Schlüsseln. Jedes Schlüssel-Wert-Paar wird als „Eigenschaft“ bezeichnet:
Strukturen akzeptieren Arrays und Objekte und geben stdClass
-Objekte zurück.
Standardmäßig sind alle Eigenschaften optional und haben den Standardwert null
. Erforderliche Eigenschaften
können Sie mit required()
definieren:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // Standardwert ist null
]);
$processor->process($schema, ['optional' => '']);
// FEHLER: Option 'required' fehlt
$processor->process($schema, ['required' => 'foo']);
// OK, gibt {'required' => 'foo', 'optional' => null} zurück
Wenn Sie in der Ausgabe keine Eigenschaften mit Standardwerten haben möchten, verwenden Sie skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, gibt {'required' => 'foo'} zurück
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, definieren wir mit nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// FEHLER: 'optional' erwartet einen String, null wurde gegeben.
$processor->process($schema, ['nullable' => null]);
// OK, gibt {'optional' => null, 'nullable' => null} zurück
Ein Array aller Eigenschaften einer Struktur gibt die Methode getShape()
zurück.
Standardmäßig dürfen in den Eingabedaten keine zusätzlichen Elemente vorhanden sein:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// FEHLER: Unerwartetes Element 'additional'
Dies können wir mit otherItems()
ändern. Als Parameter geben wir ein Schema an, nach dem die zusätzlichen
Elemente validiert werden:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // FEHLER
Eine neue Struktur können Sie durch Ableitung von einer anderen mit extend()
erstellen:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Array
Array mit definierten Schlüsseln. Für ihn gilt alles, was auch für Strukturen gilt.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // Standardwert ist null
]);
Es kann auch ein indiziertes Array, bekannt als Tupel, definiert werden:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Veraltete Eigenschaften
Sie können eine Eigenschaft mit der Methode deprecated([string $message])
als veraltet markieren. Informationen
über die Einstellung der Unterstützung werden mit $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()
Mit min()
und max()
kann die Anzahl der Elemente in Arrays begrenzt werden:
// Array, mindestens 10 Elemente, maximal 20 Elemente
Expect::array()->min(10)->max(20);
Bei Strings kann ihre Länge begrenzt werden:
// String, mindestens 10 Zeichen lang, maximal 20 Zeichen
Expect::string()->min(10)->max(20);
Bei Zahlen kann ihr Wert begrenzt werden:
// ganze Zahl, zwischen 10 und 20 einschließlich
Expect::int()->min(10)->max(20);
Natürlich ist es möglich, nur min()
oder nur max()
anzugeben:
// String maximal 20 Zeichen
Expect::string()->max(20);
Reguläre Ausdrücke: pattern()
Mit pattern()
kann ein regulärer Ausdruck angegeben werden, dem die gesamte Eingabezeichenkette
entsprechen muss (d.h. als wäre sie von den Zeichen ^
und $
umschlossen):
// genau 9 Zahlen
Expect::string()->pattern('\d{9}');
Eigene Einschränkungen: assert()
Beliebige weitere Einschränkungen geben wir mit assert(callable $fn)
an.
$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']); // FEHLER: 3 ist keine gerade Anzahl
Oder
Expect::string()->assert('is_file'); // Datei muss existieren
Zu jeder Einschränkung können Sie eine eigene Beschreibung hinzufügen. Diese wird Teil der Fehlermeldung sein.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Gerade Anzahl Elemente im Array');
$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Gerade Anzahl Elemente im Array" for item with value array.
Die Methode kann wiederholt aufgerufen werden, um mehrere Einschränkungen hinzuzufügen. Sie kann mit Aufrufen von
transform()
und castTo()
verschachtelt werden.
Transformationen: transform()
Erfolgreich validierte Daten können mit einer eigenen Funktion angepasst werden:
// Umwandlung in Großbuchstaben:
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()
verschachtelt werden. Die Operationen werden in der Reihenfolge ausgeführt, in
der sie deklariert sind:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'Alle Zeichen müssen klein geschrieben sein')
->transform(fn(string $s) => strtoupper($s)); // Umwandlung in Großbuchstaben
Die Methode transform()
kann gleichzeitig den Wert 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 der Methode
addError()
, die verwendet werden kann, um Informationen über Validierungsprobleme hinzuzufügen:
Expect::string()
->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('Alle Zeichen müssen klein geschrieben sein', 'my.case.error');
return null;
}
return strtoupper($s);
});
Typumwandlung: castTo()
Erfolgreich validierte Daten können typumgewandelt werden:
Expect::scalar()->castTo('string');
Neben nativen PHP-Typen kann auch in Klassen umgewandelt werden. Dabei wird unterschieden, ob es sich um eine einfache Klasse ohne Konstruktor oder eine Klasse mit Konstruktor handelt. Wenn die Klasse keinen Konstruktor hat, wird eine Instanz davon erstellt und alle Elemente der Struktur werden in die Properties geschrieben:
class Info
{
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
])->castTo(Info::class);
// erstellt '$obj = new Info' und schreibt in $obj->processRefund und $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,
) {
}
}
// erstellt $obj = new Info(processRefund: ..., refundAmount: ...)
Die Typumwandlung in Kombination mit einem skalaren Parameter erstellt ein Objekt und übergibt den Wert als einzigen Parameter an den Konstruktor:
Expect::string()->castTo(DateTime::class);
// erstellt new DateTime(...)
Normalisierung: before()
Vor der eigentlichen Validierung können die Daten mit der Methode before()
normalisiert werden. Als Beispiel
nehmen wir ein Element, das ein Array von Strings sein muss (zum Beispiel ['a', 'b', 'c']
), aber eine Eingabe in Form
des Strings a b c
akzeptiert:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK und gibt ['a', 'b', 'c'] zurück
Mapping auf Objekte: from()
Wir können uns das Strukturschema aus einer Klasse generieren lassen. Beispiel:
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}
Auch anonyme Klassen werden 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 den Elementen mit dem zweiten Parameter ein eigenes Schema hinzufügen:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);