Dans MVC, un modèle devrait-il gérer la validation?

25

J'essaie de ré-architecturer une application Web que j'ai développée pour utiliser le modèle MVC, mais je ne sais pas si la validation doit être gérée ou non dans le modèle. Par exemple, je configure un de mes modèles comme celui-ci:

class AM_Products extends AM_Object 
{
    public function save( $new_data = array() ) 
    {
        // Save code
    }
}

Première question: Je me demande donc si ma méthode de sauvegarde devrait appeler une fonction de validation sur $ new_data ou supposer que les données ont déjà été validées?

De plus, s'il devait offrir une validation, je pense qu'une partie du code du modèle pour définir les types de données ressemblerait à ceci:

class AM_Products extends AM_Object
{
    protected function init() // Called by __construct in AM_Object
    {
        // This would match up to the database column `age`
        register_property( 'age', 'Age', array( 'type' => 'int', 'min' => 10, 'max' => 30 ) ); 
    }
}

Deuxième question: chaque classe enfant d'AM_Object exécuterait register_property pour chaque colonne de la base de données de cet objet spécifique. Je ne sais pas si c'est une bonne façon de le faire ou non.

Troisième question: si la validation doit être gérée par le modèle, doit-il renvoyer un message d'erreur ou un code d'erreur et demander à la vue d'utiliser le code pour afficher un message approprié?

Brandon Wamboldt
la source

Réponses:

30

Première réponse: Un rôle clé du modèle est de maintenir l'intégrité. Cependant, le traitement des entrées utilisateur est la responsabilité d'un contrôleur.

Autrement dit, le contrôleur doit traduire les données utilisateur (qui la plupart du temps ne sont que des chaînes) en quelque chose de significatif. Cela nécessite une analyse (et peut dépendre de choses telles que les paramètres régionaux, étant donné que, par exemple, il existe différents opérateurs décimaux, etc.).
La validation proprement dite, comme dans «les données sont-elles bien formées?», Doit donc être effectuée par le contrôleur. Cependant la vérification, comme dans "les données ont-elles un sens?" doit être effectuée dans le modèle.

Pour clarifier cela avec un exemple:
Supposons que votre application vous permette d'ajouter des entités, avec une date (un problème avec un délai par exemple). Vous pouvez avoir une API, où les dates peuvent être représentées comme de simples horodatages Unix, tandis que lorsque vous venez d'une page HTML, ce sera un ensemble de valeurs différentes ou une chaîne au format MM / JJ / AAAA. Vous ne voulez pas ces informations dans le modèle. Vous voulez que chaque contrôleur essaie individuellement de déterminer la date. Cependant, lorsque la date est ensuite transmise au modèle, le modèle doit conserver son intégrité. Par exemple, il peut être judicieux de ne pas autoriser les dates du passé ou des dates qui sont des jours fériés / dimanches, etc.

Votre contrôleur contient des règles d'entrée (de traitement). Votre modèle contient des règles métier. Vous souhaitez que vos règles métier soient toujours appliquées, quoi qu'il arrive. En supposant que vous disposiez de règles métier dans le contrôleur, vous devrez les dupliquer si vous créez un autre contrôleur.

Deuxième réponse: L'approche a du sens, mais la méthode pourrait être rendue plus puissante. Au lieu que le dernier paramètre soit un tableau, il doit s'agir d'une instance IContstraintdéfinie comme suit:

interface IConstraint {
     function test($value);//returns bool
}

Et pour les chiffres, vous pourriez avoir quelque chose comme

class NumConstraint {
    var $grain;
    var $min;
    var $max;
    function __construct($grain = 1, $min = NULL, $max = NULL) {
         if ($min === NULL) $min = INT_MIN;
         if ($max === NULL) $max = INT_MAX;
         $this->min = $min;
         $this->max = $max;
         $this->grain = $grain;
    }
    function test($value) {
         return ($value % $this->grain == 0 && $value >= $min && $value <= $max);
    }
}

