Schema : Validation des données

Une bibliothèque pratique pour la validation et la normalisation des structures de données par rapport à un schéma donné, avec une API intelligente et facile à comprendre.

Installation :

composer require nette/schema

Utilisation de base

Dans la variable $schema nous avons un schéma de validation (ce que cela signifie exactement et comment le créer nous le dirons plus tard) et dans la variable $data nous avons une structure de données que nous voulons valider et normaliser. Cela peut être, par exemple, des données envoyées par l'utilisateur via une API, un fichier de configuration, etc.

La tâche est gérée par la classe Nette\Schema\Processor, qui traite l'entrée et renvoie les données normalisées ou lève une exception Nette\Schema\ValidationException en cas d'erreur.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Data is invalid: ' . $e->getMessage();
}

La méthode $e->getMessages() renvoie un tableau de toutes les chaînes de messages et $e->getMessageObjects() renvoie tous les messages sous forme d'objets Nette\Schema\Message.

Définition du schéma

Et maintenant, créons un schéma. La classe Nette\Schema\Expect est utilisée pour le définir, nous définissons en fait les attentes de ce à quoi les données doivent ressembler. Disons que les données d'entrée doivent être une structure (par exemple un tableau) contenant des éléments processRefund de type bool et refundAmount de type int.

use Nette\Schema\Expect;

$schema = Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
]);

Nous pensons que la définition du schéma semble claire, même si vous la voyez pour la toute première fois.

Envoyons les données suivantes pour validation :

$data = [
	'processRefund' => true,
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // OK, ça passe.

La sortie, c'est-à-dire la valeur $normalized, est l'objet stdClass. Si nous voulons que la sortie soit un tableau, nous ajoutons un cast au schéma Expect::structure([...])->castTo('array').

Tous les éléments de la structure sont facultatifs et ont une valeur par défaut null. Exemple :

$data = [
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // OK, ça passe
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

Le fait que la valeur par défaut soit null ne signifie pas qu'elle serait acceptée dans les données d'entrée 'processRefund' => null. Non, l'entrée doit être booléenne, c'est-à-dire uniquement true ou false. Il faudrait autoriser explicitement null via Expect::bool()->nullable().

Un élément peut être rendu obligatoire en utilisant Expect::bool()->required(). Nous changeons la valeur par défaut en false en utilisant Expect::bool()->default(false) ou en peu de temps en utilisant Expect::bool(false).

Et si nous voulions accepter 1 and 0 en plus des booléens ? Nous dressons alors la liste des valeurs autorisées, que nous normaliserons également en booléen :

$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

Vous connaissez maintenant les bases de la définition du schéma et du comportement des différents éléments de la structure. Nous allons maintenant montrer quels sont tous les autres éléments qui peuvent être utilisés pour définir un schéma.

Types de données : type()

Tous les types de données standard de PHP peuvent être listés dans le schéma :

Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])

Et ensuite tous les types supportés par les validateurs via Expect::type('scalar') ou en abrégé Expect::scalar(). Les noms de classes ou d'interfaces sont également acceptés, par exemple Expect::type('AddressEntity').

Vous pouvez également utiliser la notation d'union :

Expect::type('bool|string|array')

La valeur par défaut est toujours null, sauf pour array et list, où il s'agit d'un tableau vide. (Une liste est un tableau indexé dans l'ordre croissant des clés numériques à partir de zéro, c'est-à-dire un tableau non associatif).

Tableau de valeurs : arrayOf() listOf()

Le tableau est une structure trop générale, il est plus utile de spécifier exactement quels éléments il peut contenir. Par exemple, un tableau dont les éléments ne peuvent être que des chaînes de caractères :

$schema = Expect::arrayOf('string');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERREUR : 123 n'est pas une chaîne de caractères.

Le second paramètre peut être utilisé pour spécifier les clés (depuis la version 1.2) :

$schema = Expect::arrayOf('string', 'int');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERREUR : 'a' n'est pas int

