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é?
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 est logique, 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 de IContstraint
qui est définie comme:
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 'Age'
Est censé 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 à:
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é Model devrait avoir une méthode comme Result checkValue(string property, mixed value)
. Le contrôleur doit l'appeler avant paramétrer les données. Le Result
devrait avoir toutes les informations pour savoir si la vérification a échoué, et dans le cas contraire, donner des raisons, afin que le contrôleur 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.
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 construire 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 que votre couche de modèle soit trop contrainte à 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/