Je ne vois pas non plus ce que l' 'Age'on veut représenter, pour être honnête. S'agit-il du nom réel de la propriété? En supposant qu'il existe une convention par défaut, le paramètre pourrait simplement aller à la fin de la fonction et être facultatif. S'il n'est pas défini, il correspondrait par défaut à to_camel_case du nom de la colonne DB.

Ainsi, l'exemple d'appel ressemblerait à ceci:

register_property('age', new NumConstraint(1, 10, 30));

L'intérêt d'utiliser des interfaces est que vous pouvez ajouter de plus en plus de contraintes au fur et à mesure et qu'elles peuvent être aussi compliquées que vous le souhaitez. Pour qu'une chaîne corresponde à une expression régulière. Pour une date au moins 7 jours à l'avance. Etc.

Troisième réponse: chaque entité modèle doit avoir une méthode comme Result checkValue(string property, mixed value). Le contrôleur doit l'appeler avant de définir les données. Le Resultdoit disposer de toutes les informations permettant de savoir si la vérification a échoué et, dans le cas contraire, donner des raisons, afin que le responsable du traitement puisse les propager à la vue en conséquence.
Si une mauvaise valeur est transmise au modèle, le modèle doit simplement répondre en levant une exception.

back2dos
la source
Merci pour cet article. Cela a clarifié beaucoup de choses sur MVC.
AmadeusDrZaius
5

Je ne suis pas complètement d'accord avec "back2dos": Ma recommandation est de toujours utiliser une couche de formulaire / validation distincte, que le contrôleur peut utiliser pour valider les données d'entrée avant qu'elles ne soient envoyées au modèle.

D'un point de vue théorique, la validation du modèle fonctionne sur des données fiables (état du système interne) et devrait idéalement être répétable à tout moment, tandis que la validation d'entrée opère explicitement une fois sur des données provenant de sources non fiables (selon le cas d'utilisation et les privilèges de l'utilisateur).

Cette séparation permet de créer des modèles, des contrôleurs et des formulaires réutilisables qui peuvent être couplés de manière lâche via l'injection de dépendances. Considérez la validation des entrées comme une validation de liste blanche («accepter le bien connu») et la validation du modèle comme une validation de liste noire («rejeter le mauvais connu»). La validation de la liste blanche est plus sécurisée tandis que la validation de la liste noire empêche votre couche de modèle d'être trop limitée à des cas d'utilisation très spécifiques.