La liste est un tableau indexé :

$schema = Expect::listOf('string');

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERREUR : 123 n'est pas une chaîne de caractères
$processor->process($schema, ['key' => 'a']); // ERREUR : n'est pas une liste
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERREUR : n'est pas une liste.

Le paramètre peut aussi être un schéma, on peut donc écrire :

Expect::arrayOf(Expect::bool())

La valeur par défaut est un tableau vide. Si vous spécifiez la valeur par défaut, elle sera fusionnée avec les données passées. Ceci peut être désactivé en utilisant mergeDefaults(false) (depuis la version 1.1).

Enumération : anyOf()

anyOf() est un ensemble de valeurs ou de schémas que peut prendre une valeur. Voici comment écrire un tableau d'éléments qui peuvent être soit 'a', true, soit null:

$schema = Expect::listOf(
	Expect::anyOf('a', true, null),
);

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERREUR : false n'est pas à sa place.

Les éléments de l'énumération peuvent également être des schémas :

$schema = Expect::listOf(
	Expect::anyOf(Expect::string(), true, null),
);

$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ERREUR

La méthode anyOf() accepte les variantes comme paramètres individuels, et non comme tableau. Pour lui passer un tableau de valeurs, utilisez l'opérateur de décompression anyOf(...$variants).

La valeur par défaut est null. Utilisez la méthode firstIsDefault() pour faire du premier élément la valeur par défaut :

// la valeur par défaut est 'hello'.
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Structures

Les structures sont des objets avec des clés définies. Chacune de ces paires clé ⇒ valeur est appelée “propriété” :

Les structures acceptent des tableaux et des objets et renvoient des objets stdClass (sauf si vous le changez avec castTo('array'), etc.).

Par défaut, toutes les propriétés sont facultatives et ont une valeur par défaut de null. Vous pouvez définir des propriétés obligatoires en utilisant required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // la valeur par défaut est null
]);

$processor->process($schema, ['optional' => '']);
// ERROR: option 'required' is missing

$processor->process($schema, ['required' => 'foo']);
// OK, renvoie {'required' => 'foo', 'optional' => null}

Si vous ne souhaitez pas éditer les propriétés ayant uniquement une valeur par défaut, utilisez skipDefaults():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(),
])->skipDefaults();

$processor->process($schema, ['required' => 'foo']);
// OK, retourne {'required' => 'foo'}

Bien que null soit la valeur par défaut de la propriété optional, elle n'est pas autorisée dans les données d'entrée (la valeur doit être une chaîne de caractères). Les propriétés acceptant null sont définies à l'aide de nullable():

$schema = Expect::structure([
	'optional' => Expect::string(),
	'nullable' => Expect::string()->nullable(),
]);

$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' expects to be string, null given.

$processor->process($schema, ['nullable' => null]);
// OK, retourne {'optional' => null, 'nullable' => null}.

Par défaut, il ne peut y avoir aucun élément supplémentaire dans les données d'entrée :

$schema = Expect::structure([
	'key' => Expect::string(),
]);

$processor->process($schema, ['additional' => 1]);
// ERROR: Unexpected item 'additional'

Ce que nous pouvons changer avec otherItems(). En tant que paramètre, nous allons spécifier le schéma pour chaque élément supplémentaire :

$schema = Expect::structure([
	'key' => Expect::string(),
])->otherItems(Expect::int());

$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERREUR

Dépréciations

Vous pouvez déprécier une propriété en utilisant la méthode deprecated([string $message]) méthode. Les avis de dépréciation sont renvoyés par $processor->getWarnings():

