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
.
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}.
Le tableau de toutes les propriétés de la structure est renvoyé par la méthode getShape()
.
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
Vous pouvez créer une nouvelle structure en dérivant d'une autre à l'aide de extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Tableau
Un tableau dont les clés sont définies. Les mêmes règles que pour les structures s'appliquent.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Vous pouvez également définir un tableau indexé, appelé tuple :
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
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:.*'),
]);