Les données de modèle non valides doivent toujours provoquer une exception (sinon l'application peut continuer à s'exécuter sans remarquer l'erreur) tandis que les valeurs d'entrée non valides provenant de sources externes ne sont pas inattendues, mais plutôt courantes (sauf si vous avez des utilisateurs qui ne font jamais d'erreurs).

Voir aussi: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/

lastzero
la source
Par souci de simplicité, supposons qu'il existe une famille de classes Validator et que toutes les validations sont effectuées avec une hiérarchie stratégique. Les enfants valideurs concrets peuvent également être composés de validateurs spécialisés: e-mail, numéro de téléphone, jetons de formulaire, captcha, mot de passe et autres. La validation des entrées du contrôleur est de deux types: 1) vérification de l'existence d'un contrôleur et de la méthode / commande, et 2) examen préliminaire des données (c'est-à-dire la méthode de requête HTTP, combien d'entrées de données (trop? Trop peu?).
Anthony Rutledge
Une fois la quantité d'entrées vérifiée, vous devez savoir que les bons contrôles HTML ont été soumis, par leur nom, en gardant à l'esprit que le nombre d'entrées par demande peut varier, car tous les contrôles d'un formulaire HTML ne soumettent pas quelque chose lorsqu'ils sont laissés en blanc ( notamment les cases à cocher). Après cela, la dernière vérification préliminaire est un test de la taille d'entrée. À mon avis, cela devrait être tôt , pas tard. La vérification de la quantité, du nom du contrôle et de la taille d'entrée de base dans un validateur de contrôleur signifierait avoir un validateur pour chaque commande / méthode dans le contrôleur. Je pense que cela rend votre application plus sécurisée.
Anthony Rutledge
Oui, le validateur de contrôleur pour une commande sera étroitement couplé aux arguments (le cas échéant) requis pour une méthode de modèle , mais le contrôleur lui-même ne le sera pas, sauf pour la référence audit validateur de contrôleur . Il s'agit d'un compromis valable, car il ne faut pas avancer avec l'hypothèse que la plupart des contributions seront légitimes. Le plus tôt vous pouvez arrêter l'accès illégitime à votre application, mieux c'est. Le faire dans une classe de validation de contrôleur (quantité, nom et taille maximale des entrées) vous évite d'avoir à instancier tout le modèle pour rejeter les requêtes HTTP clairement malveillantes.
Anthony Rutledge
Cela étant dit, avant d'aborder les problèmes de taille d'entrée maximale, il faut s'assurer que le codage est bon. Tout bien considéré, c'est trop pour le modèle, même si le travail est encapsulé. Il devient inutilement coûteux de rejeter les demandes malveillantes. En résumé, le contrôleur doit assumer plus de responsabilités pour ce qu'il envoie au modèle. L'échec au niveau du contrôleur doit être fatal, sans retour d'information au demandeur autre que 200 OK. Enregistrez l'activité. Jetez une exception fatale. Mettez fin à toute activité. Arrêtez tous les processus dès que possible.
Anthony Rutledge
Les commandes minimales, les commandes maximales, les commandes correctes, le codage d'entrée et la taille d'entrée maximale se rapportent tous à la nature de la demande (d'une manière ou d'une autre). Certaines personnes n'ont pas identifié ces cinq éléments fondamentaux comme déterminant si une demande devait être honorée. Si toutes ces choses ne sont pas satisfaites, pourquoi envoyez-vous ces informations au modèle? Bonne question.
Anthony Rutledge
3

Oui, le modèle doit effectuer une validation. L'interface utilisateur doit également valider l'entrée.

Il est clairement de la responsabilité du modèle de déterminer des valeurs et des états valides. Parfois, ces règles changent souvent. Dans ce cas, je nourrirais le modèle à partir des métadonnées et / ou le décorerais.

Faucon
la source
Qu'en est-il des cas dans lesquels l'intention de l'utilisateur est clairement malveillante ou erronée? Par exemple, une requête HTTP particulière est censée ne pas avoir plus de sept (7) valeurs d'entrée, mais votre contrôleur obtient soixante-dix (70). Allez-vous vraiment autoriser dix fois (10x) le nombre de valeurs autorisées pour frapper le modèle lorsque la demande est clairement corrompue? Dans ce cas, c'est l'état de la demande entière qui est en cause, pas l'état d'une valeur particulière. Une stratégie de défense en profondeur suggérerait que la nature de la requête HTTP soit examinée avant d'envoyer les données au modèle.
Anthony Rutledge
(suite) De cette façon, vous ne vérifiez pas que des valeurs et des états particuliers fournis par l'utilisateur sont valides, mais que la totalité de la demande est valide. Il n'est pas encore nécessaire de creuser aussi loin. L'huile est déjà à la surface.
Anthony Rutledge
(suite) Il n'y a aucun moyen de forcer la validation frontale. Il faut considérer que des outils automatisés peuvent être utilisés en interface avec votre application web.
Anthony Rutledge
(Après réflexion) Les valeurs valides et les états des données dans le modèle sont importants, mais ce que j'ai décrit correspond à l' intention de la demande entrant via le contrôleur. L'omission de la vérification d' intention rend votre application plus vulnérable. L'intention ne peut être que bonne (jouer selon vos règles) ou mauvaise (sortir de vos règles). L'intention peut être vérifiée par des vérifications de base sur l'entrée: commandes minimales, commandes maximales, commandes correctes, codage d'entrée et taille d'entrée maximale. C'est une proposition tout ou rien. Tout passe ou la demande n'est pas valide. Pas besoin d'envoyer quoi que ce soit au modèle.
Anthony Rutledge
2

Grande question!

En ce qui concerne le développement du World Wide Web, que se passe-t-il si vous posez également les questions suivantes.

"Si une mauvaise entrée utilisateur est fournie à un contrôleur à partir d'une interface utilisateur, le contrôleur doit-il mettre à jour la vue dans une sorte de boucle cyclique, forçant les commandes et les données d'entrée à être précises avant de les traiter ? Comment? Comment la vue est-elle mise à jour dans des conditions normales Est-ce qu'une vue est étroitement couplée à un modèle? La validation des entrées utilisateur est-elle la logique métier principale du modèle, ou est-elle préliminaire et devrait donc se produire à l'intérieur du contrôleur (car les données d'entrée utilisateur font partie de la demande)?

(En effet, peut-on et devrait-on retarder l'instanciation d'un modèle jusqu'à ce qu'une bonne entrée soit acquise?)

Mon avis est que les modèles doivent gérer une circonstance pure et vierge (autant que possible), sans être gênée par une validation d'entrée de requête HTTP de base qui devrait se produire avant l'instanciation du modèle (et certainement avant que le modèle n'obtienne des données d'entrée). Étant donné que la gestion des données d'état (persistantes ou non) et des relations API est le monde du modèle, laissez la validation d'entrée de requête HTTP de base se produire dans le contrôleur.

Résumant.

1) Validez votre itinéraire (analysé à partir de l'URL), car le contrôleur et la méthode doivent exister avant que quoi que ce soit d'autre puisse avancer. Cela devrait certainement se produire dans le domaine du contrôleur frontal (classe Router), avant d'arriver au vrai contrôleur. Duh. :-)

2) Un modèle peut avoir de nombreuses sources de données d'entrée: une requête HTTP, une base de données, un fichier, une API et, oui, un réseau. Si vous allez placer toute votre validation d'entrée dans le modèle, vous envisagez la validation d'entrée de demande HTTP partie des exigences commerciales du programme. Affaire classée.

3) Pourtant, il est myope de passer par le coût de l'instanciation de beaucoup d'objets si l' entrée de requête HTTP n'est pas bonne! Vous pouvez savoir si ** l'entrée de requête HTTP ** est bonne ( fournie avec la requête ) en la validant avant d'instancier le modèle et toutes ses complexités (oui, peut-être même plus de validateurs pour les données d'entrée / sortie API et DB).

Testez les éléments suivants:

a) La méthode de requête HTTP (GET, POST, PUT, PATCH, DELETE ...)