$schema = Expect::structure([
	'old' => Expect::int()->deprecated('L'élément %path% est obsolète'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["L'élément 'old' est obsolète"].

Plages : min() max()

Utilisez min() et max() pour limiter le nombre d'éléments des tableaux :

// tableau, au moins 10 éléments, maximum 20 éléments
Expect::array()->min(10)->max(20);

Pour les chaînes de caractères, limitez leur longueur :

// chaîne de caractères, d'au moins 10 caractères et de 20 caractères maximum
Expect::string()->min(10)->max(20);

Pour les nombres, limitez leur valeur :

// nombre entier, entre 10 et 20 inclus
Expect::int()->min(10)->max(20);

Bien sûr, il est possible de ne mentionner que min(), ou que max():

// chaîne de caractères, 20 caractères maximum
Expect::string()->max(20);

Expressions régulières : pattern()

En utilisant pattern(), vous pouvez spécifier une expression régulière à laquelle la chaîne d'entrée entière doit correspondre (c'est-à-dire comme si elle était entourée de caractères ^ a $) :

// seulement 9 chiffres
Expect::string()->pattern('\d{9}');

Assertions personnalisées : assert()

Vous pouvez ajouter toute autre restriction en utilisant assert(callable $fn).

$countIsEven = fn($v) => count($v) % 2 === 0;

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // le nombre doit être pair.

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERREUR : 3 n'est pas pair.

Ou

Expect::string()->assert('is_file'); // le fichier doit exister

Vous pouvez ajouter votre propre description pour chaque assertion. Elle fera partie du message d'erreur.

$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.

La méthode peut être appelée plusieurs fois pour ajouter plusieurs contraintes. Elle peut être combinée avec les appels à transform() et castTo().

Transformation : transform()

Les données validées avec succès peuvent être modifiées à l'aide d'une fonction personnalisée :

// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));

La méthode peut être appelée à plusieurs reprises pour ajouter des transformations multiples. Elle peut être combinée avec des appels à assert() et castTo(). Les opérations seront exécutées dans l'ordre dans lequel elles sont déclarées :

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'All characters must be lowercased')
	->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase

La méthode transform() peut transformer et valider la valeur simultanément. C'est souvent plus simple et moins redondant que d'enchaîner transform() et assert(). À cette fin, la fonction reçoit un objet Contexte avec une méthode addError(), qui peut être utilisée pour ajouter des informations sur les problèmes de validation :

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()

Les données validées avec succès peuvent être coulées :

Expect::scalar()->castTo('string');

En plus des types natifs de PHP, vous pouvez également utiliser des classes. Cela permet de distinguer s'il s'agit d'une classe simple sans constructeur ou d'une classe avec constructeur. Si la classe n'a pas de constructeur, une instance est créée et tous les éléments de la structure sont inscrits dans ses propriétés :

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

Si la classe a un constructeur, les éléments de la structure sont transmis au constructeur en tant que paramètres nommés :

class Info
{
	public function __construct(
		public bool $processRefund,
		public int $refundAmount,
	) {
	}
}

// creates $obj = new Info(processRefund: ..., refundAmount: ...)

La fonte combinée à un paramètre scalaire crée un objet et transmet la valeur comme seul paramètre au constructeur :

Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)

Normalisation : before()

Avant la validation proprement dite, les données peuvent être normalisées à l'aide de la méthode before(). À titre d'exemple, prenons un élément qui doit être un tableau de chaînes de caractères (ex. ['a', 'b', 'c']), mais qui reçoit des données sous la forme d'une chaîne de caractères a b c:

$explode = fn($v) => explode(' ', $v);

$schema = Expect::arrayOf('string')
	->before($explode);

$normalized = $processor->process($schema, 'a b c');
// OK, renvoie ['a', 'b', 'c']

Mapping to Objects : from()

Vous pouvez générer un schéma de structure à partir de la classe. Exemple :

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}

Les classes anonymes sont également prises en charge :

$schema = Expect::from(new class {
	public string $name;
	public ?string $password;
	public bool $admin = false;
});

Comme les informations obtenues à partir de la définition de la classe peuvent ne pas être suffisantes, vous pouvez ajouter un schéma personnalisé pour les éléments à l'aide du deuxième paramètre :

$schema = Expect::from(new Config, [
	'name' => Expect::string()->pattern('\w:.*'),
]);
version: 2.0