TD9 – Messages Flash Une autre application des sessions

Ajouter des messages à une vue

Bandeau de messages

Au lieu de créer une vue spécifique comme utilisateurCree.php pour afficher un message au-dessus d’une vue existante, nous allons intégrer à toutes nos pages un bandeau de messages, appelés messages flash.

Les messages pourront être de 4 types : success (vert), info (bleu), warning (jaune) et danger (rouge). Chaque type peut comporter plusieurs messages. Voici 2 exemples dans le cas d’un site qui gèrerait aussi des voitures :

VoitureCreatedSuccess

VoitureCreatedWarning

Pour stocker les messages flash, nous utiliserons des tableaux de tableaux de string, par exemple :

$messagesFlash = [
    "success" => [
        0 => "Message succès"
    ],
    "danger" => [
        0 => "Message danger 1",
        1 => "Message danger 2"
    ]
];
  1. Copiez/collez dans un nouveau dossier TD9 tous les fichiers du dossier TD8.

  2. Les messages peuvent s’afficher sur n’importe quelle page qui utilise vueGenerale.php, que nous allons mettre à jour pour afficher tous les messages existants.

    Dans vueGenerale.php, rajoutez le <div> suivant juste après le <nav> dans le <header>.

    <div>
        <?php
        /** @var string[][] $messagesFlash */
        foreach($messagesFlash as $type => $messagesFlashPourUnType) {
            // $type est l'une des valeurs suivantes : "success", "info", "warning", "danger"
            // $messagesFlashPourUnType est la liste des messages flash d'un type
            foreach ($messagesFlashPourUnType as $messageFlash) {
                echo <<< HTML
                <div class="alert alert-$type">
                   $messageFlash
                </div>
                HTML;
            }
        }
        ?>
    </div>
    

    Prenez le temps de bien comprendre ce code.

  3. Dans ControleurGenerique::afficherVue(), récupérez les messages dans une variable $messagesFlash à partir de l’URL (s’ils y sont présents), juste avant le require de la vue :
    // Avec le if/else ternaire
    $messagesFlash = isset($_REQUEST["messagesFlash"]) ? $_REQUEST["messagesFlash"] : [];
    // Ou de manière équivalente avec l'opérateur "null coalescent"
    // https://www.php.net/manual/fr/migration70.new-features.php#migration70.new-features.null-coalesce-op
    // $messagesFlash = $_REQUEST["messagesFlash"] ?? [];
    
  4. Pour le CSS des messages d’alerte, rajoutez le code suivant à navstyle.css. Le code provient du composant d’alerte du framework CSS Bootstrap :
    /* Bootstrap alerts */
    /* https://getbootstrap.com/docs/3.4/components/#alerts */
    
    .alert {
        padding: 15px;
        margin-bottom: 20px;
        border: 1px solid transparent;
        border-radius: 4px;
    }
    
    .alert-success {
        color: #3c763d;
        background-color: #dff0d8;
        border-color: #d6e9c6;
    }
    
    .alert-info {
        color: #31708f;
        background-color: #d9edf7;
        border-color: #bce8f1;
    }
    
    .alert-warning {
        color: #8a6d3b;
        background-color: #fcf8e3;
        border-color: #faebcc;
    }
    
    .alert-danger {
        color: #a94442;
        background-color: #f2dede;
        border-color: #ebccd1;
    }
    

    On obtient alors par exemple un bloc de succès avec le code :

    <div class="alert alert-success">...</div>
    
  5. Testez les messages flash. Pour cela, appelez une vue en rajoutant le tableau des messages dans le query string de la manière suivante :
    http://localhost/tds-php/TD9/web/controleurFrontal.php?messagesFlash[success][]=Hello+World
    

    ou

    http://localhost/tds-php/TD9/web/controleurFrontal.php?messagesFlash[danger][]=Message+de+danger+1&messagesFlash[danger][]=Message+de+danger+2
    

    Remarque : les messages sont affichés sans htmlspecialchars (cf. TD5). Comme ils proviennent ici de l’URL, n’importe qui peut injecter du HTML ou du JavaScript dans la page (essayez avec messagesFlash[info][]=<h1>Hack</h1>). Cette faille disparaîtra dans la suite du TD, quand les messages flash seront écrits uniquement par le serveur.

Redirection

Il est pratique pour le site de pouvoir rediriger vers une autre page en cas d’erreur ou de succès. Par exemple, si le site gère les voitures et que le client est sur le formulaire de création d’une voiture :

