Un messager pour collecter les messages à l’utilisateur.

Si vous avez lu l’article « De la granularité des actions » vous avez pu approcher les avantages et les inconvénients d’utiliser des actions atomiques. Un des problèmes induit par cette approche et la gestion des messages à l’utilisateur. Ce n’est pas propre à la méthode, c’est en fait dû à l’utilisation des redirections. Ce qui suit concerne donc toute application qui utilise les redirections. La problématique est relativement simple : si dans une action A j’ai besoin d’afficher un message à l’utilisateur et que cette action fait une redirection je me retrouve avec un conflit. Si j’affiche le message je ne peux pas rediriger la page. Et si je redirige, je ne peux pas afficher le message.

Contourner le problème

Une façon simple de faire, est d’abandonner la redirection et d’afficher le message avec un lien sur la page à atteindre. Cette approche est pour le moins simpliste et facile à mettre en œuvre. Elle implique tout de même l’intervention de l’utilisateur. De plus les messages étant affichés dans une page à part, la lisibilité n’est pas toujours au rendez-vous. Imaginez que votre application vérifie un formulaire complexe et vous affiche une liste de trente messages. Et ensuite vous cliquez sur le lien pour voir le formulaire. Pas évident de se souvenir de tous les messages pour corriger le formulaire. Il en va de même si les redirections se font en cascade.

Garder les messages sous le coude

Une autre approche consiste à se poser la question : quand afficher les messages ? En effet le problème vient des redirections et il peut y en avoir plusieurs. Mais quoi qu’il arrive il va bien y avoir un affichage, car sinon mon application part dans une boucle folle. Donc si je garde mon message, je pourrai l’afficher dès qu’une action ne fait plus de redirection. Donc lorsque j’ai un message à afficher je le garde sous le coude dans la session et lorsque j’arrive sur une action qui affiche, je récupère mon message, le donne à la vue et le supprime de la session. Si je reprends mon processus décrit dans l’article « De la granularité des actions » j’ai :

  • Afficher la liste

  • Rechercher les données de l’enregistrement à éditer
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données

Dans cet enchaînement mon problème va se rencontrer dans les actions « rechercher les données » « vérifier les données » et « traiter les données ». Dans ces actions je dois donc ajouter dans la session les messages lorsqu’il y en a.

         $messenger = new Zend_Session_Namespace('messenger');
         $messenger = 'invalid datas';

Si la recherche est infructueuse, je reviens sur l’affichage de la liste. Je dois donc dans cette action récupérer les messages dans la session, les donner à la vue et les effacer. De même si les données de formulaire sont invalides, ou si le traitement échoue, je reviens sur l’affichage du formulaire. Dans celui-ci je vais aussi devoir récupérer les messages dans la session, les donner à la vue et les effacer. C’est le traitement que je donnais dans l’exemple de cet article.

      $messenger = new Zend_Session_Namespace('messenger');
      $this->view->messages = $messenger;
      unset($messenger);

Généraliser le messager

On voit aisément que tout cela va être très répétitif. Il est alors intéressant de généraliser ce comportement. Pour toutes les actions, nous avons la déclaration du namespace dans la session. Si nous plaçons cette déclaration dans la méthode init de tous nos contrôleurs, toutes nos actions bénéficieront de cette déclaration. Toutes les actions qui génèrent un affichage donnent les messages à la vue et les supprime de la session. Comment déterminer automatiquement si une action fait ou ne fait pas de redirect ? Pour cela nous allons fouiller un peu le fonctionnement du contrôleur. Ouvrez la classe Zend_Controller_Action dont tous vos contrôleurs dérivent. Vous y trouverez les méthodes suivantes

  • Init

  • dispatch
  • render
  • _redirect

Init est appelé après le constructeur. C’est la bonne méthode pour placer des initialisations communes à toutes les actions. Comme la déclaration du namespace. Dispatch est le cœur du contrôleur c’est là que l’action courante est réellement traitée. Render est appelé par une action pour faire un rendu explicite d’une page. Et _redirect est invoqué pour une redirection.
Il y a donc deux façons d’invoquer un rendu : soit explicitement dans l’action et c’est render qui est utilisé. Soit l’action se termine sans redirect et sans render, le rendu est automatique et c’est dans le dispatch que cela se passe.

