Tôt ou tard dans votre carrière de programmation, vous serez confronté au dilemme de la validation et du traitement des exceptions. Ce fut le cas avec moi et mon équipe aussi. Il y a quelques années, nous avons atteint un point où nous avons dû prendre des mesures architecturales pour prendre en compte tous les cas exceptionnels que notre projet logiciel assez volumineux devait gérer. Vous trouverez ci-dessous une liste de pratiques que nous avons appris à valoriser et à appliquer en matière de validation et de gestion des exceptions..
Lorsque nous avons commencé à discuter de notre problème, une chose est apparue très rapidement. Qu'est-ce que la validation et quelle est la gestion des exceptions? Par exemple, dans un formulaire d’inscription d’utilisateur, nous avons quelques règles pour le mot de passe (il doit contenir des chiffres et des lettres). Si l'utilisateur n'entre que des lettres, s'agit-il d'un problème de validation ou d'une exception. L’interface utilisateur doit-elle valider ou simplement la transmettre au serveur principal et attraper toutes les exceptions qui pourraient lui être renvoyées??
Nous sommes arrivés à la conclusion commune que la validation fait référence à des règles définies par le système et vérifiées par rapport aux données fournies par l'utilisateur. Une validation ne doit pas se préoccuper de la manière dont fonctionne la logique métier ou du système. Par exemple, notre système d'exploitation peut s'attendre, sans contestation, à un mot de passe composé de lettres en clair. Cependant, nous voulons imposer une combinaison de lettres et de chiffres. C’est un cas de validation, une règle que nous voulons imposer.
D’autre part, les exceptions sont les cas où notre système peut fonctionner de manière imprévisible, à tort ou pas du tout si certaines données spécifiques sont fournies dans un format incorrect. Par exemple, dans l'exemple ci-dessus, si le nom d'utilisateur existe déjà sur le système, il s'agit d'une exception. Notre logique métier doit pouvoir émettre l'exception appropriée, l'interception de l'interface utilisateur et la gérer de sorte que l'utilisateur puisse voir un beau message..
Maintenant que nous avons clairement défini nos objectifs, voyons quelques exemples basés sur la même idée de formulaire d'inscription d'utilisateur.
Pour la plupart des navigateurs actuels, JavaScript est une seconde nature. Il n’existe pratiquement aucune page Web sans un certain degré de JavaScript. Une bonne pratique consiste à valider quelques éléments de base en JavaScript..
Disons que nous avons un simple formulaire d’enregistrement utilisateur dans index.php
, comme décrit ci-dessous.
Enregistrement de l'utilisateur Enregistrer un nouveau compte
Cela produira quelque chose de similaire à l'image ci-dessous:
Chacun de ces formulaires doit valider que le texte saisi dans les deux champs de mot de passe est égal. Évidemment, cela permet d'éviter que l'utilisateur ne se trompe lorsqu'il tape son mot de passe. Avec JavaScript, faire la validation est assez simple.
Nous devons d’abord mettre à jour un peu de notre code HTML.
Nous avons ajouté des noms aux champs de saisie du mot de passe afin de pouvoir les identifier. Ensuite, nous avons spécifié que lors de l'envoi du formulaire, le résultat d'une fonction appelée validatePasswords ()
. Cette fonction est le JavaScript que nous allons écrire. Des scripts simples comme celui-ci peuvent être conservés dans le fichier HTML, d'autres, plus sophistiqués, doivent être insérés dans leurs propres fichiers JavaScript..
La seule chose que nous faisons ici est de comparer les valeurs des deux champs d’entrée nommés "mot de passe
" et "confirmer
". Nous pouvons référencer le formulaire par le paramètre que nous avons envoyé lors de l'appel de la fonction. Nous avons utilisé"ce
"dans le formulaire à soumettre
attribut, donc le formulaire lui-même est envoyé à la fonction.
Quand les valeurs sont les mêmes, vrai
sera retourné et le formulaire sera soumis, sinon un message d'alerte sera affiché pour indiquer à l'utilisateur que les mots de passe ne correspondent pas.
Bien que nous puissions utiliser JavaScript pour valider la plupart de nos entrées, il existe des cas où nous voulons aller plus facilement. Un certain degré de validation des entrées est disponible en HTML5, et la plupart des navigateurs sont heureux de les appliquer. L'utilisation de la validation HTML5 est plus simple dans certains cas, même si elle offre moins de flexibilité..
Enregistrement de l'utilisateur Enregistrer un nouveau compte
Pour illustrer plusieurs cas de validation, nous avons un peu élargi notre formulaire. Nous avons ajouté une adresse e-mail et un site Web également. Les validations HTML ont été définies sur trois champs.
Nom d'utilisateur
est simplement nécessaire. Il sera validé avec toute chaîne de plus de zéro caractère. email
"et quand on spécifie le"Champs obligatoires
"attribut, les navigateurs appliqueront une validation au champ. url
". Nous avons également spécifié un"modèle
"attribut où vous pouvez écrire vos expressions régulières qui valident les champs obligatoires. Pour informer l'utilisateur de l'état des champs, nous avons également utilisé un peu de CSS pour colorer les bordures des entrées en rouge ou en vert, en fonction de l'état de la validation requise..
Le problème avec les validations HTML est que différents navigateurs se comportent différemment lorsque vous essayez de soumettre le formulaire. Certains navigateurs ne feront qu'appliquer le code CSS pour informer les utilisateurs, d'autres empêcheront la soumission du formulaire. Je vous recommande de tester minutieusement vos validations HTML dans différents navigateurs et, si nécessaire, de prévoir également un système de secours JavaScript pour les navigateurs qui ne sont pas assez intelligents..
À ce jour, de nombreuses personnes connaissent la proposition d'architecture propre de Robert C. Martin, dans laquelle le cadre MVC est uniquement destiné à la présentation et non à la logique métier..
Pour l'essentiel, votre logique métier doit résider dans un endroit séparé, bien isolé, organisé pour refléter l'architecture de votre application, tandis que les vues et les contrôleurs de la structure doivent contrôler la livraison du contenu à l'utilisateur et que les modèles peuvent être supprimés complètement ou, si nécessaire. , utilisé uniquement pour effectuer des opérations liées à la livraison. Une telle opération est la validation. La plupart des frameworks ont d'excellentes fonctionnalités de validation. Ce serait dommage de ne pas mettre vos modèles au travail et de faire une petite validation là-bas..
Nous n'allons pas installer plusieurs frameworks Web MVC pour montrer comment valider nos formulaires précédents, mais voici deux solutions approximatives dans Laravel et CakePHP.
Laravel est conçu pour que vous ayez plus d'accès à la validation dans le contrôleur où vous avez également un accès direct aux entrées de l'utilisateur. Le type de validateur intégré préfère être utilisé ici. Cependant, il est suggéré sur Internet que la validation dans les modèles reste une bonne chose à faire à Laravel. Un exemple complet et une solution de Jeffrey Way peuvent être trouvés sur son dépôt Github.
Si vous préférez écrire votre propre solution, vous pouvez faire quelque chose de similaire au modèle ci-dessous..
La classe UserACL étend Eloquent private $ rules = array ('userName' => 'required | alpha | min: 5', 'password' => 'required | min: 6', 'confirm' => 'required | min: 6 ',' email '=>' required | email ',' website '=>' url '); private $ errors; fonction publique validate ($ data) $ validator = Validator :: make ($ data, $ this-> rules); if ($ validator-> fail ()) $ this-> errors = $ validator-> errors; retourne faux; return true; public function errors () return $ this-> errors;
Vous pouvez l'utiliser depuis votre contrôleur en créant simplement le UserACL
objet et appel validez dessus. Vous aurez probablement le "registre
"méthode également sur ce modèle, et la registre
va simplement déléguer les données déjà validées à votre logique métier.
CakePHP favorise également la validation dans les modèles. Il dispose d'une fonctionnalité de validation étendue au niveau du modèle. Voici à quoi ressemblerait une validation de notre formulaire dans CakePHP.
La classe UserACL étend AppModel public $ validate = ['userName' => ['règle' => ['minLength', 5], 'required' => true, 'allowEmpty' => false, 'on' => 'créer ',' message '=>' Le nom d'utilisateur doit comporter au moins 5 caractères. ' ], 'password' => ['rule' => ['equalsTo', 'confirm'], 'message' => 'Les deux mots de passe ne correspondent pas. Veuillez les entrer à nouveau. ' ]]; fonction publique est égale à ($ vérifiéField, $ autreField = null) $ valeur = $ ceci-> getFieldValue ($ vérifiéField); return $ value === $ this-> data [$ this-> name] [$ otherField]; fonction privée getFieldValue ($ fieldName) return array_values ($ otherField) [0];
Nous n'avons que partiellement illustré les règles. Il suffit de souligner le pouvoir de validation dans le modèle. CakePHP est particulièrement doué pour cela. Il possède un grand nombre de fonctions de validation intégrées telles que "Longueur minimale
"dans l'exemple et les différentes manières de fournir un retour d'information à l'utilisateur. Encore plus, des concepts tels que"Champs obligatoires
" ou "autoriser le vide
"Ce ne sont pas vraiment des règles de validation. Cake les examinera lors de la génération de votre vue et mettra également des validations HTML sur les champs marqués avec ces paramètres. Cependant, les règles sont géniales et peuvent facilement être étendues simplement en créant des méthodes sur la classe de modèle. comparer les deux champs de mot de passe. Enfin, vous pouvez toujours spécifier le message que vous souhaitez envoyer aux vues en cas d'échec de la validation. Plus d'informations sur la validation de CakePHP dans le livre de recettes.
La validation en général au niveau du modèle présente des avantages. Chaque infrastructure fournit un accès facile aux champs de saisie et crée le mécanisme permettant d'informer l'utilisateur en cas d'échec de la validation. Pas besoin de déclarations try-catch ou de toute autre étape sophistiquée. La validation côté serveur assure également que les données sont validées, quoi qu'il arrive. L'utilisateur ne peut plus duper notre logiciel comme avec HTML ou JavaScript. Bien entendu, chaque validation côté serveur entraîne le coût d'un aller-retour réseau et de la puissance de calcul du côté du fournisseur au lieu de celui du client..
La dernière étape de la vérification des données avant de les envoyer au système se situe au niveau de notre logique métier. Les informations qui atteignent cette partie du système doivent être suffisamment désinfectées pour pouvoir être utilisées. La logique applicative ne doit rechercher que les cas critiques. Par exemple, ajouter un utilisateur qui existe déjà est un cas où nous lançons une exception. Vérifier que l'utilisateur a au moins cinq caractères ne devrait pas se produire à ce niveau. Nous pouvons sans risque supposer que de telles limitations ont été appliquées à des niveaux plus élevés.
D'autre part, la comparaison des deux mots de passe est un sujet de discussion. Par exemple, si nous ne faisons que chiffrer et enregistrer le mot de passe auprès de l'utilisateur dans une base de données, nous pourrions supprimer la vérification et supposer que les couches précédentes s'assurent que les mots de passe sont identiques. Toutefois, si nous créons un utilisateur réel sur le système d'exploitation à l'aide d'une API ou d'un outil CLI nécessitant un nom d'utilisateur, un mot de passe et une confirmation du mot de passe, nous voudrons peut-être également saisir la deuxième entrée et l'envoyer à un outil CLI. Laissez-le re-valider si les mots de passe correspondent et soyez prêt à lever une exception s'ils ne le font pas. De cette manière, nous avons modélisé notre logique métier en fonction du comportement du système d'exploitation réel..
Lancer des exceptions depuis PHP est très facile. Créons notre classe de contrôle d'accès utilisateur et montrons comment implémenter une fonctionnalité d'addition d'utilisateur.
La classe UserControlTest étend PHPUnit_Framework_TestCase function testBehavior () $ this-> assertTrue (true);
J'aime toujours commencer par quelque chose de simple qui me fait avancer. Créer un test stupide est un excellent moyen de le faire. Cela me force également à réfléchir à ce que je veux mettre en œuvre. Un test nommé UserControlTest
signifie que je pensais avoir besoin d'un UserControl
classe pour implémenter ma méthode.
require_once __DIR__. '/… /UserControl.php'; class UserControlTest étend PHPUnit_Framework_TestCase / ** * Exception @ExpectedException * @expectedExceptionMessage L'utilisateur ne peut être vide * / function testEmptyUsernameWillThrowException () $ userControl = new UserControl (); $ userControl-> add (");
Le prochain test à écrire est un cas dégénératif. Nous ne testerons pas une longueur d'utilisateur spécifique, mais nous voulons nous assurer que nous ne voulons pas ajouter un utilisateur vide. Il est parfois facile de perdre le contenu d'une variable d'une vue à une autre, sur toutes les couches de notre application. Ce code va évidemment échouer, car nous n'avons pas encore de classe.
Avertissement PHP: require_once ([long-path-here] / Test /… /UserControl.php): échec de l'ouverture du flux: aucun fichier ou répertoire de ce type dans [long-path-here] /Test/UserControlTest.php en ligne 2
Créons la classe et exécutons nos tests. Maintenant nous avons un autre problème.
Erreur irrécupérable PHP: Appel de la méthode non définie UserControl :: add ()
Mais nous pouvons y remédier aussi en quelques secondes.
classe UserControl fonction publique add ($ username)
Maintenant, nous pouvons avoir un échec de test en nous racontant toute l'histoire de notre code.
1) UserControlTest :: testEmptyUsernameWillThrowException Échec lors de l'affirmation de l'exception de type "Exception"..
Enfin, nous pouvons faire du codage réel.
fonction publique add ($ username) if (! $ username) lancer une nouvelle exception ();
Cela fait passer l'attente de l'exception, mais sans spécifier de message, le test échouera.
1) UserControlTest :: testEmptyUsernameWillThrowException Échec lors de l'affirmation du message d'exception "contient" L'utilisateur ne peut pas être vide ".
Il est temps d'écrire le message de l'exception
fonction publique add ($ username) if (! $ username) lancer une nouvelle exception ('L'utilisateur ne peut pas être vide!');
Maintenant, cela rend notre test réussi. Comme vous pouvez le constater, PHPUnit vérifie que le message d'exception attendu est contenu dans l'exception réellement levée. Ceci est utile car il nous permet de construire dynamiquement des messages et de ne vérifier que la partie stable. Un exemple courant est lorsque vous générez une erreur avec un texte de base et que vous spécifiez à la fin le motif de cette exception. Les raisons sont généralement fournies par des bibliothèques tierces ou des applications..
/ ** * @expectedException Exception * @expectedExceptionMessage Impossible d'ajouter l'utilisateur George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> avec ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('L'utilisateur existe déjà sur le système.'); $ userControl = new UserControl ($ command); $ userControl-> add ('George');
Les erreurs de lancement sur les utilisateurs en double nous permettront d’explorer plus avant cette construction de messages. Le test ci-dessus crée un modèle qui simulera une commande système, il échouera et sur demande, il retournera un beau message d'échec. Nous allons injecter cette commande au UserControl
classe à usage interne.
class UserControl private $ systemCommand; fonction publique __construct (SystemCommand $ systemCommand = null) $ this-> systemCommand = $ systemCommand? : new SystemCommand (); fonction publique add ($ username) if (! $ username) lancer une nouvelle exception ('L'utilisateur ne peut pas être vide!'); Classe SystemCommand
Injectant le a SystemCommand
exemple était assez facile. Nous avons également créé un SystemCommand
classe dans notre test juste pour éviter les problèmes de syntaxe. Nous ne l'appliquerons pas. Sa portée dépasse le sujet de ce tutoriel. Cependant, nous avons un autre message d'échec de test.
1) UserControlTest :: testWillNotAddAnAlreadyExistingUser Échec de l'assertion de cette exception de type "Exception"..
Oui. Nous ne lançons aucune exception. La logique pour appeler la commande système et essayer d'ajouter l'utilisateur est manquante.
fonction publique add ($ username) if (! $ username) lancer une nouvelle exception ('L'utilisateur ne peut pas être vide!'); if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) lancer une nouvelle exception (sprintf ('impossible d'ajouter l'utilisateur% s. Reason:% s', $ username, $ this-> systemCommand-> getFailureMessage ()));
Maintenant, ces modifications à la ajouter()
méthode peut faire l'affaire. Nous essayons d'exécuter notre commande sur le système, quoi qu'il arrive, et si le système dit qu'il ne peut pas ajouter l'utilisateur pour une raison quelconque, nous lançons une exception. Le message de cette exception sera partiellement codé en dur, avec le nom de l'utilisateur en pièce jointe, puis le motif de la commande système concaténé à la fin. Comme vous pouvez le constater, ce code rend notre test réussi.
Lancer des exceptions avec des messages différents suffit dans la plupart des cas. Toutefois, lorsque vous avez un système plus complexe, vous devez également intercepter ces exceptions et prendre des mesures différentes en fonction de celles-ci. Analyser le message d'une exception et ne prendre que des mesures à cet égard peut entraîner des problèmes gênants. Premièrement, les chaînes font partie de l'interface utilisateur, de la présentation, et elles ont un caractère instable. Baser la logique sur des chaînes en constante évolution conduira au cauchemar de la gestion des dépendances. Deuxièmement, appeler un getMessage ()
méthode sur l'exception capturée à chaque fois est aussi un étrange moyen de décider quoi faire ensuite.
Compte tenu de tout cela, la création de nos propres exceptions est la prochaine étape logique à franchir..
/ ** * @expectedException ExceptionCannotAddUser * @expectedExceptionMessage Impossible d'ajouter l'utilisateur George * / function testWillNotAddAnAlreadyExistingUser () $ command = \ Mockery :: mock ('SystemCommand'); $ command-> shouldReceive ('execute') -> once () -> avec ('adduser George') -> andReturn (false); $ command-> shouldReceive ('getFailureMessage') -> once () -> andReturn ('L'utilisateur existe déjà sur le système.'); $ userControl = new UserControl ($ command); $ userControl-> add ('George');
Nous avons modifié notre test pour nous attendre à notre propre exception personnalisée, ExceptionCannotAddUser
. Le reste du test est inchangé.
class ExceptionCannotAddUser extend Exception fonction publique __construct ($ nom_utilisateur, $ raison) $ message = sprintf ('Impossible d'ajouter l'utilisateur% s. Reason:% s', $ nom_utilisateur, $ raison); parent :: __ construct ($ message, 13, null);
La classe qui implémente notre exception personnalisée est comme n'importe quelle autre classe, mais elle doit être étendue Exception
. L'utilisation d'exceptions personnalisées constitue également un excellent endroit pour effectuer toutes les manipulations de chaînes liées à la présentation. En déplaçant la concaténation ici, nous avons également supprimé la présentation de la logique métier et respecté le principe de responsabilité unique..
fonction publique add ($ username) if (! $ username) lancer une nouvelle exception ('L'utilisateur ne peut pas être vide!'); if (! $ this-> systemCommand-> execute (sprintf ('adduser% s', $ username))) lance new ExceptionCannotAddUser ($ username, $ this-> systemCommand-> getFailureMessage ());
Lancer notre propre exception est juste une question de changer l'ancien "jeter
"commande au nouveau et envoie deux paramètres au lieu de composer le message ici. Bien sûr, tous les tests passent.
PHPUnit 3.7.28 de Sebastian Bergmann… Durée: 18 ms, Mémoire: 3,00 Mo OK (2 tests, 4 assertions) Terminé.
Les exceptions doivent être capturées à un moment donné, à moins que vous ne souhaitiez que votre utilisateur les voit telles quelles. Si vous utilisez un framework MVC, vous souhaiterez probablement intercepter des exceptions dans le contrôleur ou le modèle. Une fois l'exception capturée, elle est transformée en un message destiné à l'utilisateur et restituée dans votre vue. Un moyen commun d’y parvenir est de créer un "tryAction ($ action)
"méthode dans le contrôleur ou le modèle de base de votre application et appelez-la toujours avec l'action en cours. Cette méthode vous permet de créer la logique de capture et de générer un message agréable en fonction de votre environnement.
Si vous n'utilisez pas d'infrastructure Web ni d'interface Web, votre couche de présentation doit prendre en charge la capture et la transformation de ces exceptions..
Si vous développez une bibliothèque, capturer vos exceptions sera la responsabilité de vos clients.
C'est tout. Nous avons parcouru toutes les couches de notre application. Nous avons validé en JavaScript, HTML et dans nos modèles. Nous avons lancé et saisi des exceptions à notre logique métier et avons même créé nos propres exceptions personnalisées. Cette approche de la validation et de la gestion des exceptions peut s’appliquer sans problèmes graves aux projets de grande envergure. Toutefois, si votre logique de validation devient très complexe et que différentes parties de votre projet utilisent des parties de logique qui se chevauchent, vous pouvez envisager d'extraire toutes les validations pouvant être effectuées à un niveau spécifique vers un service de validation ou un fournisseur de validation. Ces niveaux peuvent inclure, sans que cela ne soit nécessaire, le validateur JavaScript, le validateur backend PHP, le validateur de communication tiers, etc..
Merci pour la lecture. Bonne journée.