Avertissement: ce qui suit est une description de la façon dont je comprends les modèles de type MVC dans le contexte des applications Web basées sur PHP. Tous les liens externes utilisés dans le contenu sont là pour expliquer les termes et les concepts, et non pour impliquer ma propre crédibilité sur le sujet.
La première chose que je dois clarifier est: le modèle est une couche .
Deuxièmement: il existe une différence entre le MVC classique et ce que nous utilisons dans le développement Web. Voici un peu d'une réponse plus ancienne que j'ai écrite, qui décrit brièvement en quoi elles sont différentes.
Ce qu'un modèle n'est PAS:
Le modèle n'est pas une classe ou un objet unique. C'est une erreur très courante à commettre (moi aussi, bien que la réponse originale ait été écrite quand j'ai commencé à apprendre le contraire) , car la plupart des frameworks perpétuent cette idée fausse.
Ce n'est pas non plus une technique de cartographie relationnelle-objet (ORM) ni une abstraction des tables de base de données. Quiconque vous dit le contraire essaie très probablement de «vendre» un autre ORM flambant neuf ou un framework entier.
Qu'est-ce qu'un modèle:
Dans une adaptation MVC appropriée, le M contient toute la logique métier du domaine et la couche modèle est principalement constituée de trois types de structures:
Objets de domaine
Un objet de domaine est un conteneur logique d'informations purement de domaine; il représente généralement une entité logique dans l'espace du domaine problématique. Communément appelé logique métier .
Ce serait là que vous définissez comment valider les données avant d'envoyer une facture, ou pour calculer le coût total d'une commande. Dans le même temps, les objets de domaine ignorent complètement le stockage - ni d' où (base de données SQL, API REST, fichier texte, etc.) ni même s'ils sont enregistrés ou récupérés.
Mappeurs de données
Ces objets ne sont responsables que du stockage. Si vous stockez des informations dans une base de données, ce serait là que réside le SQL. Ou peut-être utilisez-vous un fichier XML pour stocker des données, et vos Data Mappers analysent à partir et vers des fichiers XML.
Prestations de service
Vous pouvez les considérer comme des "objets de domaine de niveau supérieur", mais au lieu de la logique métier, les services sont responsables de l'interaction entre les objets de domaine et les mappeurs . Ces structures finissent par créer une interface "publique" pour interagir avec la logique métier du domaine. Vous pouvez les éviter, mais à peine de divulguer une logique de domaine dans les contrôleurs .
Il existe une réponse connexe à ce sujet dans la question sur la mise en œuvre de l' ACL - elle pourrait être utile.
La communication entre la couche modèle et d'autres parties de la triade MVC ne doit se faire que via les services . La séparation claire présente quelques avantages supplémentaires:
- il aide à faire respecter le principe de responsabilité unique (PRS)
- offre une «marge de manœuvre» supplémentaire en cas de changement de logique
- maintient le contrôleur aussi simple que possible
- donne un plan clair, si vous avez besoin d'une API externe
Comment interagir avec un modèle?
Prérequis: regarder les conférences "Global State and Singletons" et "Don't Look For Things!" des Clean Code Talks.
Accéder aux instances de service
Pour que les instances View et Controller (ce que vous pourriez appeler: "couche UI") aient accès à ces services, il existe deux approches générales:
- Vous pouvez injecter directement les services requis dans les constructeurs de vos vues et contrôleurs, de préférence à l'aide d'un conteneur DI.
- Utilisation d'une fabrique de services comme dépendance obligatoire pour toutes vos vues et contrôleurs.
Comme vous vous en doutez, le conteneur DI est une solution beaucoup plus élégante (tout en n'étant pas la plus simple pour un débutant). Les deux bibliothèques que je recommande de considérer pour cette fonctionnalité seraient le composant DependencyInjection autonome de Syfmony ou Auryn .
Les solutions utilisant une usine et un conteneur DI vous permettent également de partager les instances de divers serveurs à partager entre le contrôleur sélectionné et de visualiser un cycle de demande-réponse donné.
Modification de l'état du modèle
Maintenant que vous pouvez accéder à la couche modèle dans les contrôleurs, vous devez commencer à les utiliser:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Vos contrôleurs ont une tâche très claire: prendre l'entrée utilisateur et, sur la base de cette entrée, changer l'état actuel de la logique métier. Dans cet exemple, les états qui sont modifiés entre "utilisateur anonyme" et "utilisateur connecté".
Le contrôleur n'est pas responsable de la validation de l'entrée de l'utilisateur, car cela fait partie des règles métier et le contrôleur n'appelle certainement pas des requêtes SQL, comme ce que vous verriez ici ou ici (veuillez ne pas les détester, elles sont erronées, pas mal).
Affichage à l'utilisateur du changement d'état.
Ok, l'utilisateur s'est connecté (ou a échoué). Maintenant quoi? Cet utilisateur l'ignore encore. Vous devez donc produire une réponse et c'est la responsabilité d'un point de vue.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
Dans ce cas, la vue a produit l'une des deux réponses possibles, en fonction de l'état actuel de la couche modèle. Pour un cas d'utilisation différent, vous auriez la vue en choisissant différents modèles à restituer, basés sur quelque chose comme "l'article actuellement sélectionné".
La couche de présentation peut en fait être assez élaborée, comme décrit ici: Comprendre les vues MVC en PHP .
Mais je fais juste une API REST!
Bien sûr, il y a des situations où c'est une surpuissance.
MVC est juste une solution concrète pour le principe de séparation des préoccupations . MVC sépare l'interface utilisateur de la logique métier et, dans l'interface utilisateur, elle a séparé la gestion des entrées utilisateur et de la présentation. C'est crucial. Bien que les gens la décrivent souvent comme une "triade", elle n'est pas composée de trois parties indépendantes. La structure ressemble plus à ceci:
Cela signifie que, lorsque la logique de votre couche de présentation est presque inexistante, l'approche pragmatique consiste à les conserver en tant que couche unique. Il peut également simplifier considérablement certains aspects de la couche modèle.
En utilisant cette approche, l'exemple de connexion (pour une API) peut être écrit comme suit:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
Bien que cela ne soit pas durable, lorsque vous avez une logique compliquée pour le rendu d'un corps de réponse, cette simplification est très utile pour des scénarios plus triviaux. Mais attention , cette approche deviendra un cauchemar, quand on tentera de l'utiliser dans de grandes bases de code avec une logique de présentation complexe.
Comment construire le modèle?
Puisqu'il n'y a pas une seule classe "Model" (comme expliqué ci-dessus), vous ne "construisez pas vraiment le modèle". Au lieu de cela, vous commencez à créer des services , qui sont capables d'exécuter certaines méthodes. Et puis implémentez les objets de domaine et les mappeurs .
Un exemple de méthode de service:
Dans les deux approches ci-dessus, il y avait cette méthode de connexion pour le service d'identification. À quoi cela ressemblerait-il réellement? J'utilise une version légèrement modifiée de la même fonctionnalité d' une bibliothèque , que j'ai écrite .. parce que je suis paresseux:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
Comme vous pouvez le voir, à ce niveau d'abstraction, il n'y a aucune indication d'où les données ont été extraites. Il peut s'agir d'une base de données, mais il peut également s'agir simplement d'un objet factice à des fins de test. Même les mappeurs de données, qui sont réellement utilisés pour cela, sont cachés dans les private
méthodes de ce service.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Façons de créer des mappeurs
Pour implémenter une abstraction de persistance, les approches les plus flexibles consistent à créer des mappeurs de données personnalisés .
De: livre PoEAA
En pratique, ils sont mis en œuvre pour l'interaction avec des classes ou des superclasses spécifiques. Disons que vous avez Customer
et Admin
dans votre code (tous deux héritant d'une User
superclasse). Les deux finiraient probablement par avoir un mappeur de correspondance distinct, car ils contiennent des champs différents. Mais vous vous retrouverez également avec des opérations partagées et couramment utilisées. Par exemple: mettre à jour l' heure de la "dernière vue en ligne" . Et au lieu de rendre les mappeurs existants plus compliqués, l'approche la plus pragmatique est d'avoir un "User Mapper" général, qui ne met à jour que l'horodatage.
Quelques commentaires supplémentaires:
Tables et modèle de base de données
Bien qu'il existe parfois une relation directe 1: 1: 1 entre une table de base de données, un objet de domaine et un mappeur , dans des projets plus importants, il peut être moins courant que prévu:
Les informations utilisées par un seul objet de domaine peuvent être mappées à partir de différentes tables, tandis que l'objet lui-même n'a aucune persistance dans la base de données.
Exemple: si vous générez un rapport mensuel. Cela collecterait des informations à partir de différentes tables, mais il n'y a pas de MonthlyReport
table magique dans la base de données.
Un mappeur unique peut affecter plusieurs tables.
Exemple: lorsque vous stockez des données de l' User
objet, cet objet de domaine peut contenir une collection d'autres objets de domaine - des Group
instances. Si vous les modifiez et les stockez User
, le Data Mapper devra mettre à jour et / ou insérer des entrées dans plusieurs tables.
Les données d'un seul objet de domaine sont stockées dans plusieurs tables.
Exemple: dans les grands systèmes (pensez: un réseau social de taille moyenne), il peut être pragmatique de stocker les données d'authentification des utilisateurs et les données souvent consultées séparément des gros morceaux de contenu, ce qui est rarement nécessaire. Dans ce cas, il se peut que vous ayez toujours une seule User
classe, mais les informations qu'elle contient dépendent de l'extraction des détails complets.
Pour chaque objet de domaine, il peut y avoir plus d'un mappeur
Exemple: vous avez un site d'actualités avec un code partagé basé à la fois pour le public et le logiciel de gestion. Mais, alors que les deux interfaces utilisent la même Article
classe, la gestion a besoin de beaucoup plus d'informations. Dans ce cas, vous auriez deux mappeurs distincts: "interne" et "externe". Chacun effectuant des requêtes différentes, ou même utilise des bases de données différentes (comme en maître ou en esclave).
Une vue n'est pas un modèle
Les instances de vue dans MVC (si vous n'utilisez pas la variation MVP du modèle) sont responsables de la logique de présentation. Cela signifie que chaque vue jonglera généralement avec au moins quelques modèles. Il acquiert des données de la couche modèle puis, en fonction des informations reçues, choisit un modèle et définit des valeurs.
L'un des avantages que vous en retirez est la réutilisation. Si vous créez une ListView
classe, alors, avec un code bien écrit, vous pouvez avoir la même classe en remettant la présentation de la liste d'utilisateurs et des commentaires sous un article. Parce qu'ils ont tous deux la même logique de présentation. Vous changez simplement de modèle.
Vous pouvez utiliser des modèles PHP natifs ou utiliser un moteur de modélisation tiers. Il peut également exister des bibliothèques tierces, capables de remplacer complètement les instances de View .
Qu'en est-il de l'ancienne version de la réponse?
Le seul changement majeur est que, ce qui est appelé modèle dans l'ancienne version, est en fait un service . Le reste de "l'analogie de la bibliothèque" se maintient assez bien.
Le seul défaut que je vois est que ce serait une bibliothèque vraiment étrange, car elle vous renverrait des informations du livre, mais ne vous laisserait pas toucher le livre lui-même, car sinon l'abstraction commencerait à "fuir". Je devrais peut-être penser à une analogie plus appropriée.
Quelle est la relation entre les instances View et Controller ?
La structure MVC est composée de deux couches: ui et model. Les structures principales de la couche d'interface utilisateur sont les vues et le contrôleur.
Lorsque vous traitez avec des sites Web qui utilisent le modèle de conception MVC, la meilleure façon est d'avoir une relation 1: 1 entre les vues et les contrôleurs. Chaque vue représente une page entière de votre site Web et dispose d'un contrôleur dédié pour gérer toutes les demandes entrantes pour cette vue particulière.
Par exemple, pour représenter un article ouvert, vous auriez \Application\Controller\Document
et \Application\View\Document
. Cela contiendrait toutes les fonctionnalités principales de la couche d'interface utilisateur, lorsqu'il s'agit de traiter des articles (bien sûr, vous pourriez avoir certains composants XHR qui ne sont pas directement liés aux articles) .