Chapitre 216 minIntermédiaire
Le modèle MVC : modèle, vue, contrôleur
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- définir le rôle du modèle, de la vue et du contrôleur ;
- suivre le trajet d'une requête dans une application MVC ;
- ranger une ligne de code dans la bonne couche ;
- situer MVP, MVVM et le MVC côté client par rapport au MVC classique.
5 exercices corrigés1 schéma
MVC (modèle-vue-contrôleur, Model-View-Controller) est une façon d'organiser le code d'une application en trois rôles : le modèle gère les données et les règles, la vue affiche, le contrôleur reçoit les actions de l'utilisateur, demande au modèle de les traiter et choisit la vue à afficher. Ce chapitre décrit chaque rôle, suit une requête de bout en bout et donne les règles qui permettent de savoir où ranger une ligne de code. Les trois chapitres suivants appliquent MVC en PHP, en Node.js et en C#.
Les trois rôles
Le modèle
Le modèle représente les données de l'application et ce qu'on a le droit d'en faire. Dans ResaSalles, il connaît les salles et les réservations, sait les lire et les enregistrer en base, et sait qu'une réservation ne peut pas dépasser 4 heures.
Il ne sait rien de l'affichage ni de la requête HTTP : pas de HTML, pas de $_POST, pas de req ni de res, pas de MessageBox. On doit pouvoir l'utiliser depuis une page web, une API, une application de bureau ou un script en ligne de commande sans le modifier.
Selon les projets, le mot « modèle » recouvre plus ou moins de choses :
- les entités, des classes ou des objets simples qui portent les données (
Salle,Reservation) ; - l'accès aux données, le code qui exécute le SQL (souvent un DAO ou un Repository, chapitre 7) ;
- les règles de gestion, qui disent ce qui est permis (souvent un service, chapitre 6).
Dans une petite application, les trois tiennent dans une seule classe ReservationModel. Dans une application qui grandit, on les sépare : c'est le passage de MVC à l'architecture en couches.
La vue
La vue présente les données à l'utilisateur et lui offre les moyens d'agir : une page HTML avec un formulaire, un formulaire WinForms avec des boutons, un composant React, ou même un document JSON dans le cas d'une API. Elle reçoit des données toutes prêtes et se contente de les mettre en forme.
Une vue peut contenir un peu de logique d'affichage : une boucle pour afficher les lignes d'un tableau, une condition pour afficher « Aucune réservation » quand la liste est vide, le formatage d'une date. Elle ne contient jamais de requête SQL, ni de règle de gestion (« si la salle a moins de 6 places, interdire la visio »).
Le contrôleur
Le contrôleur reçoit l'action de l'utilisateur (une requête HTTP, un clic sur un bouton), en extrait les paramètres, appelle le modèle, puis choisit la vue à afficher ou la réponse à renvoyer. C'est le chef d'orchestre : il coordonne, il ne joue d'aucun instrument.
Un bon contrôleur est mince : quelques lignes par action. S'il contient des calculs, des conditions métier ou du SQL, c'est qu'il fait le travail du modèle. Les développeurs parlent de fat controller (contrôleur obèse) pour désigner ce défaut, le plus fréquent dans les projets d'étudiants.
| Rôle | Il fait | Il ne fait jamais | Dans ResaSalles |
|---|---|---|---|
| Modèle | Lire et écrire les données, appliquer les règles de gestion | Afficher, lire la requête HTTP ou les contrôles de l'écran | ReservationModel, Salle, la règle des 4 heures |
| Vue | Afficher les données reçues, proposer des actions | Exécuter du SQL, décider d'une règle de gestion | La page « Liste des salles », le formulaire de réservation |
| Contrôleur | Recevoir l'action, appeler le modèle, choisir la vue | Écrire du HTML, du SQL, des calculs métier | SalleController::detail(), btnReserver_Click qui délègue |
Le trajet d'une requête dans une application MVC web
Suivons ce qui se passe quand un salarié ouvre la page de la salle n° 1 dans une application web rendue côté serveur.
- Le navigateur envoie la requête
GET /salles/1. - Le routeur examine la méthode et l'adresse, et appelle l'action
detaildu contrôleur des salles avec le paramètre1. Dans beaucoup d'applications, un seul fichier reçoit toutes les requêtes et fait ce tri : c'est le contrôleur frontal (front controller),public/index.phpen PHP,app.jsavec Express. - Le contrôleur vérifie le paramètre (
1est-il un entier ?) et demande au modèle la salle et ses réservations. - Le modèle exécute les requêtes SQL et reçoit les lignes.
- Le modèle renvoie des données au contrôleur : un tableau, des objets, ou
nullsi la salle n'existe pas. - Le contrôleur choisit la vue :
salles/detailsi la salle existe, une page d'erreur 404 sinon. Il lui passe les données. - La vue produit le HTML, que le serveur renvoie au navigateur.
Pour un formulaire envoyé en POST, le trajet est le même, avec une étape de plus à la fin : après un enregistrement réussi, le contrôleur ne renvoie pas une page mais une redirection (303 See Other) vers la page de la salle. Si l'utilisateur appuie sur F5, le navigateur recharge la page en GET au lieu de renvoyer le formulaire et de créer une deuxième réservation. Ce schéma s'appelle Post/Redirect/Get ; le chapitre 3 le met en œuvre.
Les règles de dépendance
MVC fonctionne si chacun respecte les règles suivantes. Elles sont plus utiles à retenir que les définitions.
| Qui | Peut connaître | Ne connaît pas |
|---|---|---|
| Contrôleur | Le modèle et les vues | Le SQL, le HTML |
| Vue | Les données qu'on lui passe | La base, la session, la requête HTTP brute |
| Modèle | La base de données (ou la couche qui y accède) | Le contrôleur, la vue, HTTP |
La flèche importante est celle qui manque : le modèle ne dépend de personne au-dessus de lui. C'est ce qui permet de le réutiliser et de le tester seul.
MVC dans une application de bureau
MVC est né pour les interfaces graphiques, bien avant le web. Dans une application de bureau comme une application WinForms, il n'y a pas de requête HTTP : l'action de l'utilisateur est un événement (un clic, une sélection dans une liste).
- L'utilisateur clique sur « Réserver » ; le formulaire (la vue) reçoit l'événement
Click. - Le gestionnaire d'événement lit les champs et transmet la demande au contrôleur.
- Le contrôleur appelle le modèle, qui vérifie les règles et enregistre.
- Le contrôleur renvoie le résultat : succès ou liste d'erreurs.
- La vue met à jour la grille ou affiche un message.
La différence avec le web : la vue vit en permanence à l'écran, elle n'est pas reconstruite à chaque requête. Dans le MVC d'origine, la vue s'abonne au modèle (patron Observer) et se redessine toute seule quand les données changent. Dans une application WinForms d'étudiant, on fait plus simple : la vue recharge la grille après chaque action. Le chapitre 5 montre le code complet.
| Étape | Application web (PHP, Express) | Application de bureau (WinForms) |
|---|---|---|
| Action de l'utilisateur | Requête HTTP (GET, POST) | Événement (Click, SelectedIndexChanged) |
| Qui la reçoit d'abord | Le routeur, puis le contrôleur | Le formulaire, qui délègue au contrôleur |
| La vue | Reconstruite à chaque requête (HTML) | Toujours à l'écran, mise à jour |
| État entre deux actions | Session, cookie, base | Variables du formulaire, en mémoire |
Où mettre la logique métier ?
C'est la question la plus débattue de MVC, et la commission d'E6 la pose souvent. La réponse classique est : dans le modèle, jamais dans la vue, le moins possible dans le contrôleur. On résume par la formule « modèle épais, contrôleur mince » (fat model, skinny controller).
Le problème de la formule, c'est qu'un modèle qui contient le SQL et toutes les règles finit lui aussi par devenir énorme. La solution moderne découpe le modèle en deux :
- un service (couche métier) qui porte les règles de gestion :
ReservationService.reserver()vérifie la durée, la capacité, le chevauchement ; - un Repository (couche d'accès aux données) qui porte le SQL :
ReservationRepository.existeChevauchement().
Le contrôleur appelle le service, le service appelle le Repository. C'est l'architecture en couches du chapitre 6, qui n'est rien d'autre qu'un MVC dont on a rangé le modèle.
Les variantes : MVP, MVVM et le MVC côté client
MVC a donné naissance à plusieurs variantes. Tu dois savoir les reconnaître et dire ce qui les distingue ; tu n'as pas besoin de les maîtriser.
MVP (modèle-vue-présentateur). La vue est passive : elle ne connaît pas le modèle, elle expose des méthodes (AfficherReservations(liste), AfficherErreur(message)) et transmet les événements au présentateur. Le présentateur lit le modèle, prépare les données et met la vue à jour. On le croise dans les applications WinForms bien organisées : ce que beaucoup d'étudiants appellent « MVC en WinForms » est en réalité très proche de MVP.
MVVM (modèle-vue-vue modèle). La vue est liée à un objet intermédiaire, le ViewModel, par une liaison de données (data binding) : quand une propriété du ViewModel change, l'écran se met à jour tout seul, et inversement. C'est l'organisation de WPF et de .NET MAUI chez Microsoft, et l'esprit de Vue.js côté web.
Le MVC côté client et les composants. Avec React, l'interface est découpée en composants : chaque composant reçoit des données (props), garde un état local (useState) et décrit son affichage. Quand l'état change, React redessine le composant. Ce n'est pas du MVC au sens strict : un composant React mélange volontairement un peu de « vue » et un peu de « contrôleur ». Dans une application React + API, le découpage MVC se retrouve à l'échelle de l'ensemble : React est la présentation, l'API Express contient contrôleurs, services et accès aux données (chapitre 10).
| Patron | Qui reçoit l'action | La vue connaît-elle le modèle ? | Mise à jour de l'écran | Où on le croise |
|---|---|---|---|---|
| MVC | Le contrôleur (après le routeur ou l'événement) | Elle lit les données qu'on lui passe | La vue est reconstruite ou rechargée | Applications web rendues côté serveur, PHP, Express, ASP.NET Core MVC |
| MVP | La vue, qui transmet au présentateur | Non : vue passive | Le présentateur appelle la vue | WinForms, Android des années 2010 |
| MVVM | La vue, liée au ViewModel | Non : elle connaît le ViewModel | Automatique, par liaison de données | WPF, .NET MAUI, Vue.js |
| Composants | Le composant lui-même | Il reçoit des données (props) | Automatique quand l'état change | React, Angular, Svelte |
Avantages et limites de MVC
| Avantages | Limites |
|---|---|
| Chaque fichier a un rôle clair : on sait où chercher | Plus de fichiers et de dossiers qu'une page unique |
| Le designer travaille sur les vues, le développeur sur le modèle, sans conflit | Le « modèle » est flou : sans discipline, il devient un fourre-tout |
| Le modèle se réutilise pour une autre interface | Le contrôleur grossit vite si on n'ajoute pas de couche service |
| Les règles du modèle se testent sans interface | Pour une page statique ou un script de dix lignes, c'est trop |
| Tous les frameworks web l'utilisent : savoir MVC, c'est savoir lire Symfony, Laravel, ASP.NET Core ou Ruby on Rails | Il ne dit rien de la répartition sur plusieurs machines (c'est le rôle des tiers, chapitre 9) |
MVC est superflu pour un script d'administration, un prototype jetable ou un site vitrine sans données. Dès qu'une application a des données, des règles et plus de trois écrans, il paie.
Exercices
Exercice 1 sur 5 Ranger les lignes Réussi À revoir
Pour chaque morceau de code de ResaSalles, indique s'il appartient au modèle, à la vue ou au contrôleur.
SELECT id, nom, capacite FROM salle ORDER BY nom<td><?= htmlspecialchars($salle['nom']) ?></td>$id = (int) $parametres['id'];if ($participants > $salle->capacite) throw new ErreurMetier(...)res.status(201).json(reservation)dgvReservations.DataSource = liste;if (!salle) return res.status(404).render('erreur')- Le calcul du nombre d'heures réservées par service sur le mois.
Voir la réponseMasquer la réponse de l'exercice 1
- Modèle (accès aux données).
- Vue : elle affiche une donnée, échappée.
- Contrôleur : il extrait et convertit un paramètre de la requête.
- Modèle (règle de gestion, dans un service si l'application est en couches).
- Contrôleur : il choisit la réponse HTTP. Le JSON produit joue le rôle de la vue.
- Vue : la mise à jour d'un contrôle WinForms (le code est dans le formulaire).
- Contrôleur : il choisit la vue d'erreur et le code 404 quand le modèle ne trouve rien.
- Modèle : un calcul métier, souvent fait par une requête SQL avec
GROUP BY, appelée par un service.
Tu avais trouvé ?
Exercice 2 sur 5 Remettre le trajet dans l'ordre Réussi À revoir
Un salarié envoie le formulaire de réservation. Remets ces étapes dans l'ordre.
a. Le contrôleur redirige vers /salles/1 avec le code 303.
b. Le routeur appelle l'action reserver du contrôleur avec l'identifiant 1.
c. Le navigateur envoie POST /salles/1/reservations avec les champs du formulaire.
d. Le modèle vérifie les règles et insère la réservation.
e. Le navigateur demande GET /salles/1, et la vue affiche la salle avec la nouvelle réservation.
f. Le contrôleur lit les champs du formulaire et appelle le modèle.
Voir la réponseMasquer la réponse de l'exercice 2
c, b, f, d, a, e.
La redirection (a) puis le nouveau GET (e) forment le schéma Post/Redirect/Get : un rafraîchissement de la page ne renvoie pas le formulaire.
Tu avais trouvé ?
Exercice 3 sur 5 Repérer les entorses à MVC Réussi À revoir
Dans chacun de ces extraits, quelle règle de MVC est enfreinte, et comment corriger ?
- Dans la vue
liste.php:<?php $nb = $pdo->query("SELECT COUNT(*) FROM reservation WHERE salle_id = {$s['id']}")->fetchColumn(); ?> - Dans le modèle
ReservationModel:echo '<p class="erreur">Créneau déjà pris</p>'; - Dans le contrôleur Express : quarante lignes qui calculent le taux d'occupation de chaque salle par semaine.
- Dans le formulaire WinForms :
if (dtpFin.Value - dtpDebut.Value > TimeSpan.FromHours(4)) { … }
Voir la réponseMasquer la réponse de l'exercice 3
- La vue exécute du SQL, et une requête par salle (problème N+1). Correction : le modèle fournit la liste des salles avec leur nombre de réservations en une requête (
LEFT JOINetGROUP BY) ; le contrôleur la passe à la vue. - Le modèle produit du HTML. Correction : il lève une exception ou renvoie une erreur ; le contrôleur choisit comment l'afficher. Le même modèle pourra servir à une API JSON.
- Contrôleur obèse : le calcul est une logique métier. Correction : le déplacer dans un service (
OccupationService.tauxParSemaine()), testable sans HTTP. - Une règle de gestion est dans la vue. Correction : la vue transmet les dates au contrôleur, qui appelle le modèle ou le service ; la règle n'existe qu'à un endroit. La vue peut garder un contrôle de confort (griser le bouton) à condition que la vraie vérification soit faite côté modèle.
Tu avais trouvé ?
Exercice 4 sur 5 MVC, MVP ou MVVM ? Réussi À revoir
Identifie le patron décrit.
- La vue expose une méthode
AfficherErreur(string message); un objet séparé décide quand l'appeler, après avoir interrogé le modèle. - Quand la propriété
SalleSelectionneechange, la liste des réservations à l'écran se met à jour sans qu'aucune ligne de code ne l'y oblige. - Une requête
GET /sallesest routée vers une méthode qui récupère les salles et rend un gabarit HTML.
Voir la réponseMasquer la réponse de l'exercice 4
- MVP : la vue est passive, le présentateur la pilote.
- MVVM : la liaison de données synchronise la vue et le ViewModel.
- MVC web classique : routeur, contrôleur, modèle, vue.
Tu avais trouvé ?
Exercice 5 sur 5 QCM Réussi À revoir
Une seule bonne réponse par question.
- Dans MVC, qui choisit la vue à afficher ? a) le modèle ; b) le contrôleur ; c) le routeur ; d) la base de données.
- Quel composant doit pouvoir être réutilisé tel quel par une autre interface ? a) la vue ; b) le contrôleur ; c) le modèle ; d) aucun.
- Le schéma Post/Redirect/Get évite : a) les injections SQL ; b) les doubles soumissions de formulaire ; c) les erreurs 404 ; d) les failles XSS.
- Le contrôleur frontal est : a) le premier contrôleur écrit ; b) le fichier unique qui reçoit toutes les requêtes et les aiguille ; c) le contrôleur des pages d'accueil ; d) un pare-feu applicatif.
Voir la réponseMasquer la réponse de l'exercice 5
- b) Le contrôleur. Le routeur choisit l'action du contrôleur, pas la vue.
- c) Le modèle : il ne dépend ni de l'affichage ni de HTTP.
- b) Après un
POSTréussi, la redirection fait que F5 recharge unGET. - b) Par exemple
public/index.phpen PHP natif.
Tu avais trouvé ?