VoitureCreate

S’il saisit une immatriculation existante, le site le redirige vers le formulaire de création avec un message flash d’avertissement :

VoitureCreatedWarning

De même, s’il oublie un champ du formulaire (ce qui ne devrait normalement pas arriver puisque les <input> ont l’attribut required), le site le redirige vers le formulaire de création avec un message flash de danger :

VoitureCreatedDanger

Quand le formulaire est valide, le client est redirigé vers la vue qui liste toutes les voitures avec un message flash de succès :

VoitureCreatedSuccess

Le système de redirection est pratique car il évite la duplication de code. Précédemment, pour que l’action creerDepuisFormulaire affiche la liste des voitures en cas de succès, il fallait récupérer la liste des voitures et appeler la vue voitures/liste.php. Ce code existe déjà dans l’action afficherListe.

À la place, nous allons juste rediriger le client vers l’URL correspondant à l’action afficherListe sans code supplémentaire.

  1. Pour la redirection, nous allons utiliser le code suivant (à encapsuler dans une méthode statique redirigerVersURL(string $url): never du contrôleur générique) :
    header("Location: $url");
    exit();
    

    En effet, quand un navigateur reçoit l’en-tête de réponse Location, il effectue une redirection, c’est-à-dire qu’il lance une requête vers la nouvelle URL comme si on avait cliqué sur un lien. Enfin, exit() termine l’exécution du script courant.

    Note : Le type de retour never indique une fonction qui ne retourne jamais, c.-à-d. qui appelle exit(), lève une exception, ou est une boucle infinie.

  2. Utilisons la redirection dans un premier scénario d’erreur. Dans ControleurUtilisateur::afficherDetail, si le login ne correspond à aucun utilisateur, utilisez la redirection précédente vers l’URL de l’action afficherListe avec le message flash de type danger suivant : Login inconnu.

    Aide : Par exemple, pour rediriger vers l’action bonjour du contrôleur exemple avec la donnée var=a avec un message flash de succès OK, on peut écrire :

     ControleurUtilisateur::redirigerVersURL('controleurFrontal.php?controleur=exemple&action=bonjour&var=a&messagesFlash[success][]=OK');
    
  3. Dans ControleurUtilisateur::supprimer, si l’utilisateur a bien été supprimé, redirigez-le vers l’action afficherListe avec le message flash de succès L'utilisateur a bien été supprimé.

  4. Vérifiez que tout fonctionne et que les messages flash s’affichent bien.

Application des sessions : messages flash

Le client est sur le formulaire de création d’une voiture :

VoitureCreate

Quand le formulaire est valide, le client est redirigé vers la vue qui liste toutes les voitures avec un message flash de succès :

VoitureCreatedSuccess

Nous souhaitons maintenant que ce message ne s’affiche qu’une fois ; si le client rafraîchit la page (F5), alors le message flash disparaît :

VoitureReadAll

Un message flash est détruit dès qu’il est lu : il ne s’affiche donc qu’une seule fois.

Les messages flash seront stockés en session pour pouvoir être écrits lors d’une requête et lus lors de la requête de redirection suivante. Prenons un exemple :

  1. En cliquant sur le bouton Envoyer du formulaire de création de voiture, le client fait une requête à l’action creerDepuisFormulaire. Si la création se passe bien, le contrôleur enregistre en session un message flash de succès, puis redirige le client vers l’affichage des voitures.

  2. Sur la page de l’action afficherListe, le contrôleur va lire les messages flash stockés en session. Ces messages sont affichés sur la page. Selon le principe des messages flash, cette lecture entraînera leur suppression de la session.

  3. Si le client rafraîchit la page, l’action afficherListe va toujours lire les messages flash, mais n’en trouvera pas. Le message disparaîtra donc.

Dans l’exercice suivant, vous allez manipuler les sessions (lors du TD7, vous avez créé une classe Session pour faciliter la manipulation des données). Vous allez notamment stocker et lire le tableau à deux dimensions contenant les messages flash.

Attention, en PHP, les tableaux sont copiés par valeur : quand on récupère un tableau depuis les données de session, on obtient une copie. Si on modifie cette copie, les modifications ne sont pas répercutées dans le tableau stocké en session. Il faut réenregistrer le tableau après modification pour écraser l’ancien :

