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.

Structure de nos fichiers

Conventions de nommage :

  1. Les noms des fichiers PHP de déclaration de classe commencent par une majuscule, les autres non.
  2. Les noms des répertoires qui contiennent des déclarations de classe PHP commencent par une majuscule, les autres non.
  3. 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()).

  1. Créez les répertoires Configuration, Controleur, Modele, vue et vue/utilisateur.
  2. Déplacez vos fichiers Utilisateur.php et ConnexionBaseDeDonnees.php dans le répertoire Modele/ en évitant d’utiliser PHPStorm.

    En effet, PHPStorm vous rajouterait des lignes namespace ... et use ... 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 ... et use ... dans de TD5.

  3. Déplacez la classe ConfigurationBaseDeDonnees dans le dossier Configuration en évitant d’utiliser PHPStorm.
  4. Corrigez le chemin relatif du require_once du fichier ConfigurationBaseDeDonnees.php dans ConnexionBaseDeDonnees.php.

  5. Utilisez l’outil de refactoring de votre IDE pour renommer la classe Utilisateur en ModeleUtilisateur.

    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 RefactorRename.

  6. Assurez-vous que PHPStorm n’a pas créé de ligne namespace ..., ni use ... 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 des namespace ... et/ou use ... 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 :

  1. un gros contrôleur unique
  2. un contrôleur par modèle
  3. 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 :

  1. On charge la déclaration de la classe ModeleUtilisateur ;
  2. on se sert du modèle pour récupérer le tableau de tous les utilisateurs avec $utilisateurs = Utilisateur::recupererUtilisateurs();
  3. on appelle alors la vue qui va nous générer la page Web avec require ('../vue/utilisateur/liste.php');

Notes :

  1. Créez le contrôleur Controleur/ControleurUtilisateur.php avec le code précédent.
  2. Testez votre page en appelant l’URL …/Controleur/ControleurUtilisateur.php
  3. 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 :

  1. afficher tous les utilisateurs : action afficherListe
  2. afficher les détails d’un utilisateur : action afficherDetail
  3. afficher le formulaire de création d’un utilisateur : action afficherFormulaireCreation
  4. 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
?>
  1. Modifiez le code de ControleurUtilisateur.php et créez le fichier Controleur/routeur.php pour correspondre au code ci-dessus ;
  2. Testez la nouvelle architecture en appelant la page …/Controleur/routeur.php.
  3. 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();
?>
  1. 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.
  2. 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.
  3. 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 :

  1. Le client demande l’URL …/Controleur/routeur.php?action=afficherListe.
  2. Le routeur récupère l’action donnée par l’utilisateur dans l’URL avec $action = $_GET['action']; (donc $action="afficherListe")
  3. le routeur appelle la méthode statique afficherListe de ControleurUtilisateur.php
  4. ControleurUtilisateur.php se sert du modèle pour récupérer le tableau de tous les utilisateurs ;
  5. 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

  1. 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 dans ModeleUtilisateur). Note : La variable $utilisateur sera initialisée dans le contrôleur plus tard, cf. $utilisateurs dans l’exemple précédent.

  2. Ajoutez une action afficherDetail au contrôleur ControleurUtilisateur.php. Cette action devra récupérer le login donné dans l’URL, appeler la fonction recupererUtilisateurParLogin() du modèle, mettre l’utilisateur visée dans la variable $utilisateur et appeler la vue précédente.

  3. Testez cette vue en appelant la page du routeur avec les bons paramètres dans l’URL.

  4. Ajoutez des liens cliquables <a> sur les logins de la vue liste.php qui renvoient sur la vue de détail de l’utilisateur concernée.

  5. 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 si recupererUtilisateurParLogin() 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 :

  1. la vue a accès à toutes les variables accessibles dans ControleurUtilisateur::afficherListe(),
  2. la manière de procéder du require est très éloignée d’un code orienté-objet propre,
  3. une duplication de code commence à se dessiner avec les multiples instructions “require (‘../vue
  1. Créez dans ControleurUtilisateur.php une méthode
    private 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 appelez

    ControleurUtilisateur::afficherVue('utilisateur/detail.php', [
       "utilisateurEnParametre" => new Utilisateur("leblancj", "Leblanc", "Juste")
    ]);
    

    alors afficherVue affichera la vue vue/utilisateur/detail.php, qui aura accès uniquement à la variable $utilisateurEnParametre qui vaut new 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 de afficherVue indique que l’argument $parametres est optionnel. S’il n’est pas fourni, alors il prendra la valeur par défaut [].
      Ainsi, on peut appeler ControleurUtilisateur::afficherVue('utilisateur/detail.php'), ce qui est un raccourci pour ControleurUtilisateur::afficherVue('utilisateur/detail.php', []).

  2. 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.

  1. Commençons par l’action afficherFormulaireCreation qui affichera le formulaire :
    1. Créez la vue ./vue/utilisateur/formulaireCreation.php qui reprend le code de formulaireCreationUtilisateur.html du TD3.
      Repasser le formulaire en méthode GET pour faciliter son débogage.
    2. Ajoutez une action afficherFormulaireCreation à ControleurUtilisateur.php qui affiche cette vue.
  2. Testez la page d’affichage du formulaire en appelant l’action afficherFormulaireCreation depuis routeur.php.
  3. Ajoutez aussi un lien Créer un utilisateur vers l’action afficherFormulaireCreation dans liste.php.
  1. Créez l’action creerDepuisFormulaire dans le contrôleur qui devra

    1. récupérer les données de l’utilisateur à partir de la query string,
    2. créer une instance de ModeleUtilisateur avec les données reçues,
    3. appeler la méthode ajouter du modèle,
    4. appeler la fonction afficherListe() pour afficher le tableau de tous les utilisateurs.
  2. Testez en appelant l’action creerDepuisFormulaire depuis routeur.php en donnant le login, le nom et le prénom.

  3. Nous souhaitons maintenant relier l’envoi du formulaire de création à l’action creerDepuisFormulaire pour que l’utilisateur soit bien créé :
    1. La page de traitement du formulaire (l’attribut action de <form>) devra renvoyer vers routeur.php;
    2. 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.

  4. Testez le tout, c.-à-d. que la création de l’utilisateur depuis le formulaire (action afficherFormulaireCreation) appelle bien l’action creerDepuisFormulaire 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”.

Diagramme entité association

Concernant le PHP, notez la communication entre les 3 entités Modèle, Vue, Contrôleur :

Plus globalement :

Quelques détails de lecture des diagrammes de séquence :