Schéma net
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.
La documentation est disponible sur le site Internet.
Installation:
composer require nette/schema
Il nécessite PHP version 8.1 et prend en charge PHP jusqu'à 8.4.
Aimez-vous Nette Schema? Attendez-vous avec impatience les nouvelles fonctionnalités ?
Merci!
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. Il peut s'agir par exemple de 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 NetteSchemaProcessor, qui traite l'entrée et renvoie des données normalisées ou lève une exception NetteSchemaValidationException 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 NetteSchemaMessage.
Et maintenant, créons un schéma. La classe NetteSchemaExpect est utilisée pour le définir, nous définissons en fait les attentes quant à ce à quoi devraient ressembler les données. 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 le 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, it passes
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, it passes
// $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
. Nous devrions autoriser explicitement null
via Expect::bool()->nullable()
.
Un élément peut être rendu obligatoire en utilisant Expect::bool()->required()
. Nous modifions la valeur par défaut en false
en utilisant Expect::bool()->default(false)
ou brièvement en utilisant Expect::bool(false)
.
Et si nous voulions accepter 1
et 0
en plus des booléens ? Ensuite, nous listons les valeurs autorisées, que nous normaliserons également en booléennes :
$ 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 éléments individuels de la structure. Nous allons maintenant montrer quels autres éléments peuvent être utilisés pour définir un schéma.
Tous les types de données PHP standard peuvent être répertorié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 puis tous les types pris en charge par les validateurs via Expect::type('scalar')
ou en abrégé Expect::scalar()
. Les noms de classe ou d'interface sont également acceptés, par exemple Expect::type('AddressEntity')
.
Vous pouvez également utiliser la notation 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é par ordre croissant de touches numériques à partir de zéro, c'est-à-dire un tableau non associatif).
Le tableau est une structure trop générale, il est plus utile de préciser exactement quels éléments il peut contenir. Par exemple, un tableau dont les éléments ne peuvent être que des chaînes :
$ schema = Expect:: arrayOf ( ' string ' );
$ processor -> process ( $ schema , [ ' hello ' , ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' => ' hello ' , ' b ' => ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' key ' => 123 ]); // ERROR: 123 is not a string
Le deuxième paramètre peut être utilisé pour spécifier des clés (depuis la version 1.2) :
$ schema = Expect:: arrayOf ( ' string ' , ' int ' );
$ processor -> process ( $ schema , [ ' hello ' , ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' => ' hello ' ]); // ERROR: 'a' is not int
La liste est un tableau indexé :
$ schema = Expect:: listOf ( ' string ' );
$ processor -> process ( $ schema , [ ' a ' , ' b ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , 123 ]); // ERROR: 123 is not a string
$ processor -> process ( $ schema , [ ' key ' => ' a ' ]); // ERROR: is not a list
$ processor -> process ( $ schema , [ 1 => ' a ' , 0 => ' b ' ]); // ERROR: is not a list
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 une valeur par défaut et appelez mergeDefaults()
, elle sera fusionnée avec les données transmises.
anyOf()
est un ensemble de valeurs ou de schémas que peut être une valeur. Voici comment écrire un tableau d'éléments qui peuvent être 'a'
, true
ou null
:
$ schema = Expect:: listOf (
Expect:: anyOf ( ' a ' , true , null ),
);
$ processor -> process ( $ schema , [ ' a ' , true , null , ' a ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , false ]); // ERROR: false does not belong there
Les éléments d'é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 ]); // ERROR
La méthode anyOf()
accepte les variantes comme paramètres individuels, et non comme tableau. Pour lui transmettre un tableau de valeurs, utilisez l'opérateur de déballage anyOf(...$variants)
.
La valeur par défaut est null
. Utilisez la méthode firstIsDefault()
pour définir le premier élément par défaut :
// default is 'hello'
Expect:: anyOf (Expect:: string ( ' hello ' ), true , null )-> firstIsDefault ();
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 les tableaux et les objets et renvoient les objets stdClass
(sauf si vous le modifiez 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 (), // the default value is null
]);
$ processor -> process ( $ schema , [ ' optional ' => '' ]);
// ERROR: option 'required' is missing
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => 'foo', 'optional' => null}
Si vous ne souhaitez pas afficher les propriétés avec 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, returns {'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). 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, returns {'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 paramètre, nous préciserons le schéma de 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 ]); // ERROR
Vous pouvez déprécier la propriété à l’aide de la méthode deprecated([string $message])
. Les avis de dépréciation sont renvoyés par $processor->getWarnings()
:
$ schema = Expect:: structure ([
' old ' => Expect:: int ()-> deprecated ( ' The item %path% is deprecated ' ),
]);
$ processor -> process ( $ schema , [ ' old ' => 1 ]); // OK
$ processor -> getWarnings (); // ["The item 'old' is deprecated"]
Utilisez min()
et max()
pour limiter le nombre d'éléments pour les tableaux :
// array, at least 10 items, maximum 20 items
Expect:: array ()-> min ( 10 )-> max ( 20 );
Pour les chaînes, limitez leur longueur :
// string, at least 10 characters long, maximum 20 characters
Expect:: string ()-> min ( 10 )-> max ( 20 );
Pour les nombres, limitez leur valeur :
// integer, between 10 and 20 inclusive
Expect:: int ()-> min ( 10 )-> max ( 20 );
Bien entendu, il est possible de mentionner uniquement min()
, ou uniquement max()
:
// string, maximum 20 characters
Expect:: string ()-> max ( 20 );
En utilisant pattern()
, vous pouvez spécifier une expression régulière à laquelle toute la chaîne d'entrée doit correspondre (c'est-à-dire comme si elle était entourée de caractères ^
a $
) :
// just 9 digits
Expect:: string ()-> pattern ( ' d{9} ' );
Vous pouvez ajouter d'autres restrictions en utilisant assert(callable $fn)
.
$ countIsEven = fn ( $ v ) => count ( $ v ) % 2 === 0 ;
$ schema = Expect:: arrayOf ( ' string ' )
-> assert ( $ countIsEven ); // the count must be even
$ processor -> process ( $ schema , [ ' a ' , ' b ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , ' b ' , ' c ' ]); // ERROR: 3 is not even
Ou
Expect:: string ()-> assert ( ' is_file ' ); // the file must exist
Vous pouvez ajouter votre propre description pour chaque assertion. Cela 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 reprises pour ajouter plusieurs contraintes. Il peut être mélangé à des appels à transform()
et castTo()
.
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 plusieurs transformations. Il peut être mélangé à 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 à la fois transformer et valider la valeur simultanément. C'est souvent plus simple et moins redondant que d'enchaîner transform()
et assert()
. A cet effet, la fonction reçoit un objet NetteSchemaContext 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 );
});
Les données validées avec succès peuvent être diffusées :
Expect:: scalar ()-> castTo ( ' string ' );
En plus des types PHP natifs, vous pouvez également diffuser vers des classes. Il distingue 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 de celle-ci est créée et tous les éléments de la structure sont écrits 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 passés en paramètres nommés au constructeur :
class Info
{
public function __construct (
public bool $ processRefund ,
public int $ refundAmount ,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
Le casting combiné à 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(...)
Avant la validation elle-même, les données peuvent être normalisées à l'aide de la méthode before()
. A titre d'exemple, prenons un élément qui doit être un tableau de chaînes (par exemple ['a', 'b', 'c']
), mais qui reçoit une entrée sous la forme d'une chaîne abc
:
$ explode = fn ( $ v ) => explode ( ' ' , $ v );
$ schema = Expect:: arrayOf ( ' string ' )
-> before ( $ explode );
$ normalized = $ processor -> process ( $ schema , ' a b c ' );
// OK, returns ['a', 'b', 'c']
Vous pouvez générer un schéma de structure à partir de la classe. Exemple:
class Config
{
public string $ name ;
public ? string $ 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 ;
});
Étant donné que les informations obtenues à partir de la définition de classe peuvent ne pas être suffisantes, vous pouvez ajouter un schéma personnalisé pour les éléments avec le deuxième paramètre :
$ schema = Expect:: from ( new Config , [
' name ' => Expect:: string ()-> pattern ( ' w:.* ' ),
]);