$tabA = [1,2,3,4];
Session::getInstance()->enregistrer('tableau', $tabA);
$tabB = Session::getInstance()->lire('tableau');
//$tabB => [1,2,3,4]
$tabB[] = 5;
//$tabB => [1,2,3,4,5]
$tabC = Session::getInstance()->lire('tableau');
//$tabC => [1,2,3,4] -> les modifications réalisées sur $tabB ne sont pas répercutées.
Session::getInstance()->enregistrer('tableau', $tabB);
$tabC = Session::getInstance()->lire('tableau');
//$tabC => [1,2,3,4,5]
  1. Implémentez un système de message flash en complétant le squelette suivant dans le fichier src/Lib/MessageFlash.php :

    namespace App\Covoiturage\Lib;
    
    use App\Covoiturage\Modele\HTTP\Session;
    
    class MessageFlash
    {
    
       // Les messages sont enregistrés en session associée à la clé suivante
       private static string $cleFlash = "_messagesFlash";
    
       /**
        * Renvoie le tableau à deux dimensions ne contenant aucun message.
        * Ce tableau sert de valeur initiale lorsque aucun message flash n'est
        * stocké en session, et de valeur de réinitialisation après lecture.
        * @return string[][]
        */
       private static function messagesVides(): array
       {
          return ["success" => [], "info" => [], "warning" => [], "danger" => []];
       }
    
       /**
        * Renvoie le tableau à deux dimensions stocké en session à la clé
        * MessageFlash::$cleFlash.
        *
        * Si cette clé n'est pas présente en session, renvoie le tableau
        * sans message donné par MessageFlash::messagesVides().
        *
        * Remarque : c'est le serveur qui écrit cette donnée en session.
        * Si elle est présente, on peut donc supposer qu'elle a le bon format.
        * @return string[][]
        */
       private static function lireSession(): array
       {
          // À compléter
          return [];
       }
    
       /**
        * Récupère le tableau des messages flashs grâce à MessageFlash::lireSession(),
        * puis ajoute une entrée contenant $message à la fin du sous-tableau
        * associé à la clé $type.
        * Enregistre ensuite ce tableau en session à la clé MessageFlash::$cleFlash.
        */
       public static function ajouter(string $type, string $message): void
       {
          // À compléter
          return;
       }
    
       /**
        * Renvoie le tableau à deux dimensions contenant tous les messages flashs,
        * rangés par type, obtenu grâce à MessageFlash::lireSession().
        *
        * Attention : la lecture doit détruire tous les messages. Avant de renvoyer
        * le tableau, il faut donc enregistrer en session, le tableau sans message 
        * donné par MessageFlash::messagesVides().
        * @return string[][]
        */
       public static function lireTousMessages(): array
       {
          // À compléter
          return [];
       }
    
    }
    
  2. Modifiez ControleurGenerique::afficherVue() pour lire les messages flash depuis cette classe avec :
    $messagesFlash = MessageFlash::lireTousMessages();
    
  3. Testez vos nouveaux messages flash. Pour cela, reprenez les scénarios de la section Redirection. Au lieu de passer les messages flash dans l’URL, vous les ajouterez dans la session avec MessageFlash::ajouter() avant la redirection.

Amélioration du système de redirection

La méthode redirigerVersURL fonctionne, mais n’est pas forcément très pratique à utiliser :

Nous allons mettre en place une méthode plus pratique qui fera tout cela automatiquement :

public static function rediriger(string $controleur = "", string $action = "", array $query = []): never
{
    $tableauQueryString = [];
    if($controleur !== "") {
        $tableauQueryString[] = "controleur=$controleur";
    }
    if($action !== "") {
        $tableauQueryString[] = "action=$action";
    }
    foreach ($query as $name => $value) {
        $valueURL = rawurlencode($value);
        $tableauQueryString[] = "$name=$valueURL";
    }
    $queryString = !empty($tableauQueryString) ? ("?" . join("&", $tableauQueryString)) : "";
    $url = "controleurFrontal.php" . $queryString;
    ControleurGenerique::redirigerVersURL($url);
}

Grâce à cette nouvelle méthode, il sera possible de rediriger plus facilement l’utilisateur.

Par exemple, si on souhaite rediriger vers l’action monAction du contrôleur exemple :

//Redirige vers controleurFrontal.php?controleur=exemple&action=monAction
ControleurGenerique::rediriger('exemple', 'monAction');

Le tableau $query est un tableau associatif qui permet de venir compléter le query string avec des données utiles à l’action ciblée (un peu comme ce qu’on fait déjà avec afficherVue et les données transmises par le contrôleur aux vues).

