TD5 – Architecture MVC avancée 1/2 Contrôleur frontal, échappement du HTML, vues modulaires, CRUD

Nous allons continuer de développer notre site-école de covoiturage. Au fur et à mesure que le projet grandit, nous allons bénéficier du patron d’architecture MVC qui va nous faciliter la tâche.

Le but des TDs 5 & 6 est donc d’avoir un site qui propose une gestion minimale des voitures, utilisateurs et trajets proposés en covoiturage. En attendant de pouvoir gérer les sessions d’utilisateur, nous allons développer l’interface “administrateur” du site.

Remise en route

Lors du TD4, nous avons commencé à utiliser l’architecture MVC. Le code était découpé en trois parties :

  1. Le modèle (e.g. Modele/ModeleVoiture.php) est une bibliothèque des fonctions permettant de gérer les données, i.e. l’interaction avec la base de données dans notre cas. Cette bibliothèque sera utilisée par le contrôleur.

  2. Les vues (e.g. vue/voiture/liste.php) ne doivent contenir que les parties du code qui écrivent la page Web. Ces scripts seront appelés par le contrôleur qui s’en servira comme d’un outil pour générer la page Web ;

  3. Le contrôleur est la partie principale du script PHP. Dans notre cas, il est composé de deux parties :

    1. le routeur (e.g. Controleur/routeur.php) est la page que l’utilisateur demande pour se connecter au site. C’est donc le script PHP de cette page qui est exécuté. Cette page est chargée de récupérer l’action envoyée avec la demande de la page et d’appeler le bon code du contrôleur correspondant.

    2. la partie Voiture du contrôleur (e.g. Controleur/ControleurVoiture.php) contient le code source des actions. C’est donc ici qu’est présente la logique du site Web : on y appelle le modèle pour récupérer/enregistrer des données, on traite ces données, on appelle les vues pour écrire la page Web…

Dessinez sur papier un schéma qui explique comment le contrôleur (le routeur et la partie Voiture), le modèle et la vue interagissent pour créer la page qui correspond par exemple à l’action afficherDetail. Ce schéma doit représenter les différents fichiers exécutés par PHP, les regrouper par composant MVC, et indiquer à l’aide de flèches l’ordre d’exécution.

Préparez-vous à l’expliquer à votre chargé de TD quand il passera le corriger.

Réorganisation du site

Limitation des pages accessibles

L’organisation actuelle du site pose un problème majeur : un client du site Web est censé accéder au site avec une requête Controleur/routeur.php. Mais rien ne garantit qu’il n’essayera pas d’accéder aux autres fichiers PHP “internes”. Nous allons donc séparer les fichiers PHP dans des dossiers différents en fonction de s’ils doivent être accessibles sur le Web.

  1. Renommez et déplacez le fichier Controleur/routeur.php pour qu’il devienne web/controleurFrontal.php.

    Note : Ce script s’appelle contrôleur frontal (front controller en anglais) puisque c’est la partie visible de notre site.

  2. Déplacez les dossiers Configuration, Controleur, Modele et vue dans un dossier src. Ce déplacement casse le site Web. Nous allons le réparer dans le prochain exercice.

Réparer les inclusions de fichiers du site

Lorsque l’on a déplacé la page d’accueil vers controleurFrontal.php, tous nos require_once ont été décalés. En effet, le problème quand on utilise des chemins de fichiers relatifs dans nos require_once, c’est que comme ils sont tous copiés/collés dans routeur.php, ils utilisent le dossier du routeur comme base.

Prenons l’exemple de require_once '../Configuration/ConfigurationBaseDeDonnees.php' dans ConnexionBaseDeDonnees.php :

Pour éviter ce comportement qui porte à confusion, nous allons utiliser des chemins de fichiers absolus. Pour ce faire, nous utiliserons la constante __DIR__ qui contient le chemin absolu du dossier contenant le fichier actuel. Par exemple, nous pouvons écrire dans ConnexionBaseDeDonnees.php