b) Contrôles HTML minimum (en avez-vous assez?).

c) Contrôles HTML maximum (en avez-vous trop?).

d) Corriger les contrôles HTML (avez-vous les bons?).

e) Encodage d'entrée (est-ce généralement l'encodage UTF-8?).

f) Taille d'entrée maximale (l'une des entrées est-elle largement hors limites?).

N'oubliez pas que vous pouvez obtenir des chaînes et des fichiers, donc attendre que le modèle soit instancié peut devenir très coûteux lorsque les demandes arrivent sur votre serveur.

Ce que j'ai décrit ici correspond à l' intention de la demande via le contrôleur. L'omission de la vérification d' intention rend votre application plus vulnérable. L'intention ne peut être que bonne (en respectant vos règles fondamentales) ou mauvaise (sortir de vos règles fondamentales).

L'intention d'une demande HTTP est une proposition tout ou rien. Tout passe ou la demande n'est pas valide . Pas besoin d'envoyer quoi que ce soit au modèle.

Ce niveau de base de l' intention de demande HTTP n'a rien à voir avec les erreurs de saisie utilisateur standard et la validation. Dans mes applications, une requête HTTP doit être valide des cinq façons ci-dessus pour que je puisse l'honorer. Dans un mode de défense en profondeur , vous n'obtenez jamais de validation d'entrée utilisateur côté serveur si l' une de ces cinq choses échoue.

