TD4 – Architecture MVC Modèle, Vue, Contrôleur
Au fur et à mesure que votre site Web grandit, vous allez rencontrer des difficultés à organiser votre code. Les prochains TDs visent à vous montrer une bonne façon de concevoir votre site web. On appelle architectural pattern (patron d’architecture) une série de bonnes pratiques pour l’organisation de votre site.
Un des plus célèbres architectural patterns s’appelle MVC (Modèle - Vue - Contrôleur) : c’est celui que nous allons découvrir dans ce TD.
Présentation du patron d’architecture MVC
Le pattern MVC permet de bien organiser son code source. Jusqu’à présent, nous avons programmé de manière monolithique : nos pages Web mélangent traitement (PHP), accès aux données (SQL) et présentation (balises HTML). Nous allons maintenant séparer toutes ces parties pour plus de clarté.
L’objectif de ce TD est donc de réorganiser le code du TD3 pour finalement y
ajouter plus facilement de nouvelles fonctionnalités. Nous allons vous
expliquer le fonctionnement sur l’exemple de la page lireUtilisateurs.php
du TD2 :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Liste des utilisateurs</title>
</head>
<body>
<?php
require_once 'Utilisateur.php';
$utilisateurs = Utilisateur::recupererUtilisateurs();
foreach ($utilisateurs as $utilisateur)
echo $utilisateur;
?>
</body>
</html>
Cette page se basait sur votre classe Utilisateur
dans Utilisateur.php
:
<?php
require_once "ConnexionBaseDeDonnees.php";
class Utilisateur {
private string $nom;
private string $prenom;
private string $login;
// getters et setters...
public function __construct(string $nom, ...) { ... }
public static function construireDepuisTableauSQL(array $utilisateurTableau): ModeleUtilisateur { ... }
public function __toString()() { ... }
public static function recupererUtilisateurs() { ... }
public static function recupererUtilisateurParLogin(string $login) { ... }
public function ajouter() { ... }
// Gestion des trajets comme passager si vous avez fait les bonus du TD3
}
?>
L’architecture MVC est une manière de découper le code en trois bouts M, V et C
ayant des fonctions bien précises. Dans notre exemple, l’ancien fichier
lireUtilisateur.php
va être réparti entre le contrôleur
Controleur/ControleurUtilisateur.php
, le modèle Modele/ModeleUtilisateur.php
et la vue
vue/utilisateur/liste.php
.
Voici un aperçu de tous les fichiers que nous allons créer dans ce TD.
Conventions de nommage :
- Les noms des fichiers PHP de déclaration de classe commencent par une majuscule, les autres non.
- Les noms des répertoires qui contiennent des déclarations de classe PHP commencent par une majuscule, les autres non.
- On privilégie le français dans les noms de classe, de fichiers, de fonctions…
M : Le modèle
Le modèle est chargé de la gestion des données, notamment des interactions avec
la base de données. C’est, par exemple, la classe Utilisateur
que vous avez créé
lors des TDs précédents (sauf la fonction __toString()
).
- Créez les répertoires
Configuration
,Controleur
,Modele
,vue
etvue/utilisateur
. -
Déplacez vos fichiers
Utilisateur.php
etConnexionBaseDeDonnees.php
dans le répertoireModele/
en évitant d’utiliser PHPStorm.En effet, PHPStorm vous rajouterait des lignes
namespace ...
etuse ...
en haut de vos scripts PHP qu’il faudrait supprimer.Remarque : Vous pourrez enfin utiliser PhpStorm pour déplacer des classes quand vous aurez appris les
namespace ...
etuse ...
dans de TD5. - Déplacez la classe
ConfigurationBaseDeDonnees
dans le dossierConfiguration
en évitant d’utiliser PHPStorm. -
Corrigez le chemin relatif du
require_once
du fichierConfigurationBaseDeDonnees.php
dansConnexionBaseDeDonnees.php
. -
Utilisez l’outil de refactoring de votre IDE pour renommer la classe
Utilisateur
enModeleUtilisateur
.Vérifiez que les déclarations de type ont bien été mises à jour partout dans votre code.
Mettez en commentaire la fonction__toString()
pour la désactiver.Aide pour le refactoring : Clic droit sur le fichier de déclaration de classe à renommer à PhpStorm, puis Refactor → Rename.
- Assurez-vous que PHPStorm n’a pas créé de ligne
namespace ...
, niuse ...
en haut de vos scripts PHP. Sinon, supprimez ces lignes. Attention : si l’utilisation de certaines classes dans votre code est soulignée en avertissement par votre IDE, alors desnamespace ...
et/ouuse ...
persistent quelque part dans vos classes.
N.B. : Il est vraiment conseillé de renommer les fichiers et non de les copier. Avoir plusieurs copies de vos classes et fichiers est source d’erreur difficile à déboguer.
Dans notre cas, la nouvelle classe ModeleUtilisateur
gère la persistance au travers
des méthodes :
$utilisateur->ajouter();
ModeleUtilisateur::recupererUtilisateurParLogin($login);
ModeleUtilisateur::recupererUtilisateurs();
N.B. : Souvenez-vous que les deux dernières fonctions recupererUtilisateurParLogin
et recupererUtilisateurs
sont static
. Elles ne dépendent donc que de
leur classe (et non pas des objets instanciés). D’où la syntaxe différente
NomClasse::nomMethodeStatique()
pour les appeler.
V : la vue
Dans la vue sont regroupées toutes les lignes de code qui génèrent la page HTML
que l’on va envoyer à l’utilisateur. Les vues sont des fichiers qui ne
contiennent quasiment exclusivement que du code HTML, à l’exception de quelques
echo
permettant d’afficher les variables préremplies par le contrôleur. Une
boucle for
est toutefois autorisée pour les vues qui affichent une liste
d’éléments. La vue n’effectue pas de traitement, de calcul.
Dans notre exemple, la vue serait le fichier vue/utilisateur/liste.php
suivant. Le code de ce fichier permet d’afficher une page Web contenant tous
les utilisateurs contenus dans la variable $utilisateurs
.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Liste des utilisateurs</title>
</head>
<body>
<?php
foreach ($utilisateurs as $utilisateur)
echo '<p> Utilisateur de login ' . $utilisateur->getLogin() . '.</p>';
?>
</body>
</html>
Créez la vue vue/utilisateur/liste.php
avec le code précédent.
Remarque : Pour ce fichier, votre IDE risque de souligner l’utilisation de la variable $utilisateurs
comme étant non-définie. C’est un comportement tout à fait normal, car il n’est jamais sûr d’utiliser
une variable dont l’existence n’est pas garantie ! Cependant, si vous savez ce que vous faites, vous pouvez
“aider” l’IDE en lui fournissant une documentation sous forme de PHPDOC. Dans notre cas :
/** @var ModeleUtilisateur[] $utilisateurs */
C : le contrôleur
Le contrôleur gère la logique du code qui prend des décisions. C’est en quelque sorte l’intermédiaire entre le modèle et la vue : le contrôleur va demander au modèle les données, les analyser, prendre des décisions et appeler la vue adéquate en lui donnant le texte à afficher. Le contrôleur contient exclusivement du PHP.
Il existe une multitude d’implémentations du MVC :
- un gros contrôleur unique
- un contrôleur par modèle
- un contrôleur pour chaque action de chaque modèle
Nous choisissons ici la version intermédiaire et commençons à créer un
contrôleur pour ModeleUtilisateur
. Voici le contrôleur
Controleur/ControleurUtilisateur.php
sur notre exemple :
<?php
require_once ('../Modele/ModeleUtilisateur.php'); // chargement du modèle
$utilisateurs = ModeleUtilisateur::recupererUtilisateurs(); //appel au modèle pour gérer la BD
require ('../vue/utilisateur/liste.php'); //copie la vue ici
?>
Notre contrôleur se décompose donc en plusieurs parties :
- On charge la déclaration de la classe
ModeleUtilisateur
; - on se sert du modèle pour récupérer le tableau de tous les utilisateurs avec
$utilisateurs = Utilisateur::recupererUtilisateurs();
- on appelle alors la vue qui va nous générer la page Web avec
require ('../vue/utilisateur/liste.php');
Notes :
- Pourquoi
../
? Les adresses sont relatives au fichier courant qui estControleur/ControleurUtilisateur.php
dans notre cas. - Notez bien que c’est le contrôleur qui initialise la variable
$utilisateurs
et que la vue ne fait que lire cette variable pour générer la page Web.
- Créez le contrôleur
Controleur/ControleurUtilisateur.php
avec le code précédent. - Testez votre page en appelant l’URL …/Controleur/ControleurUtilisateur.php
- Prenez le temps de comprendre le MVC sur cet exemple.
Avez-vous compris l’ordre dans lequel PHP exécute votre code ?
Est-ce que ce code vous semble similaire à l’ancien fichier
lireUtilisateur.php
? N’hésitez à parler de votre compréhension avec votre chargé de TD.
Le routeur : un autre composant du contrôleur
Un contrôleur doit en fait gérer plusieurs pages. Dans notre exemple, il doit
gérer toutes les pages liées au modèle ModeleUtilisateur
. Du coup, on regroupe le
code de chaque page Web dans une fonction, et on met le tout dans une classe
contrôleur.
Voici à quoi va ressembler notre contrôleur ControleurUtilisateur.php
. On
reconnaît dans la fonction afficherListe
le code précédent qui affiche tous les
utilisateurs.
<?php
require_once ('../Modele/ModeleUtilisateur.php'); // chargement du modèle
class ControleurUtilisateur {
// Déclaration de type de retour void : la fonction ne retourne pas de valeur
public static function afficherListe() : void {
$utilisateurs = ModeleUtilisateur::recupererUtilisateurs(); //appel au modèle pour gérer la BD
require ('../vue/utilisateur/liste.php'); //"redirige" vers la vue
}
}
?>
On appelle action une fonction du contrôleur ; une action correspond
généralement à une page Web. Dans notre exemple du contrôleur
ControleurUtilisateur
, nous allons bientôt ajouter les actions qui correspondent
aux pages suivantes :
- afficher tous les utilisateurs : action
afficherListe
- afficher les détails d’un utilisateur : action
afficherDetail
- afficher le formulaire de création d’un utilisateur : action
afficherFormulaireCreation
- créer un utilisateur dans la base de données et afficher un message de confirmation : action
creerDepuisFormulaire
Pour recréer la page précédente, il manque encore un bout de code qui appelle la
méthode ControleurUtilisateur::afficherListe()
. Le routeur est la partie du contrôleur
qui s’occupe d’appeler l’action du contrôleur. Un routeur simpliste serait le
fichier suivant Controleur/routeur.php
:
<?php
require_once 'ControleurUtilisateur.php';
ControleurUtilisateur::afficherListe(); // Appel de la méthode statique $action de ControleurUtilisateur
?>
- Modifiez le code de
ControleurUtilisateur.php
et créez le fichierControleur/routeur.php
pour correspondre au code ci-dessus ; - Testez la nouvelle architecture en appelant la page …/Controleur/routeur.php.
- Prenez le temps de comprendre le MVC sur cet exemple. Avez-vous compris l’ordre dans lequel PHP exécute votre code ?
Maintenant un vrai routeur
Le code précédent marche sauf que le client doit pouvoir choisir quelle action
est-ce qu’il veut effectuer. Du coup, il va faire une requête pour la page
routeur.php
, mais en envoyant l’information qu’il veut que action
soit égal à
afficherListe
. Pour transmettre ces données à la page du routeur, nous allons les
écrire dans l’URL avec la syntaxe du query string (cf. rappel sur query
string dans le cours
1).
De son côté, le routeur doit récupérer l’action envoyée et appeler la méthode
correspondante du contrôleur. Pour appeler la méthode statique de
ControleurUtilisateur
dont le nom se trouve dans la variable $action
, le PHP
peut faire comme suit. Voici le fichier Controleur/routeur.php
mis à jour :
<?php
require_once 'ControleurUtilisateur.php';
// On récupère l'action passée dans l'URL
$action = ...;
// Appel de la méthode statique $action de ControleurUtilisateur
ControleurUtilisateur::$action();
?>
- Modifiez le code
Controleur/routeur.php
pour correspondre au code ci-dessus en remplissant vous-même la ligne 4. Si vous ne vous souvenez plus comment extraire un paramètre d’une URL, relisez la partie sur le query string dans le cours 1. - Testez la nouvelle architecture en appelant la page
…/Controleur/routeur.php
en ajoutant l’information que
action
est égal àafficherListe
dans l’URL au format query string. - Prenez le temps de comprendre le MVC sur cet exemple.
Avez-vous compris l’ordre dans lequel PHP exécute votre code ?
Est-ce que ce code vous semble similaire à l’ancien fichier
lireUtilisateur.php
? N’hésitez à parler de votre compréhension avec votre chargé de TD.
Solutions
Voici le déroulé de l’exécution du routeur pour l’action afficherListe
:
- Le client demande l’URL …/Controleur/routeur.php?action=afficherListe.
- Le routeur récupère l’action donnée par l’utilisateur dans l’URL avec
$action = $_GET['action'];
(donc$action="afficherListe"
) - le routeur appelle la méthode statique
afficherListe
deControleurUtilisateur.php
ControleurUtilisateur.php
se sert du modèle pour récupérer le tableau de tous les utilisateurs ;ControleurUtilisateur.php
appelle alors la vue qui va nous générer la page Web.
À vous de jouer
Vue “détail d’un utilisateur”
Comme la page qui liste tous les utilisateurs (action afficherListe
) ne donne pas
toutes les informations, nous souhaitons créer une page de détail dont le rôle
sera d’afficher toutes les informations de l’utilisateur. Cette action aura besoin
de connaître le login de l’utilisateur visé ; on utilisera encore le
query string pour passer l’information dans l’URL en même temps que l’action :
…/routeur.php?action=afficherDetail&login=AAA111BB
-
Créez une vue
./vue/utilisateur/detail.php
qui doit afficher tous les détails de l’utilisateur stocké dans$utilisateur
de la même manière que l’ancienne fonction__toString()
(encore commentée dansModeleUtilisateur
). Note : La variable$utilisateur
sera initialisée dans le contrôleur plus tard, cf.$utilisateurs
dans l’exemple précédent. -
Ajoutez une action
afficherDetail
au contrôleurControleurUtilisateur.php
. Cette action devra récupérer le login donné dans l’URL, appeler la fonctionrecupererUtilisateurParLogin()
du modèle, mettre l’utilisateur visée dans la variable$utilisateur
et appeler la vue précédente. -
Testez cette vue en appelant la page du routeur avec les bons paramètres dans l’URL.
-
Ajoutez des liens cliquables
<a>
sur les logins de la vueliste.php
qui renvoient sur la vue de détail de l’utilisateur concernée. -
On souhaite gérer les logins non reconnus : Créez une vue
./vue/utilisateur/erreur.php
qui affiche un message d’erreur générique “Problème avec l’utilisateur” et renvoyez vers cette vue sirecupererUtilisateurParLogin()
ne trouve pas d’utilisateur qui correspond à ce login, ou si aucun login n’est indiqué dans l’URL.
Changement de l’appel à la vue
Actuellement, le chargement d’une vue se fait à l’aide du code
require ('../vue/utilisateur/liste.php');
On peut légitimement se demander comment le script liste.php
accède à la variable locale $utilisateurs
de
ControleurUtilisateur::afficherListe()
. C’est parce que require
a pour effet
de “copier/coller” les instructions du liste.php
dans la méthode ControleurUtilisateur::afficherListe()
.
Cela pose plusieurs problèmes :
- la vue a accès à toutes les variables accessibles dans
ControleurUtilisateur::afficherListe()
, - la manière de procéder du
require
est très éloignée d’un code orienté-objet propre, - une duplication de code commence à se dessiner avec les multiples instructions “require (‘../vue”
- Créez dans
ControleurUtilisateur.php
une méthodeprivate static function afficherVue(string $cheminVue, array $parametres = []) : void { extract($parametres); // Crée des variables à partir du tableau $parametres require "../vue/$cheminVue"; // Charge la vue }
dont le rôle est d’afficher la vue qui se trouve au chemin
$cheminVue
. L’argument$parametres
sert à définir quelles variables existeront lors de l’exécution de la vue. Par exemple, si vous appelezControleurUtilisateur::afficherVue('utilisateur/detail.php', [ "utilisateurEnParametre" => new Utilisateur("leblancj", "Leblanc", "Juste") ]);
alors
afficherVue
affichera la vuevue/utilisateur/detail.php
, qui aura accès uniquement à la variable$utilisateurEnParametre
qui vautnew Utilisateur("leblancj", "Leblanc", "Juste")
.Remarques :
-
La création des variables est faite par la fonction
extract
fournie par PHP. -
La syntaxe
$parametres = []
dans la déclaration deafficherVue
indique que l’argument$parametres
est optionnel. S’il n’est pas fourni, alors il prendra la valeur par défaut[]
.
Ainsi, on peut appelerControleurUtilisateur::afficherVue('utilisateur/detail.php')
, ce qui est un raccourci pourControleurUtilisateur::afficherVue('utilisateur/detail.php', [])
.
-
- Remplacez tous les
require ('../vue/utilisateur/xxx.php');
par des appels àafficherVue()
. Testez que tout fonctionne.
Vue “ajout d’un utilisateur”
Nous allons créer deux actions afficherFormulaireCreation
et creerDepuisFormulaire
qui doivent respectivement
afficher un formulaire de création d’un utilisateur et effectuer l’enregistrement
dans la base de données.
- Commençons par l’action
afficherFormulaireCreation
qui affichera le formulaire :- Créez la vue
./vue/utilisateur/formulaireCreation.php
qui reprend le code deformulaireCreationUtilisateur.html
du TD3.
Repasser le formulaire en méthodeGET
pour faciliter son débogage. - Ajoutez une action
afficherFormulaireCreation
àControleurUtilisateur.php
qui affiche cette vue.
- Créez la vue
- Testez la page d’affichage du formulaire en appelant l’action
afficherFormulaireCreation
depuisrouteur.php
. - Ajoutez aussi un lien Créer un utilisateur vers l’action
afficherFormulaireCreation
dansliste.php
.
-
Créez l’action
creerDepuisFormulaire
dans le contrôleur qui devra- récupérer les données de l’utilisateur à partir de la query string,
- créer une instance de
ModeleUtilisateur
avec les données reçues, - appeler la méthode
ajouter
du modèle, - appeler la fonction
afficherListe()
pour afficher le tableau de tous les utilisateurs.
-
Testez en appelant l’action
creerDepuisFormulaire
depuisrouteur.php
en donnant le login, le nom et le prénom. - Nous souhaitons maintenant relier l’envoi du formulaire de création à
l’action
creerDepuisFormulaire
pour que l’utilisateur soit bien créé :- La page de traitement du formulaire (l’attribut
action
de<form>
) devra renvoyer versrouteur.php
; -
Afin d’envoyer l’information
action=creerDepuisFormulaire
en plus des informations saisies dans le formulaire, la bonne façon de faire est d’ajouter un champ caché à votre formulaire :<input type='hidden' name='action' value='creerDepuisFormulaire'>
Si vous ne connaissez pas les
<input type='hidden'>
, allez lire la documentation.
- La page de traitement du formulaire (l’attribut
- Testez le tout, c.-à-d. que la création de l’utilisateur depuis le formulaire
(action
afficherFormulaireCreation
) appelle bien l’actioncreerDepuisFormulaire
et que l’utilisateur est bien créée dans la base de données.
Récapitulons
Voici le diagramme de séquence UML (simplifié) du cas d’utilisation “Afficher les détails d’un utilisateur”.
Concernant le PHP, notez la communication entre les 3 entités Modèle, Vue, Contrôleur :
- le Contrôleur interroge le Modèle afin de récupérer les différents éléments concernant les utilisateurs ;
- le Modèle encapsule les informations de connexion à la base de données et plus généralement gère la persistance des données (cette partie sera approfondie et améliorée dans le TD6) ;
- le Contrôleur transmet les éléments à afficher à la Vue ; ces éléments sont préalablement traités, simplifiés afin de ne pas trop exposer la logique du code métier à la Vue
- le rôle de la Vue est d’afficher les informations transmises ; idéalement la Vue ne gère pas les objets métiers de l’application et se contente uniquement d’afficher les infos
- Il n’y a pas d’interactions entre la vue et le modèle dans l’optique d’encourager une bonne séparation des tâches.
Plus globalement :
- Le serveur Web (Apache) existe avant la requête et continuera de vivre après : c’est un démon, c’est-à-dire qu’il tourne en continu pour écouter les requêtes HTTP. Quand il reçoit une requête, il crée un nouveau processus à l’aide d’un fork. C’est ce nouveau processus qui traitera la requête, notamment en exécutant PHP.
- Les scripts PHP sont donc exécutés indépendamment. Ceci implique par exemple que la connexion à la base de données est refaite à chaque exécution (requête HTTP du client).
- Le script PHP renvoie le code HTML de la page Web, ce qui constituera le corps de la réponse HTTP. À partir de ce code HTML, le serveur Web Apache crée et renvoie au client une réponse HTTP complète en ajoutant des en-têtes HTTP.
- On voit que le serveur de base de données est un autre démon.
Quelques détails de lecture des diagrammes de séquence :
- Quand un acteur (bulle grise) apparait au milieu du diagramme de séquence, cela signifie que l’instance correspondante est créée à ce moment-là.
- Un acteur «class» NomDeClasse fait référence au
NomDeClasse
en tant que classe (et pas une instance particulière de celle-ci). Donc cet acteur existe tout le temps et n’est pas créé par un appel de constructeur. Ainsi, sur cet acteur, seules les méthodes statiques peuvent être invoquées.