// __DIR__ renvoie vers le dossier contenant ConnexionBaseDeDonnees.php
// c-à-d ici __DIR__ égal "/chemin_du_site/Modele"
require_once __DIR__ . '/../Configuration/ConfigurationBaseDeDonnees.php';

À partir de maintenant, nous n’utiliserons plus de require_once avec des chemins relatifs. Il va donc falloir changer ceux qui existent déjà.

Corrigez tous les require_once pour que le site remarche. Si besoin, changez les liens de liste.php et l’attribut action du formulaire formulaireCreation.php.

Maintenant que le site remarche et que les scripts accessibles sur le Web sont isolées dans des dossiers différents, nous allons pouvoir appliquer la restriction d’accès.

  1. Nous allons indiquer au serveur Web Apache que les fichiers ne sont pas accessibles sur internet par défaut. Pour ceci, créez un fichier .htaccess à la racine de votre site TD5 avec le contenu suivant

    Require all denied
    
  2. Pour indiquer que les fichiers du dossier web sont accessibles, créez un fichier web/.htaccess avec le contenu suivant

    Require all granted
    
  3. Vérifiez que l’accès par internet aux scripts autres que web/controleurFrontal.php affiche une page Web Forbidden You don't have permission to access this resource.

    Note : Si votre fichier .htaccess n’a pas d’effet et que vous êtes sur votre machine, il se peut qu’il faille configurer Apache autrement.

Chargement automatique des classes

Nous venons de faire l’expérience des limites des chemins relatifs. Dans le monde professionnel du PHP, on utilise le chargement automatique de classe (autoloading en anglais) : quand PHP doit utiliser une classe qu’il ne connait pas, il va charger le fichier de déclaration de cette classe.

Vous avez déjà utilisé ce mécanisme en Java sans le savoir. En effet, vous n’avez jamais inclus de fichier de déclaration de classe en Java avec des require_once comme en PHP. Alors, comment fait Java pour savoir quel fichier inclure ?

Le chemin du fichier est directement lié au nom de classe qualifié, c.-à-d. du nom de classe précédé du nom de package. Par exemple, le fichier Java src/main/java/fr/umontpellier/iut/svg/SVG.java

package fr.umontpellier.iut.svg;

public class SVG { }

contient la déclaration de la classe dont le nom qualifié est fr.umontpellier.iut.svg.SVG. On imagine bien comment Java s’est servi du nom de classe qualifié pour trouver l’adresse du fichier de déclaration.

Les principaux avantages du chargement automatique de classe sont :

Espaces de noms

Avant d’utiliser le chargement automatique, nous avons besoin de préciser nos noms de classes avec des espaces de noms (namespace en anglais). C’est l’équivalent des package en Java.

  1. Rajoutez
    namespace App\Covoiturage\Configuration;
    

    au début de src/Configuration/ConfigurationBaseDeDonnees.php.

    Explication : La déclaration namespace regroupe toutes les classes (et fonctions) déclarées dans le fichier dans l’espace de nom App\Covoiturage\Configuration, ce qui a pour effet de rajouter un préfixe à leur nom. Ainsi, la classe déclarée dans ConfigurationBaseDeDonnees.php s’appelle maintenant App\Covoiturage\Configuration\ConfigurationBaseDeDonnees.

    Attention : Les espaces de nom utilisent des antislashs \, tandis que les chemins de fichiers Linux/Mac utilisent des slashs /.

  2. Le site est de nouveau cassé : ConnexionBaseDeDonnees.php ne connait pas la classe ConfigurationBaseDeDonnees. En effet, cette classe s’appelle désormais App\Covoiturage\Configuration\ConfigurationBaseDeDonnees.
    Complétez le nom de la classe ConfigurationBaseDeDonnees dans ConnexionBaseDeDonnees.php. Le site Web doit refonctionner.

    Note : Vous ne devez pas toucher aux noms de fichiers dans les require_once, mais plutôt changer le nom de classe ConfigurationBaseDeDonnees dans les appels à des méthodes statiques.

  3. Vous conviendrez volontiers que ce nom de classe à rallonge est pénible. Nous allons utiliser un alias à la place :

    // ConfigurationBaseDeDonnees est un raccourci pour App\Covoiturage\Configuration\ConfigurationBaseDeDonnees
    use App\Covoiturage\Configuration\ConfigurationBaseDeDonnees as ConfigurationBaseDeDonnees; 
    // ou syntaxe équivalente plus rapide 
    use App\Covoiturage\Configuration\ConfigurationBaseDeDonnees;
    

    Raccourcissez les noms de classe dans ConnexionBaseDeDonnees.php grâce à cet alias.

    Remarques :

    • use est similaire à import en Java.
    • PhpStorm peut faire ce travail à votre place. Par exemple, quand il ne connait pas la classe Configuration, il la surligne pour indiquer un warning. Lorsque votre curseur est sur la ligne du warning, une ampoule apparait pour vous proposer des solutions rapides (ou faites Alt+Entrée). Choisissez la solution Import Class.

