Valider des Données
Avant que vous ne sauvegardiez des données vous voudrez
probablement vous assurer que les données sont correctes et cohérentes. Dans
CakePHP, nous avons deux étapes de validation:
Avant que les données de requête ne soient converties en entities, il est
possible d’appliquer des règles de validation concernant le type de données
et leur format.
Avant que les données ne soient sauvegardées, il est possible d’appliquer des
règles du domaine ou de l’application. Ces règles aident à garantir la
cohérence des données de votre application.
Valider les Données Avant de Construire les Entities
Vous pouvez valider les données lors de leur transformation en entities. Cette
validation vous permet de vérifier le type, la forme et la taille des données.
Par défaut, les données de la requête seront validées avant d’être converties en
entities.
Si une ou plusieurs règles de validation échouent, l’entity retournée contiendra
des erreurs. Les champs comportant des erreurs ne figureront pas dans l’entity
renvoyée:
$article = $articles->newEntity($this->request->getData());
if ($article->getErrors()) {
// La validation de l'entity a échoué.
}
Voici ce qui se passe quand vous construisez une entity et que la validation est
activée:
L’objet validator est créé.
Les fournisseurs de validation (providers) table
et default
sont
attachés.
La méthode de validation désignée est appelée. Par exemple
validationDefault()
.
L’événement Model.buildValidator
est déclenché.
Les données de la requête sont validées.
Les données de la requête sont castées en types correspondant aux types des
colonnes.
Les erreurs sont définies dans l’entity.
Les données valides sont définies dans l’entity, tandis que les champs qui
ont échoué à la validation sont laissés de côté.
Si vous voulez désactiver la validation lors de la conversion des données de la
requête, définissez l’option validate
à false:
$article = $articles->newEntity(
$this->request->getData(),
['validate' => false]
);
La méthode patchEntity()
fonctionne de façon identique:
$article = $articles->patchEntity($article, $newData, [
'validate' => false
]);
Créer un Ensemble de Validation par Défaut
Les règles de validation sont définies dans la classe Table par commodité.
Cela définit quelles données doivent être validées en conjonction avec l’endroit
où elles seront sauvegardées.
Pour créer un objet de validation par défaut dans votre table, créez la fonction
validationDefault()
:
use Cake\ORM\Table;
use Cake\Validation\Validator;
class ArticlesTable extends Table
{
public function validationDefault(Validator $validator): Validator
{
$validator
->requirePresence('title', 'create')
->notEmptyString('title');
$validator
->allowEmptyString('link')
->add('link', 'valid-url', ['rule' => 'url']);
...
return $validator;
}
}
Les méthodes et règles de validation disponibles proviennent de la classe
Validator
et sont documentées dans la section Créer les Validators.
Note
Les objets de validation sont principalement destinés à valider les données
provenant de l’utilisateur, par exemple les formulaires ou d’autres
données postées dans la requête.
Utiliser un Ensemble de Validation Différent
Outre la possibilité de désactiver la validation, vous pouvez choisir quel
ensemble de règles de validation que vous souhaitez appliquer:
$article = $articles->newEntity(
$this->request->getData(),
['validate' => 'update']
);
Ceci appellerait la méthode validationUpdate()
sur l’instance de table pour
construire les règles requises. Par défaut, c’est la méthode
validationDefault()
qui sera utilisée. Voici un exemple de validator pour
notre table articles
:
class ArticlesTable extends Table
{
public function validationUpdate($validator)
{
$validator
->notEmptyString('title', __('Vous devez indiquer un titre'))
->notEmptyString('body', __('Un contenu est nécessaire'));
return $validator;
}
}
Vous pouvez avoir autant d’ensembles de validation que vous le souhaitez.
Consultez le chapitre sur la validation
pour plus d’informations sur la construction des ensembles de règles de
validation.
Utiliser un Ensemble de Validation Différent pour les Associations
Les ensembles de validation peuvent également être définis pour chaque association.
Lorsque vous utilisez les méthodes newEntity()
ou patchEntity()
, vous
pouvez passer des options supplémentaires à chaque association qui doit être
convertie:
$data = [
'title' => 'Mon titre',
'body' => 'Le texte',
'user_id' => 1,
'user' => [
'username' => 'marc'
],
'comments' => [
['body' => 'Premier commentaire'],
['body' => 'Second commentaire'],
]
];
$article = $articles->patchEntity($article, $data, [
'validate' => 'update',
'associated' => [
'Users' => ['validate' => 'signup'],
'Comments' => ['validate' => 'custom']
]
]);
Combiner les Validators
Grâce à la manière dont les objets validator sont construits, vous pouvez
diviser leur process de construction en petites étapes réutilisables:
// UsersTable.php
public function validationDefault(Validator $validator): Validator
{
$validator->notEmptyString('username');
$validator->notEmptyString('password');
$validator->add('email', 'valid-email', ['rule' => 'email']);
...
return $validator;
}
public function validationHardened(Validator $validator): Validator
{
$validator = $this->validationDefault($validator);
$validator->add('password', 'length', ['rule' => ['lengthBetween', 8, 100]]);
return $validator;
}
Avec cette configuration, lors de l’utilisation de l’ensemble de validation
hardened
, vous aurez aussi les règles de l’ensemble default
.
Validation Providers
Les règles de validation peuvent utiliser des fonctions définies par n’importe
quel provider connu. Par défaut, CakePHP définit quelques providers:
Les méthodes sur la classe de table, ou ses behaviors, sont disponibles dans
le provider table
.
La classe du cœur Validation
est
configurée en tant que provider default
.
En créant une règle de validation, vous pouvez désigner le provider de cette
règle. Par exemple, si votre table a une méthode isValidRole
, vous pouvez
l’utiliser comme une règle de validation:
use Cake\ORM\Table;
use Cake\Validation\Validator;
class UsersTable extends Table
{
public function validationDefault(Validator $validator): Validator
{
$validator
->add('role', 'validRole', [
'rule' => 'isValidRole',
'message' => __('Vous devez fournir un rôle valide'),
'provider' => 'table',
]);
return $validator;
}
public function isValidRole($value, array $context): bool
{
return in_array($value, ['admin', 'editor', 'author'], true);
}
}
Vous pouvez également utiliser des closures en tant que règles de validation:
$validator->add('name', 'myRule', [
'rule' => function ($value, array $context) {
if ($value > 1) {
return true;
}
return 'Valeur incorrecte.';
}
]);
Les méthodes de validation peuvent renvoyer des messages d’erreur lorsqu’elles échouent.
C’est un moyen simple de créer des messages d’erreur dynamiques basés en
fonction de la valeur fournie.
Récupérer des Validators depuis les Tables
Une fois que vous avez créé quelques ensembles de validation dans votre classe
de table, vous pouvez récupérer l’objet résultant par son nom:
$defaultValidator = $usersTable->getValidator('default');
$hardenedValidator = $usersTable->getValidator('hardened');
Classe Validator par Défaut
Comme mentionné ci-dessus, par défaut les méthodes de validation reçoivent
une instance de Cake\Validation\Validator
. Si vous souhaitez utiliser
une instance d’un validator personnalisé, vous pouvez utiliser l’attribut
$_validatorClass
de la table:
// Dans votre classe de table
public function initialize(array $config): void
{
$this->_validatorClass = \FullyNamespaced\Custom\Validator::class;
}
Appliquer des Règles d’Application
Au-delà de la validation basique des données qui est lancée quand
les données de la requête sont converties en entities,
de nombreuses applications ont des validations plus complexes qui doivent être
appliquées après la validation basique.
Là où la validation s’assure que la forme ou la syntaxe de vos données
sont correctes, les règles s’attellent à comparer les données avec l’état
existant de votre application et/ou du réseau.
Ces types de règles sont souvent appelées “règles de domaine” ou
“règles d’application”. CakePHP utilise ce concept avec les “RulesCheckers”
qui sont appliquées avant que les entities ne soient sauvegardées. Voici
quelques exemples de règles de domaine:
S’assurer qu’un email est unique.
Transition d’états ou étapes de flux de travail, par exemple pour mettre à
jour le statut d’une facture.
Eviter la modification d’articles ayant fait l’objet d’une suppression
logique.
Appliquer des limites d’usage, que ce soit en nombre d’appels total ou en nombre
d’appels sur une période donnée.
Les règles de domaine sont vérifiées lors de l’appel aux méthodes save()
et
delete()
de Table.
Créer un Vérificateur de Règles
Les classes de vérificateur de règles (rules checkers) sont généralement définies par la
méthode buildRules()
dans votre classe de table. Les behaviors et les autres
souscripteurs d’events peuvent utiliser l’événement Model.buildRules
pour
ajouter des règles au vérificateur pour une classe Table donnée:
use Cake\ORM\RulesChecker;
// Dans une classe de table
public function buildRules(RulesChecker $rules): RulesChecker
{
// Ajoute une règle qui est appliquée pour les opérations de création et de mise à jour
$rules->add(function ($entity, $options) {
// Retourne un booléen pour indiquer si succès/échec
}, 'ruleName');
// Ajoute une règle pour la création.
$rules->addCreate(function ($entity, $options) {
// Retourne un booléen pour indiquer si succès/échec
}, 'ruleName');
// Ajoute une règle pour la mise à jour.
$rules->addUpdate(function ($entity, $options) {
// Retourne un booléen pour indiquer si succès/échec
}, 'ruleName');
// Ajoute une règle pour la suppression.
$rules->addDelete(function ($entity, $options) {
// Retourne un booléen pour indiquer si succès/échec
}, 'ruleName');
return $rules;
}
Vos fonctions de règles ont pour paramètres l’Entity à vérifier et un tableau
d’options. Le tableau d’options contiendra errorField
, message
et
repository
. L’option repository
contiendra la classe de table à
laquelle les règles sont attachées. Comme les règles acceptent n’importe quel
callable
, vous pouvez aussi utiliser des fonctions d’instance:
$rules->addCreate([$this, 'uniqueEmail'], 'uniqueEmail');
ou des classes callable:
$rules->addCreate(new IsUnique(['email']), 'uniqueEmail');
Lors de l’ajout de règles, vous pouvez définir en options le champ correspondant
à la règle et le message d’erreur:
$rules->add([$this, 'isValidState'], 'validState', [
'errorField' => 'status',
'message' => 'Cette facture ne peut pas être déplacée vers ce statut.'
]);
L’erreur sera visible lors de l’appel à la méthode getErrors()
dans
l’entity:
$entity->getErrors(); // Contient les messages d'erreur des règles de domaine
Créer des Règles d’Unicité de Champ
Les règles d’unicité étant couramment utilisées, CakePHP inclut une classe
simple qui vous permet de définir des ensembles de champs uniques:
use Cake\ORM\Rule\IsUnique;
// Un seul champ.
$rules->add($rules->isUnique(['email']));
// Une liste de champs
$rules->add($rules->isUnique(
['username', 'account_id'],
'Cette combinaison `username` & `account_id` est déjà utilisée.'
));
Quand vous définissez des règles sur des champs de clé étrangère, il est
important de se rappeler que seuls les champs listés sont utilisés dans la
règle. L’ensemble des règles d’unicité sera trouvé avec find('all')
. Cela
signifie que la règle ci-dessus ne sera pas déclenchée en définissant
$user->account->id
.
De nombreux moteurs de bases de données autorisent plusieurs valeurs NULL dans
les index UNIQUE. Pour simuler ce comportement, définissez l’option
allowMultipleNulls
à true
:
$rules->add($rules->isUnique(
['username', 'account_id'],
['allowMultipleNulls' => true]
));
Règles de Clés Etrangères
Bien que vous puissiez vous reposer sur les erreurs de la base de données pour
imposer des contraintes, le fait d’utiliser des règles vous permet de fournir une
expérience utilisateur plus sympathique. C’est pour cela que CakePHP inclut
une classe de règle ExistsIn
:
// Un champ unique.
$rules->add($rules->existsIn('article_id', 'Articles'));
// Plusieurs clés, utile pour des clés primaires composites.
$rules->add($rules->existsIn(['site_id', 'article_id'], 'Articles'));
Les champs dont vous demandez à vérifier l’existence dans la table
correspondante doivent faire partie de la clé primaire.
Vous pouvez forcer existsIn
à accepter qu’une partie de votre clé étrangère
composite soit null:
// Exemple: NodesTable contient une clé primaire composite (parent_id, site_id).
// Un "Node" peut faire référence à un Node parent mais ce n'est pas
// obligatoire. Dans ce dernier cas, parent_id est null.
// Nous autorisons la validation de cette règle même si les champs qui sont
// nullables, comme parent_id, sont null:
$rules->add($rules->existsIn(
['parent_id', 'site_id'], // Schema: parent_id NULL, site_id NOT NULL
'ParentNodes',
['allowNullableNulls' => true]
));
// Un Node doit cependant toujours avoir une référence à un Site.
$rules->add($rules->existsIn(['site_id'], 'Sites'));
Dans la majorité des bases de données SQL, les index UNIQUE
sur plusieurs
colonnes autorisent plusieurs valeurs null car NULL
n’est
pas égal à lui même. Même si CakePHP autorise par défaut plusieurs valeurs null,
vous pouvez inclure les nulls dans vos
vérifications d’unicité en utilisant allowMultipleNulls
:
// Une seule valeur null peut exister dans `parent_id` et `site_id`
$rules->add($rules->existsIn(
['parent_id', 'site_id'],
'ParentNodes',
['allowMultipleNulls' => false]
));
Règles sur le Nombre de Valeurs d’une Association
Si vous avez besoin de valider qu’une propriété ou une association contient un
certain nombre de valeurs, vous pouvez utiliser la règle validCount()
:
// Dans le fichier ArticlesTable.php
// Pas plus de 5 tags sur un article.
$rules->add($rules->validCount('tags', 5, '<=', 'Vous ne pouvez avoir que 5 tags'));
Quand vous définissez des règles qui basées sur un nombre de valeurs, le troisième
paramètre vous permet de définir l’opérateur de comparaison à utiliser. Les
opérateurs acceptés sont ==
, >=
, <=
, >
, <
, et !=
. Pour
vérifier que le décompte d’une propriété est entre certaines valeurs, utilisez
deux règles:
// Dans le fichier ArticlesTable.php
// Entre 3 et 5 tags
$rules->add($rules->validCount('tags', 3, '>=', 'Vous devez avoir au moins 3 tags'));
$rules->add($rules->validCount('tags', 5, '<=', 'Vous devez avoir au plus 5 tags'));
Notez que validCount
retourne false
si la propriété ne peut pas être comptée
ou n’existe pas:
// La sauvegarde échouera si tags est null
$rules->add($rules->validCount('tags', 0, '<=', 'Vous ne devez pas avoir de tags'));
Règles de Contraintes d’Association
La règle LinkConstraint
vous permet d’émuler des contraintes SQL dans les
bases de données qui ne les supportent pas, ou quand vous voulez fournir des
messages d’erreur plus sympathiques quand la contrainte échoue. Cette règle vous
permet de vérifier si une association a ou non des enregistrements liés en
fonction du mode utilisé:
// S'assure que chaque commentaire est lié à un Article lors de sa mise à jour.
$rules->addUpdate($rules->isLinkedTo(
'Articles',
'article',
'Spécifiez un article'
));
// S'assure qu'un article n'a pas de commentaire au moment de sa suppression.
$rules->addDelete($rules->isNotLinkedTo(
'Comments',
'comments',
'Impossible de supprimer un article qui contient des commentaires.'
));
Nouveau dans la version 4.0.0.
Utiliser les Méthodes d’Entity en tant que Règles
Vous pouvez utiliser les méthodes d’une entity en tant que règles de domaine:
$rules->add(function ($entity, $options) {
return $entity->isOkLooking();
}, 'ruleName');
Utiliser des Règles Conditionnelles
Vous pouvez appliquer des règles conditionnelles en fonction des données de
l’entity:
$rules->add(function ($entity, $options) use($rules) {
if ($entity->role == 'admin') {
$rule = $rules->existsIn('user_id', 'Admins');
return $rule($entity, $options);
}
if ($entity->role == 'user') {
$rule = $rules->existsIn('user_id', 'Users');
return $rule($entity, $options);
}
return false;
}, 'userExists');
Messages d’Erreur Dynamiques/Conditionnels
Les règles, qu’elles soient des callables personnalisés
ou des objets Rule, peuvent soit retourner
un booléen indiquant si elles ont réussi, soit retourner une chaîne qui
signifie que la règle a échoué et que la chaîne doit être utilisée comme message
d’erreur.
Les messages d’erreur possibles définis par l’option message
seront écrasés
par ceux retournés par la règle:
$rules->add(
function ($entity, $options) {
if (!$entity->length) {
return false;
}
if ($entity->length < 10) {
return "Message d'erreur quand la valeur est inférieure à 10";
}
if ($entity->length > 20) {
return "Message d'erreur quand la valeur est supérieure à 20";
}
return true;
},
'ruleName',
[
'errorField' => 'length',
'message' => "Message d'erreur générique utilisé quand la règle retourne `false`."
]
);
Note
Notez que pour que le message retourné puisse être utilisé, vous devez
aussi définir l’option errorField
, sinon la règle va se contenter
d’échouer silencieusement, c’est-à-dire sans insérer le message d’erreur
dans l’entity !
Créer des Règles Personnalisées Réutilisables
Vous pouvez vouloir réutiliser des règles de domaine personnalisées. Vous pouvez
le faire en créant votre propre règle invokable:
use App\ORM\Rule\IsUniqueWithNulls;
// ...
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add(new IsUniqueWithNulls(['parent_id', 'instance_id', 'name']), 'uniqueNamePerParent', [
'errorField' => 'name',
'message' => 'Doit être unique pour chaque parent.'
]);
return $rules;
}
Regardez les règles du cœur pour avoir des exemples sur la façon de créer de
telles règles.
Créer des Objets de Règles Personnalisées
Si votre application a des règles qui sont souvent réutilisées, il peut être
utile de packager ces règles dans des classes réutilisables:
// Dans src/Model/Rule/CustomRule.php
namespace App\Model\Rule;
use Cake\Datasource\EntityInterface;
class CustomRule
{
public function __invoke(EntityInterface $entity, array $options)
{
// Faire le boulot ici
return false;
}
}
// Ajouter la règle personnalisée
use App\Model\Rule\CustomRule;
$rules->add(new CustomRule(/* ... */), 'ruleName');
En ajoutant des classes de règles personnalisées, vous pouvez garder votre code
DRY et tester vos règles de domaine isolément.
Désactiver les Règles
Quand vous sauvegardez une entity, vous pouvez désactiver les règles si c’est
nécessaire:
$articles->save($article, ['checkRules' => false]);
Validation vs. Règles d’Application
L’ORM de CakePHP est unique dans le sens où il utilise une approche à deux
couches pour la validation.
La première couche est la validation. Les règles de validation ont pour objet
d’opérer sans état (stateless). Elles servent à s’assurer que la forme, les
types de données et le format des données sont corrects.
La seconde couche est celle des règles d’application. Les règles d’application
sont plus appropriés pour vérifier l’état des propriétés de vos entities. Par exemple,
les règles de validation peuvent permettre de s’assurer qu’une adresse email est
valide, tandis qu’une règle d’application permet de s’assurer que l’adresse
email est unique.
Comme vous avez pu le voir, la première couche est réalisée par l’objet
Validator
lors de l’appel à newEntity()
ou patchEntity()
:
$validatedEntity = $articlesTable->newEntity(
$donneesDouteuses,
['validate' => 'maRegle']
);
$validatedEntity = $articlesTable->patchEntity(
$entity,
$donneesDouteuses,
['validate' => 'maRegle']
);
Dans l’exemple ci-dessus, nous allons utiliser un validateur “maRegle”, qui est
défini en utilisant la méthode validationMaRegle()
:
public function validationMaRegle($validator)
{
$validator->add(
// ...
);
return $validator;
}
La validation présuppose que les arguments passés sont des chaînes de caractères
ou des tableaux, puisque c’est ce qui est passé dans n’importe quelle requête:
// Dans src/Model/Table/UsersTable.php
public function validatePasswords($validator)
{
$validator->add('confirm_password', 'no-misspelling', [
'rule' => ['compareWith', 'password'],
'message' => 'Les mots de passe ne sont pas identiques',
]);
// ...
return $validator;
}
La validation n’est pas déclenchée lorsque vous définissez directement une
propriété sur vos entities:
$userEntity->email = 'pas un email!!';
$usersTable->save($userEntity);
Dans l’exemple ci-dessus, l’entity sera sauvegardée car la validation n’est
déclenchée que par les méthodes newEntity()
et patchEntity()
. Le second
niveau de validation est conçu pour gérer cette situation.
Les règles d’application, comme expliqué précédement, seront vérifiées à chaque
appel de save()
ou delete()
:
// Dans src/Model/Table/UsersTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->isUnique(['email']));
return $rules;
}
// Autre part dans le code de votre application
$userEntity->email = '[email protected]';
$usersTable->save($userEntity); // Retourne false
La validation est conçue pour les données provenant directement des
utilisateurs, tandis que les règles d’application sont conçues spécifiquement
pour les transitions de données générées à l’intérieur de l’application:
// Dans src/Model/Table/CommandesTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
$check = function($commande) {
if ($commande->livraison !== 'gratuit') {
return true;
}
return $commande->prix >= 100;
};
$rules->add($check, [
'errorField' => 'livraison',
'message' => 'Pas de frais de port gratuits pour une commande de moins de 100!'
]);
return $rules;
}
// Autre part dans le code de l'application
$commande->prix = 50;
$commande->livraison = 'gratuit';
$commandesTable->save($commande); // Retourne false
Utiliser la Validation en tant que Règle d’Application
Dans certaines situations, vous voudrez peut-être lancer les mêmes routines
à la fois pour des données générées par un utilisateur et pour l’intérieur de
votre application. Cela peut se produire lorsque vous exécutez un script CLI
qui définit des propriétés directement dans des entities:
// Dans src/Model/Table/UsersTable.php
public function validationDefault(Validator $validator): Validator
{
$validator->add('email', 'email_valide', [
'rule' => 'email',
'message' => 'Email invalide'
]);
// ...
return $validator;
}
public function buildRules(RulesChecker $rules): RulesChecker
{
// Ajoute des règles de validation
$rules->add(function($entity) {
$data = $entity->extract($this->getSchema()->columns(), true);
if (!$entity->isNew() && !empty($data)) {
$data += $entity->extract((array)$this->getPrimaryKey());
}
$validator = $this->getValidator('default');
$errors = $validator->validate($data, $entity->isNew());
$entity->setErrors($errors);
return empty($errors);
});
// ...
return $rules;
}
Lors de l’exécution du code suivant, la sauvegarde échouera grâce à la nouvelle
règle d’application qui a été ajoutée:
$userEntity->email = 'Pas un email!!!';
$usersTable->save($userEntity);
$userEntity->getErrors('email'); // Email invalide
Le même résultat est attendu lors de l’utilisation de newEntity()
ou
patchEntity()
:
$userEntity = $usersTable->newEntity(['email' => 'Pas un email!!']);
$userEntity->getErrors('email'); // Email invalide