Oui, cela signifie que même l'entrée de fichiers doit être conforme à vos tentatives frontales de vérifier et d'indiquer à l'utilisateur la taille de fichier maximale acceptée. Uniquement HTML? Pas de JavaScript? D'accord, mais l'utilisateur doit être informé des conséquences du téléchargement de fichiers trop volumineux (principalement, il perdra toutes les données du formulaire et sera expulsé du système).

4) Cela signifie-t-il que les données d'entrée de requête HTTP ne font pas partie de la logique métier de l'application? Non, cela signifie simplement que les ordinateurs sont des appareils finis et que les ressources doivent être utilisées à bon escient. Il est logique d'arrêter les activités malveillantes le plus tôt possible. Vous payez plus en ressources de calcul pour attendre de l'arrêter plus tard.

5) Si l' entrée de demande HTTP est mauvaise, la demande entière est mauvaise . Voilà comment je le vois. La définition d'une bonne entrée de requête HTTP est dérivée des exigences commerciales du modèle, mais il doit y avoir un point de démarcation des ressources. Combien de temps laisserez-vous vivre une mauvaise demande avant de la tuer et de dire: "Oh, hé, tant pis. Mauvaise demande."

Le jugement n'est pas simplement que l'utilisateur a fait une erreur de saisie raisonnable, mais qu'une requête HTTP est tellement hors limites qu'elle doit être déclarée malveillante et arrêtée immédiatement.

6) Donc, pour mon argent, la requête HTTP (MÉTHODE, URL / route et données) est TOUT bonne ou RIEN d'autre ne peut continuer. Un modèle robuste a déjà des tâches de validation à gérer, mais un bon berger de ressources dit: "Mon chemin, ou le chemin du haut. Venez correct, ou ne venez pas du tout."

Mais c'est votre programme. "Il y a plus d'une façon de le faire." Certains moyens coûtent plus cher en temps et en argent que d'autres. La validation ultérieure des données de requête HTTP (dans le modèle) devrait coûter plus cher pendant la durée de vie d'une application (en particulier si elle est mise à l'échelle ou augmentée).

Si vos validateurs sont modulaires, la validation de l'entrée de requête HTTP * de base ** dans le contrôleur ne devrait pas poser de problème. Il vous suffit d'utiliser une classe Validator stratégique, où les validateurs sont parfois également composés de validateurs spécialisés (e-mail, téléphone, jeton de formulaire, captcha, ...).

Certains voient cela comme complètement faux, mais HTTP en était à ses balbutiements lorsque le gang des quatre a écrit Design Patterns: Elements of Re-usable Object-Oriented Software .

================================================== ========================

Maintenant, en ce qui concerne la validation d'entrée utilisateur normale (une fois que la requête HTTP a été jugée valide), il met à jour la vue lorsque l'utilisateur gâche et que vous devez y penser! Ce type de validation d'entrée utilisateur doit se produire dans le modèle.

Vous n'avez aucune garantie de JavaScript sur le front-end. Cela signifie que vous n'avez aucun moyen de garantir la mise à jour asynchrone de l'interface utilisateur de votre application avec des états d'erreur. Une véritable amélioration progressive couvrirait également le cas d'utilisation synchrone.

La prise en compte du cas d'utilisation synchrone est un art qui se perd de plus en plus parce que certaines personnes ne veulent pas passer par le temps et se tracasser pour suivre l'état de toutes leurs astuces d'interface utilisateur (afficher / masquer les contrôles, désactiver / activer les contrôles , indications d'erreur, messages d'erreur) sur le serveur principal (généralement en suivant l'état des tableaux).

Mise à jour : Dans le diagramme, je dis que le Viewdevrait faire référence au Model. Non. Vous devez transmettre les données à Viewpartir du Modelpour conserver le couplage lâche. entrez la description de l'image ici

Anthony Rutledge
la source