Une classe Messager

Lorsque l’on collecte des messages, cela implique de gérer une collection de messages et non une simple chaine. De plus on peut avoir divers type de messages. Des infos, des alertes, des erreurs, etc. Un messager est donc un agent qui collecte des messages les enregistre en session et les restitue pour la vue. Il est aussi nécessaire de purger les messages de temps en temps.
Nous allons définir une classe pour cela.
Nous allons donc avoir un membre pour référencer le namespace.et un tableau contenant la liste des messages.
Nous avons besoin des méthodes addOk, addNotice, addWarning, addError, getAll, clear, write, et nous ajouterons pour nous faciliter l’écriture add
Write place le tableau des messages dans le namespace, add permet d’ajouter un message en passant son type en paramètre.
Celle classe est relativement simple et peut être donnée directement ici.

_messenger = new Zend_Session_Namespace('Fast_Controller_Messenger');
        if (isset($this->_messenger->messages)) $this->_messages = &$this->_messenger->messages;
    }

    /**
    * met en session un objet standard contenant tous les membres
    * de l'objet Messenger.
    *
    * @return null
    */
    public function write()
    {
        $this->_messenger->messages = $this->_messages;
    }

    /**
    * vide tous les messages de l'objet Messenger.
    *
    * @return null
    */
    public function clear()
    {
        $this->_messenger->messages = null;
        $this->_messenger->messages = $this->_messages = array();
    }

    /**
    * Ajoute un message à la collection de messages.
    *
    * @param string $msg Texte du message
    * @param string $type Type du message (Messenger::NORMAL, Messenger::NOTICE, Messenger::WARNING, Messenger::ERROR, Messenger::OK)
    */
    public function add($msg, $type=self::NORMAL)
    {
        $obj = new StdClass();
        $obj->label = $msg;
        $obj->type = $type;
        $this->_messages[] = $obj;
    }

    /**
    * Ajoute un message de simple information.
    *
    * Alias de add($msg)
    *
    * @param string $msg Texte du message
    */
    public function addNormal($msg)
    {
        $this->add($msg, self::NORMAL);
    }

    /**
    * Ajoute un message indiquant que les opérations se sont
    * déroulées avec succès.
    *
    * Alias de add($msg, Messenger::OK)
    *
    * @param string $msg Texte du message
    */
    public function addOk($msg)
    {
        $this->add($msg, self::OK);
    }

    /**
    * Ajoute un message indiquant un avertissement, sans conséquence
    * sur le déroulement des opérations.
    *
    * Alias de add($msg, Messenger::NOTICE)
    *
    * @param string $msg Texte du message
    */
    public function addNotice($msg)
    {
        $this->add($msg, self::NOTICE);
    }

    /**
    * Ajoute un message indiquant un avertissement fort, la suite
    * des opérations pouvant être compromise.
    *
    * Alias de add($msg, Messenger::WARNING)
    *
    * @param string $msg Texte du message
    */
    public function addWarning($msg)
    {
        $this->add($msg, self::WARNING);
    }

    /**
    * Ajoute un message indiquant une erreur, la suite
    * des opérations pouvant être gravement compromise, voire arrêtée.
    *
    * Alias de add($msg, Messenger::ERROR)
    *
    * @param string $msg Texte du message
    */
    public function addError($msg)
    {
        $this->add($msg, self::ERROR);
    }

    /**
    * Retourne le tableau de messages.
    *
    * @return array
    */
    public function getAll()
    {
        return $this->_messages;
    }
}

Automatisation

Nous avons donc un messager. Il ne reste qu’à l’invoquer. Nous avons vu que tous les contrôleurs dérivent de Zend_Controller_Action. Nous ne pouvons pas modifier cette classe. Et il serait lourd de devoir redéfinir ces méthodes dans tous les Contrôleurs de l’application. Nous allons donc dériver Zend_Controller_Action en Fast_Controller_Action et faire dériver nos contrôleurs de cette classe.


Nous avons ainsi la possibilité de redéfinir les méthodes qui nous intéressent pour tous les contrôleurs.
La méthode init instancie le messager pour le rendre disponible dans toutes les actions

    public function init()
    {
        parent::init();
        $this->_messenger = new Fast_Controller_Messenger();
    }