Par exemple, si on souhaite fournir des données à l’action de l’exemple précédent :

//Redirige vers controleurFrontal.php?controleur=exemple&action=monAction&varA=valeurA&varB=5
ControleurGenerique::rediriger('exemple', 'monAction', ["varA" => "valeurA", "varB" => 5]);
  1. Ajoutez la méthode rediriger au contrôleur générique, en copiant le code ci-dessus.

  2. Prenez bien le temps de comprendre ce que fait cette méthode, ligne par ligne. Demandez à votre enseignant chargé de TD si vous n’êtes pas sûr de comprendre certaines parties.

  3. Remplacez les appels à redirigerVersURL par rediriger dans les actions ControleurUtilisateur::afficherDetail et ControleurUtilisateur::supprimer.

  4. Testez et vérifiez que tout fonctionne et que vos messages flash s’affichent toujours.

Utilisation des messages flash

Utilisez les messages flash pour enlever toutes les vues qui affichaient un message puis incluaient une autre vue.

  1. En particulier, supprimez les vues désormais inutiles comme utilisateurCree.php, utilisateurConnecte.php, utilisateurDeconnecte.php, utilisateurMisAJour.php et utilisateurSupprime.php. Faites de même pour les vues trajets et éventuellement les vues concernant les passagers si vous aviez fait certaines questions bonus.

  2. Remplacez ces vues par des messages flash de type success dans les actions concernées.

    • Par exemple, en cas de succès de création d’un utilisateur, on pourrait ajouter un message flash de succès L’utilisateur a bien été créé puis rediriger vers l’action afficherListe du contrôleur utilisateur.

    • Ou bien, pour la connexion, on pourra ajouter un message flash de succès Connexion réussie puis rediriger vers l’action afficherDetail du contrôleur utilisateur en passant le login de l’utilisateur dans l’URL grâce au tableau associatif $query de la méthode rediriger.

  3. L’action enregistrerPreference() doit maintenant rediriger vers l’action par défaut du contrôleur par défaut, que l’on obtient sans indiquer d’action ni de contrôleur dans la méthode rediriger, avec un message flash de succès La préférence de contrôleur est enregistrée.

Dans votre site, nous allons remplacer tous les appels à afficherErreur (qui affichent la vue d’erreur) par des redirections avec messages flash.