PSR-4 : Autoloading Standard

Le groupe PHP-FIG (PHP Framework Interoperability Group) pour l’interopérabilité de PHP travaille pour standardiser la pratique de PHP. Ce travail vise notamment à ce que les différents composants ou framework PHP puissent bien communiquer entre eux.

Parmi les recommandations de standards PHP (PSR en anglais) les plus importants, on trouve :

PhpStorm peut formater votre code en suivant les standards de style PSR-1 et PSR-12 en allant dans le menu Code > Reformat Code (ou en tapant Ctrl+Alt+L).

Dans la suite, nous allons vous fournir une classe Psr4AutoloaderClass qui implémente un chargeur automatique de classe suivant le standard PSR-4. En pratique, après avoir initialisé la classe avec

$chargeurDeClasse = new App\Covoiturage\Lib\Psr4AutoloaderClass();
$chargeurDeClasse->register();

vous pourrez enregistrer une association entre un espace de nom et un dossier

$chargeurDeClasse->addNamespace('App\Covoiturage', __DIR__ . '/../src');

Vous pouvez maintenant utiliser n’importe quelle classe dont l’espace nom commence par App\Covoiturage et Psr4AutoloaderClass chargera le fichier de déclaration de classe correspondant avec un require_once. Par exemple, si vous exécutez maintenant

use App\Covoiturage\Configuration\ConfigurationBaseDeDonnees;
echo ConfigurationBaseDeDonnees::getPort();

alors Psr4AutoloaderClass exécutera pour vous

require_once(__DIR__ . '/../src/Configuration/ConfigurationBaseDeDonnees.php')