Dans la méthode redirect on écrit les messages dans la session

    protected function _redirect($url, array $options = array())
    {
        // stocke les messages en session pour affichage ultérieur
        $this->_messenger->write();
        parent::_redirect() ;
    }

La méthode postDispatch est appelée par la méthode disptach après le traitement. En fin de processus. C’est donc un bon endroit pour donner les messages à la vue et vider le messager

    function postDispatch()
    {
        if (!$this->_redirected) {
            $this->view->messages = $this->_messenger->getAll();
            $this->_messenger->clear();
        }
        parent::postDispatch();
    }

La méthode render pour les appels explicites

    public function render($action = null, $name = null, $noController = false)
    {
        // on ajoute à la vue le tableau de messages à diffuser
        $this->view->messages = $this->_messenger->getAll();
        // on passe la main au renderer pour afficher la vue
        parent::render($action, $name, $noController);
    }

Utilisation

Nous avons donc maintenant un messager automatique dans nos contrôleurs. Voyons comment l’utiliser. Pour cela reprenons l’exemple de l’article sur la granularité des actions. Dans cet exemple nous avions des bouts de code pour placer les messages en session. Ils sont à remplacer par les méthodes add du messager. Nous avions aussi du code pour afficher les messages qui est devenu inutile.

view->list = $this->model->getObjectList();
   }

   /**
    * Prépare un enregistrement pour l'afficher dans le formulaire
    * redirige vers showForm
    * @see showForm
    */
   public function addAction() {
      $context = new Zend_Session_Namespace('context');

      $context->returnPath = '/FormController/showList';
      $context->saveMethod = 'add';
      // Demander au model un nouvel enregistrement avec les valeurs par défaut
      $context->formData = $this->model->newObject();
      $this->_redirect('/FormController/showForm');
   }

   /**
    * Recherche l'enregistrement pour l'afficher dans le formulaire
    * redirige vers showForm
    * @see showForm
    */
   public function editAction() {
      $context = new Zend_Session_Namespace('context');

      $context->returnPath = '/FormController/showList';
      $context->saveMethod = 'update';

      $id = $this->_request->get('id');
      $context->formData = $this->model->getItemById($id);
      if (!$context->formData) {
         // revenir au point de retour
         $this->_messenger->addError('no object for: '.$id);
         $redirect = $context->returnPath;
      } else {
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }

   /**
    * Affiche le formulaire d'édition d'un élément.
    *
    * @return null
    */
   public function showFormAction(){
      $context = new Zend_Session_Namespace('context');

      $this->view->cancelAction = $context->returnPath;
      $this->view->saveAction = '/FormController/checkForm/';

      $this->view->form = clone($context->formData);
   }

   /**
    * Récupère les données du formulaire les filtres et les vérifie
    * redirige vers save si le formulaire est valide showFrom sinon
    * @see save
    */
   public function checkFormAction() {
      $context = new Zend_Session_Namespace('context');
      if ($context->formData = $this->_request->get('form'))
      // vérification
      $ok = true;
      if ($ok) {
         $redirect = '/FormController/save';
      } else {
         $this->_messenger->addError('invalid datas');
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }

   /**
    * enregistre l'élément dans la collection
    * redirige vers l'action qui précédé l'action add ou edit en cas de succès
    * vers showForm en cas d'échec
    * @see add
    * @see edit
    * @see showForm
    */
   public function saveAction($perform = true) {
      $context = new Zend_Session_Namespace('context');

      $data = $context->formData;
      $method = $context->saveMethod;

      $ok = $this->model->saveObject($data, $method);
      if ($ok) {
         $this->_messenger->addOk('data saved');
         $redirect = $context->returnPath;
      } else {
         $this->_messenger->addError('error data could not be saved');
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }
}

On le voit ici, nous sommes totalement déchargés de la gestion des messages. Dans l’écriture d’un contrôleur nous ne faisons que nous concentrer sur le traitement et indiquer les messages à afficher. Ils le seront au moment approprié, sans que l’on ait à s’en préoccuper.
Le monde parfait n’existant pas cette façon de faire à un très léger défaut qu’il faut tout de même connaitre. Le messager est systématiquement vidé après chaque affichage. Ce qui signifie que si l’utilisateur use de la touche F5 sur son navigateur il appelle de nouveau l’action courante qui à déjà vidé les messages, les messages ne seront donc pas réaffichés.

10 réponses à “Un messager pour collecter les messages à l’utilisateur.”

  1. Mr.MoOx dit :

    Et bé, c’est marrant je viens de me coder un truc dans le genre.
    A une petite différence près, c’est que je supprime automatiquement les messages une fois
    que je les ai récupéré.
    Après j’ai du coup logique (très) légèrement différente du fait que je ne dois récupérer les messages qu’une fois.

    Le FlashMessenger natif du zf est quand à lui un peu problématique du fait que les messages
    ne peuvent pas être gardé si il y a redirection…

  2. Azema dit :

    Salut et un grand merci pour ce tuto fort intéressant, ainsi que les autres d’ailleurs ;-)

    J’aurai juste deux petites questions.

    – Tu fais appel à une variable (redirected) dans ton controller, mais je ne vois nul part son initialisation, est ce normal ?

    – Et au lieu d’utiliser toutes les méthodes d’ajout de messages typés, ne serait il pas préférable d’utiliser une méthode magique « __set($type, $msg) » et faire un switch en mettant la constante self::NORMAL par défaut ?

    Sinon, le principe est super pratique et astucieux, je pense qu’il fera partit de mes futurs développements, si tu veux bien.

    Encore merci, cela me fait de la lecture passionnante et instructive.

    Cordialement, Azema.

  3. sekaijin dit :

    _redirect est une méthode standard du contrôleur
    la variable $redirect est elle systématiquement positionné dans les actions
    en général en fonction du contexte

    quant aux méthodes addXX elles font toute un appel comme
    $this->add($msg, self::ERROR);
    add étant publique tu peux l’utiliser

    en terme d’écriture ça ne change rien du coup mettre les deux dans la classe ne coûte rien à l’usage chacun peu ainsi travailler comme il le sent
    A+JYT

  4. Lesauf dit :

    Intéressant. J’ai codé un truc similaire en partant de ton post dans z-f.
    Merci.

  5. sekaijin dit :

    de rien ;)

  6. found your site on del.icio.us today and really liked it.. i bookmarked it and will be back to check it out some more later ..

  7. Delprog dit :

    Salut,

    Encore un article très intéressant.

    J’avais de mon côté mis en place un système de messages utilisant aussi les sessions, mais j’ai rencontré quelques difficultés lorsque je suis passé au Framework Zend.

    Merci, ton article et ton expérience me permettent de corriger quelques erreurs de conception dans ce que j’ai adapté pour mes controllers.

    J’ai quand même une petite question, tu fais un test « if (!$this->_redirected) » dans la méthode de postDispatch() et un clear() du messenger à ce moment là.

    Que se passe-t-il lorsqu’on rend la vue directement depuis l’action (donc par un render()) et qu’il n’y a pas eu de redirection ? Tu passes bien les messages à la vue, mais tu ne nettoie pas la session par la suite. Est-ce un oublie ? Ou est-ce que j’ai raté quelquechose ?

    De plus, d’où vient cette variable « _redirected » ? ( »if (!$this->_redirected) »).

    Merci,

    A+ benjamin.

  8. sekaijin dit :

    la variable _redirected est positionnée par ZF lorsqu’on fait une redirection.

    pour ce qui est de rendu explicite cela ne change rien car ZF passe par le post dispatch même s’il y a eut une demande de rendu explicite.

    A+JYT

  9. Delprog dit :

    Je ne vois pas cette variable dans ZF, je pense qu’elle a du sauter dans les nouvelles versions.

    Enfin, au pire je la rajoute dans la surcharge du _redirect.

    Merci en tout cas.

    A+ benjamin.

  10. sekaijin dit :

    Sorry j’ai écrit cette partit il y a longtemps et j’avais oublié un peu cette partie

    _redirected est définie par Fast_Controller_Action

    elle vaut false au départ elle est positionnée à true dans la méthode _redirect
    puis elle est utilisée dans postDispatch

    A+JYT

Laisser un commentaire