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