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 (es sei denn, Sie ändern dies mit castTo('array'), etc.).

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}

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

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