Le chemin de fichier est déterminé par Psr4AutoloaderClass en utilisant l’association déclarée précédemment avec addNamespace pour remplacer 'App\Covoiturage' par __DIR__ . '/../src' dans le nom de classe qualifié de ConfigurationBaseDeDonnees.

  1. Créez le dossier src/Lib (attention à la majuscule). Enregistrez le fichier Psr4AutoloaderClass.php directement à l’emplacement src/Lib/Psr4AutoloaderClass.php.

    Attention : Un bug apparaît si vous vous servez de PhpStorm pour déplacer Psr4AutoloaderClass et le changer de dossier. Il est donc important d’enregistrer Psr4AutoloaderClass directement dans le bon dossier.

  2. Au début du contrôleur frontal, incluez ce fichier à l’aide d’un require_once. Utilisez un chemin de fichier absolu avec __DIR__ comme vu précédemment.

  3. Rajoutez le code suivant dans le contrôleur frontal juste avant de traiter les actions :
    // initialisation en activant l'affichage de débogage
    $chargeurDeClasse = new App\Covoiturage\Lib\Psr4AutoloaderClass(true);
    $chargeurDeClasse->register();
    // enregistrement d'une association "espace de nom" → "dossier"
    $chargeurDeClasse->addNamespace('App\Covoiturage', __DIR__ . '/../src');
    

    En résumé, ce code dit au système d’autoloading de PHP que les classes dont l’espace de nom commence par App\Covoiturage se trouvent dans le dossier src.

  4. Nous allons enfin pouvoir utiliser l’autoloader. Comme expliqué précédemment, la classe App\Covoiturage\Configuration\ConfigurationBaseDeDonnees sera cherchée dans le fichier src/Configuration/ConfigurationBaseDeDonnees.php.
    Dans ConnexionBaseDeDonnees.php, enlevez le require_once de la classe Configuration.
    Le site Web doit refonctionner.

  5. Répétez ce processus pour enlever tous les require_once de fichier de déclaration de classe (sauf pour Psr4AutoloaderClass) :
    • ajout de namespace dans chaque classe,
    • utilisation d’alias pour faire référence à cette classe,
    • suppression des require_once.

    Nous vous conseillons de procéder classe par classe, dans l’ordre suivant : ConnexionBaseDeDonnees, ModeleVoiture puis ControleurVoiture.

    Attention : La classe PDO dans ConnexionBaseDeDonnees.php est comprise comme App\Covoiturage\Modele\PDO à cause du namespace App\Covoiturage\Modele. Or son nom complet est \PDO. Deux solutions possibles :

    • Ajoutez use \PDO as PDO; pour que PHP sache que PDO est dans l’espace de nom global.
    • Ou spécifiez que PDO est dans l’espace de nom global en appelant la classe \PDO.
  6. Maintenant que vous avez compris le principe de Psr4AutoloaderClass, vous pouvez si vous le souhaitez désactiver son affichage de débogage :
    $loader = new App\Covoiturage\Lib\Psr4AutoloaderClass(false);
    

Sécurité des vues

Nous allons apprendre pourquoi nous devons faire attention lorsque nous remplaçons une variable PHP par sa valeur dans l’écriture de la page HTML. Vous allez voir que les raisons sont assez similaires au problème derrière les injections SQL.

Prenons l’exemple de notre vue detail.php qui écrit entre autre

echo "<p> Voiture {$v->getImmatriculation()} </p>";

Que se passe-t-il si l’utilisateur a rentré du code HTML à la place d’une immatriculation ?

Créez une voiture d’immatriculation <h1>Hack et regardez comment elle s’affiche. Inspectez le code source HTML correspondant pour comprendre ce qu’il s’est passé.

L’immatriculation est comprise comme du code HTML et est donc interprétée. Ce comportement est non désiré et peut carrément être dangereux, notamment si l’utilisateur se met à écrire du JavaScript.

Échappement dans du HTML

Pour éviter cela, il faut faire attention aux caractères protégés du HTML. Voici la liste des caractères qui font la différence entre du texte pur et du code HTML :

  1. les chevrons < et > car ils délimitent les balises HTML ;
  2. les guillemets simples ' ou doubles " car ils délimitent les valeurs des attributs ;
  3. L’esperluette & car elle sert à échapper les caractères. Par exemple, le code HTML &amp; sert à afficher une esperluette &.

Ces caractères spéciaux doivent être échappés dans les vues pour que le texte s’affiche bien mais ne risque pas de changer la structure du document HTML. Voici comment échapper ces caractères :

&lt; &gt; &amp; &quot; &apos;
< > & " '

Bonne nouvelle : PHP fait ceci pour nous avec la fonction htmlspecialchars. Par exemple, le code

echo htmlspecialchars('<a href="test">Test</a>');

renvoie

&lt;a href=&quot;test&quot;&gt;

Le remplacement des caractères spéciaux a bien eu lieu.

Du coup, il faut utiliser htmlspecialchars à chaque fois que l’on écrit une variable non sûre (par ex. provenant de l’utilisateur) comme texte de la page HTML ou comme attribut d’une balise.