Nous réserverons les messages flash de type danger pour les erreurs qui ne sont censées se produire que si le client a essayé de hacker le site (accès à une page qu’on ne lui a pas proposée, donnée de formulaire manquante alors qu’elle était required), et les messages flash de type warning pour les erreurs de l’utilisateur.

  1. Action afficherDetail

    • s’il manque le login, redirigez vers la liste des utilisateurs avec un message flash Login manquant de type danger.
  2. Action creerDepuisFormulaire

    • s’il manque des données, redirigez vers le formulaire de création avec un message flash Données manquantes de type danger.

    • si les deux champs mot de passe ne coïncident pas, redirigez vers le formulaire de création avec un message flash Mots de passe distincts de type warning.

    • si le format de l’email n’est pas valide, redirigez vers le formulaire de création avec un message flash Adresse email invalide de type warning.

    • si la sauvegarde échoue (par exemple parce que le login est déjà pris), redirigez vers le formulaire de création avec un message flash Échec de la sauvegarde de type warning.

  3. Action afficherFormulaireMiseAJour :
    • s’il manque le login, redirigez vers la liste des utilisateurs avec un message flash Login manquant de type danger.

    • si l’utilisateur qui modifie le profil n’est pas l’utilisateur à qui appartient le profil ou un administrateur, redirigez vers la liste des utilisateurs avec un message flash La mise à jour n’est possible que pour l’utilisateur connecté de type danger.

    • si l’utilisateur n’existe pas, redirigez vers la liste des utilisateurs avec un message flash Utilisateur inexistant de type danger.

  4. Action mettreAJour :

    • s’il manque des données, redirigez vers la liste des utilisateurs avec un message flash Données manquantes de type danger.

    • si l’utilisateur n’existe pas, redirigez vers la liste des utilisateurs avec un message flash Utilisateur inexistant de type danger.

    • si l’utilisateur qui modifie le profil n’est pas l’utilisateur à qui appartient le profil ou un administrateur, redirigez vers la liste des utilisateurs avec un message flash La mise à jour n’est possible que pour l’utilisateur connecté de type danger.

    • si les 2 nouveaux mots de passe ne coïncident pas, redirigez vers le formulaire de mise à jour en indiquant le login dans l’URL (toujours grâce au paramètre $query de rediriger) avec un message flash Mots de passe distincts de type warning.

    • si l’ancien mot de passe n’est pas correct et que l’utilisateur n’est pas administrateur, redirigez vers le formulaire de mise à jour en indiquant le login dans l’URL avec un message flash Ancien mot de passe erroné de type warning.

    • si le format du nouvel email n’est pas valide, redirigez vers le formulaire de mise à jour en indiquant le login dans l’URL avec un message flash Adresse email invalide de type warning.

  5. Action supprimer :
    • s’il manque le login, redirigez vers la liste des utilisateurs avec un message flash Login manquant de type danger.

    • si l’utilisateur qui supprime l’utilisateur n’est pas l’utilisateur à qui appartient le profil ou un administrateur, redirigez vers la liste des utilisateurs avec un message flash La suppression n’est possible que pour l’utilisateur connecté de type danger.

    • si la suppression a échoué, par exemple parce que l’utilisateur n’existe pas, redirigez vers la liste des utilisateurs avec un message flash Utilisateur inexistant de type danger.

  6. Action connecter :
    • si le login ou le mot de passe ne sont pas transmis, redirigez vers le formulaire de connexion avec un message flash Login et/ou mot de passe manquant de type danger.

    • si l’utilisateur n’existe pas ou que le mot de passe est incorrect, redirigez vers le formulaire de connexion avec un message flash Login et/ou mot de passe incorrect de type warning.

    • si l’utilisateur n’a pas validé son adresse email, redirigez vers le formulaire de connexion avec un message flash Votre adresse email n’est pas validée de type warning.

  7. Action validerEmail :

    • s’il manque le login et/ou le nonce, redirigez vers la liste des utilisateurs avec un message flash Données manquantes de type danger.

    • en cas de succès de la validation de l’email, redirigez vers la page de détail de cet utilisateur (en passant son login dans l’URL grâce au paramètre $query de rediriger) avec un message flash Adresse email validée de type success.

    • en cas d’échec de la validation de l’email, redirigez vers la liste des utilisateurs avec un message flash Erreur lors de la validation de type danger.

  8. Remplacez les autres appels à afficherErreur restants (s’ils n’ont pas été listés dans cet exercice), notamment ceux des autres contrôleurs. Par exemple :
    • à la création d’un trajet, si le login du conducteur ne correspond à aucun utilisateur, redirigez vers le formulaire de création avec un message flash Conducteur inconnu de type warning ;
    • si vous avez codé le contrôleur passager (bonus du TD6), vérifiez la présence des paramètres et l’existence du trajet, et traitez l’échec de l’inscription (login inconnu, passager déjà inscrit).
  9. Normalement, à ce stade, vous ne devriez plus avoir d’appel à afficherErreur dans vos contrôleurs. Nous allons donc, a priori, pouvoir supprimer cette méthode. Cependant, elle est encore utilisée à un dernier endroit : le point d’entrée de notre site, controleurFrontal.php. Nous pouvons mettre en place deux solutions :

    • Soit remplacer les appels à afficherErreur dans controleurFrontal.php par un ajout de messages flash de type danger et une redirection vers le contrôleur et l’action par défaut (en appelant ControleurGenerique::rediriger sans paramètres).
      Attention : ne redirigez pas vers le contrôleur demandé (rediriger($controleur)). Par exemple, pour controleurFrontal.php?controleur=preference, l’action par défaut afficherListe n’existe pas dans ce contrôleur : la redirection tournerait en boucle.

    • Soit créer une vue globale erreur.php dans le dossier src/vue puis une méthode statique afficherErreur($message) dans ControleurGenerique qui appellera cette vue en lui passant le message d’erreur (proche de ce que nous avions jusqu’ici). Il suffit alors d’utiliser ControleurGenerique::afficherErreur dans controleurFrontal.php à la place de ce qui était utilisé jusqu’ici.

    Choisissez une des deux solutions et implémentez-la. Ensuite, vérifiez dans chaque contrôleur (sauf le contrôleur générique) que la méthode afficherErreur n’est plus utilisée et supprimez-la.

  10. Tentez d’accéder à un contrôleur et/ou une action qui n’existe pas et vérifiez que le message d’erreur est affiché correctement. Testez aussi controleurFrontal.php?controleur=preference (sans action).