$nom = "<h1>Danger ! </h1>";
echo "Page personnelle de ". $nom; // Danger !  
echo "Page personnelle de ". htmlspecialchars($nom); // Écriture sécurisée
$valeurDefaut = '"><script>alert("Danger!");';
echo '<input type="text" value="' . $valeurDefaut . '">'; // Danger !  
echo '<input type="text" value="' . htmlspecialchars($valeurDefaut) . '">'; // Écriture sécurisée  
  1. Changer donc toutes vos vues pour appliquer la fonction htmlspecialchars à toutes les variables PHP qui se trouvent à un endroit où du code HTML pourrait être interprété. L’endroit typique est dans les zones de texte.
    Nous vous conseillons de créer des variables temporaires pour stocker le texte échappé, par exemple $immatriculationHTML, puis d’afficher ces variables.

  2. Vérifiez que votre voiture d’immatriculation <h1>Hack s’affiche maintenant correctement et ne créé plus de balise HTML <h1>. Allez voir dans le code source comme l’immatriculation a été échappée.

Échappement des URLs

De la même manière, il faut encoder les URLs pour éviter d’en changer le sens lorsque l’on insère une donnée fournie par l’utilisateur. Par exemple, nous allons devoir échapper les caractères ? et = puisqu’ils permettent de passer de l’information dans l’URL avec le format query string.

Pour information, la liste des caractères réservés des URLs sont :/?#[]@!$&'()*+,;=. Nous allons donc utiliser la fonction rawurlencode pour échapper les variables PHP qui interviennent dans des URLs.

  1. Créez une voiture d’immatriculation &a=b en utilisant votre action afficherFormulaireCreation ;

  2. Observez que le lien vers la vue de détail de cette voiture ne marche pas. Pourquoi ?

  3. Changer la vue liste.php pour qu’elle encode à l’aide de rawurlencode la variable PHP correspondant à l’immatriculation.
    Attention : Il ne faut pas encoder l’immatriculation déjà échappée pour le HTML. Il faut créer deux variables : une immatriculation pour le HTML et une pour les URLs.

  4. Testez que le lien vers la vue de détail remarche.

Source : RFC 3986 sur les URI

Vues modulaires

En l’état, certains bouts de code de nos vues se retrouvent dupliqués à de multiples endroits. Les prochaines questions vont vous aider à réorganiser le code pour éviter les redondances en vue d’améliorer la maintenance du code et son debuggage.

Mise en commun de l’en-tête et du pied de page

Actuellement, les scripts de vues sont chargées d’écrire l’ensemble de la page Web, du <!DOCTYPE HTML><html>... jusqu’au </body></html> . C’est problématique car cela nous empêche de mettre facilement deux vues bout à bout. Voyons cela sur un exemple.

Supposez que l’on souhaite que notre vue de création (action creerDepuisFormulaire) de voiture affiche “Votre voiture a bien été créée” puis la liste des voitures. Il serait donc naturel d’écrire le message puis d’appeler la vue liste.php. Mais comme cette dernière vue écrivait la page HTML du début à la fin, on ne pouvait rien y rajouter au milieu !

Décomposons nos pages Web en trois parties : le header (en-tête), le body (corps ou fil d’Ariane) et le footer (pied de page). Dans le site final de l’an dernier, on voit bien la distinction entre les 3 parties. On note aussi que le header et le footer sont communs à toutes nos pages.

Au niveau du HTML, le header correspond à la partie suivante (nous créerons l’en-tête et le pied de page plus tard).

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Liste des trajets</title>
    </head>
    <body>
        <header>
            <nav>
                <!-- Le menu de l'en-tête -->
            </nav>
        </header>

le body à la partie :

        <main>
            <h1>Liste des trajets:</h1>
            <ol>
                <li>...</li>
                <li>...</li>
            </ol>
        </main>

et le footer à la partie :

        <footer>
            <p>Copyleft Romain Lebreton</p>
        </footer>    
    </body>
</html>

Nous allons donc changer nos vues pour qu’elles n’écrivent plus que le corps de la page. Enfin une vue spéciale, appelée vue générique, chargera l’une de ces vues “corps” en l’incluant dans l’en-tête et le pied de page communs.

  1. Créer une vue générique TD5/vue/vueGenerale.php avec le code suivant. La fonction de vueGenerale.php est de charger un en-tête et un pied de page communs, ainsi que la vue dont le nom de fichier est stocké dans la variable $cheminVueBody (et le titre de page contenu dans $pagetitle).

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="UTF-8">
          <title><?php echo $pagetitle; ?></title>
       </head>
       <body>
          <header>
                <nav>
                    <!-- Votre menu de navigation ici -->
                </nav>
          </header>
          <main>
                <?php
                require __DIR__ . "/{$cheminVueBody}";
                ?>
          </main>
          <footer>
          </footer>
       </body>
    </html>
    
  2. Dans vos vues existantes, supprimer les parties du code correspondant aux header et footer.

  3. Reprendre l’action afficherListe du contrôleur pour afficher la vue vueGenerale.php avec les paramètres supplémentaires "pagetitle" => "Liste des voitures", "cheminVueBody" => "voiture/liste.php".

  4. Testez votre action afficherListe. Regardez le code source de la page Web pour vérifier que le HTML généré est correct.

  5. Modifiez les autres actions et testez votre site.

Nous allons bénéficier de notre changement d’organisation pour rajouter un header et un footer (minimalistes) à toutes nos pages.

  1. Modifier la vue vueGenerale.php pour ajouter en en-tête de page une barre de menu, avec trois liens vers les différents contrôleurs :

    <nav>
       <ul>
          <li>
             <a href="controleurFrontal.php?action=afficherListe&controleur=voiture">Gestion des voitures</a>
          </li><li>
             <a href="controleurFrontal.php?action=afficherListe&controleur=utilisateur">Gestion des utilisateurs</a>
          </li><li>
             <a href="controleurFrontal.php?action=afficherListe&controleur=trajet">Gestion des trajets</a>
          </li>
       </ul>
    </nav>
    
  2. Modifier la vue vueGenerale.php pour rajouter un pied de page comme

    <p>
      Site de covoiturage de ...
    </p>
    
  3. Rajoutez un style CSS minimaliste à votre page Web. Ce style sera mis dans un dossier css. Où mettre ce dossier css sachant que nous interdisons l’accès internet à certaines parties du dossier TD5 ?

    Une façon de faire est de créer un dossier TD5/ressources qui sera accessible sur internet (copiez le .htaccess de web), et qui contiendra le dossier css, mais aussi plus tard des dossiers img d’images et js pour le JavaScript.

  4. Ce fichier CSS rajoute aussi un style pour les formulaires. Pour l’appliquer, changez formulaireCreation.php pour qu’un champ de formulaire s’obtienne par exemple avec

     <p class="InputAddOn">
         <label class="InputAddOn-item" for="immat_id">Immatriculation&#42;</label>
         <input class="InputAddOn-field" type="text" placeholder="Ex : 256AB34" name="immatriculation" id="immat_id" required>
     </p>
    

Concaténer des vues

Notre réorganisation nous permet aussi de résoudre le problème soulevé plus tôt à propos de la vue de création d’une voiture.

Nous souhaitons créer une vue voitureCreee.php qui affiche le message

<p>La voiture a bien été créée !</p>

avant de faire un require de liste.php puisque cette vue sert à écrire la liste des voitures. Ceci donnerait le visuel suivant.

VoitureCreate

  1. Créez la vue voitureCreee.php comme expliqué ci-dessus, en utilisant le concept de vue modulaire.
    Remarque : La vue voitureCreee.php doit faire deux lignes maintenant.

  2. Changez l’action creerDepuisFormulaire du contrôleur pour appeler cette vue.
    Attention : Il faut initialiser la variable $voitures contenant le tableau de toutes les voitures afin qu’elle puisse être affichée dans la vue.

  3. Comme vous développez un site Web, il faut vérifier régulièrement sa conformité HTML et CSS. Faites-le maintenant.