Archive pour la catégorie ‘Général’

Gestion de contexte applicatif.

Dimanche 5 octobre 2008

Nous avons vu dans l’article précédent qu’il était possible de rendre plus libre la navigation dans une application web. Mais pour cela il est nécessaire de garder en mémoire les informations en cours de traitement. Mais cela n’est pas suffisant. Il est aussi nécessaire de bien les associer au traitement en cours. En effet il ne suffit pas de mettre les informations d’un client en mémoire car si l’utilisateur entame deux processus qui portent sur deux clients nous risquons d’avoir des problèmes. On voit bien dans un cas qui faut associer chaque client à son traitement. Partant de ce constat au fil du temps j’en suis venu à définir un espace mémoire associé à un processus, que j’ai nommé « contexte ». Un contexte est donc quelque chose d’extrêmement simple. Dans le cas d’une application web, le contexte doit survivre à l’exécution d’un script. La session est là pour cela.

Une utilisation basique de la session.

Lorsque dans une application nous avons besoin de conserver quelque chose en session, nous lui associons un nom et nous l’enregistrons simplement dans la session. La session n’est en fait qu’une Hash-Table, un tableau associatif. Avec le nom, nous sommes capables de retrouver l’information ainsi préservée. C’est simple et efficace. Mais comme dit dans l’introduction il arrive parfois que cela ne soit pas suffisant. On commence généralement par essayer de trouver de nouveaux noms pour les éléments que l’on met dans la session afin de mieux s’y repérer.

Une approche structurée de la session.

Indépendamment de toute action et de tout processus on peut se demander comment organiser la session pour en faciliter la gestion. Pour cela il est pertinent de se demander ce qu’on place dans une session. La première chose qui vient à l’esprit d’un développeur web expérimenté, est l’utilisateur de l’application. C’est d’ailleurs très souvent par ce point qu’un développeur PHP expérimente pour la première fois la session. En essayant de prendre du recul on s’aperçoit que les informations sur l’utilisateur de l’application sont des informations très générales, elle impacte toute l’application. D’autres informations que nous plaçons en session sont quant à elles très spécifiques à l’opération en cours. Par exemple garder les informations sur un objet qu’on propose à l’édition, pour pouvoir gérer les modifications. Dans l’article précédent, je parlais de garder en session un historique des actions de l’utilisateur. Cet historique est de nouveau un élément transverse qui impacte toute l’application.
On peut rapidement voir que nous mettons en session des objets transverses et d’autre spécifiques à un processus. En regardant de loin une session, on peut dire que nous avons un gros tableau associatif. Et en ce plaçant dans une action on voit vite que seule une partite nous intéresse. Cette partie contient tous les objets transverses. Et tous les objets spécifiques à l’action, au processus en cours. Tout le reste n’a pas d’intérêt. Il peut être intéressant de structurer la session pour différencier ces deux types d’éléments. Reste à trouver comment.

Sous tableau associatif.

La première idée généraliste consiste à découper la session en deux parties. Un tableau associatif pour les objets transverses, et un pour les objets spécifiques. Essayons de voir ce que cela nous apporte. Imaginons que pour la partie globale nous ayons défini un tableau « global. » Chaque fois que nous voudrons accéder à un objet global il nous faudra en passer par le tableau « global » pour récupérer l’élément par son nom. Cela ne nous apporte pas grand-chose par rapport à une session gérée de façon basique. C’est tout aussi simple d’utiliser l’objet directement à partir de son nom. Pour la partie globale donc, l’impact n’est pas très probant voire même complexifié pour rien. Pour la partie spécifique, pour retrouver un élément il nous faut là aussi le retrouver par son nom. Et l’apport et là encore non probant. Cette approche ne nous permet pas de différencier un élément spécifique d’un processus de l’élément spécifique d’un autre. Organiser sa session en objets transverses et objets spécifiques n’est pas pertinente. Mais cela nous permet tout de même de voir qu’il n’est pas bien plus compliqué de gérer les éléments de la session dans des tableaux plutôt que directement dans la session elle-même.

Session et processus.

Forts de notre expérience laissons les objets transverses directement dans la session et définissons un tableau associatif par processus lancé par l’utilisateur. Pour les objets globaux l’utilisation reste aussi simple qu’avec un usage basique de la session et pour les objets spécifiques, nous les retrouvons tous dans le tableau associatif relié au processus. Le tri est donc bien effectué. Chaque processus ayant son tableau directement dans la session, les quelques fois où un processus doit passer une information à un autre il peut, soit utiliser l’espace global, soit utiliser le tableau de cet autre processus. Avec un simple tableau il est donc possible de gérer ce contexte propre à un processus.

Contexte et MVC

Dans Zend_Framework, pour la gestion de la session nous trouvons la classe Zend_Session_Namespace qui permet de définir une nouvelle entrée dans la session. De plus la programmation par objet y est très largement généralisée. Ne serait-il pas intéressant plutôt que d’utiliser un tableau associatif pour le contexte d’utiliser un objet ? Tout comme pour la vue à laquelle nous donnons des informations par ajout d’un nouveau membre, ajouter ou lire un élément de contexte par un membre semble séduisant. La différence entre une simple classe et un tableau associatif n’est pas très grande. Mais passer par une classe contexte vas impliquer des contrainte supplémentaire. Si nous plaçons un objet d’une telle classe en session nous devons être sur qu’à l’ouverture de la session celle-ci sera connue où nous allons nous heurter à une erreur PHP. Que pourrait nous apporter une telle classe ? Le contexte est en fait un tas, un espace où on associe une clef à une valeur, on accède à une valeur par sa clef, et où l’on supprime une valeur par sa clef. Bref exactement ce que fait un tableau associatif ou si on veut rester objet une stdClass de PHP. Quelles sont les spécificités d’un contexte ? Ce qui rend particulier un contexte par rapport à un simple tableau est le fait qu’on va le mettre dans la session et l’utiliser dans une action d’un Contrôleur. Nous pourrions mettre ce petit traitement dans le constructeur d’une classe contexte. Nous devrions alors nous assurer de la présence de la classe à l’ouverture de la session et passer au constructeur le contrôleur de l’action courante pour pouvoir faire la bonne association. En utilisant une stdClass, nous nous retrouvons contraints de faire ce travail dans le contrôleur. J’ai pour ma part préféré cette dernière approche.

Le contrôleur et le contexte.

Pour faciliter l’usage j’ai ajouté un membre context à tous mes contrôleurs. Et une méthode permettant de créer le contexte.

    /* methode contexte */
    /**
    * Crée un espace associé à l'action courante dans la session
    * et l'attache au controller.
    *
    */
    protected function _createContext($name)
    {
        $this->context = new Zend_Session_Namespace($name);
    }

Ajouter un objet dans le contexte consiste comme pour la vue à ajouter un membre. De même pour l’utiliser. Il faut noter ici le fonctionnement de Zend_Session_Namespace qui vas créer une entrée dans la session avec le nom $name si cette entrée n’existe pas et qui va sinon lire cette entrée et en retourner une référence. _createContext associe donc l’action courante à une entrée de la session et la créé si nécessaire. Il est parfois nécessaire de trouver le contexte d’un processus sans pour autant vouloir l’associer à l’action courante, pour par exemple y placer ou y lire une information. Cela permet une perméabilité des contextes, maitrisée par le contrôleur.

    /**
    * trouve l'espace associé à un autre processus dans la session
    *
    */
    protected function _getContext($name)
    {
        return new Zend_Session_Namespace($name);
    } 

Il est aussi nécessaire de pouvoir supprimer un contexte. Le ménage de la session est quelque chose d’important.

    /**
    * Supprime un contexte par son nom
    *
    */
    protected function _deleteContext($name)
    {
        if ($name) {
            $acontext = new Zend_Session_Namespace($name);
            $acontext->unsetAll();
        }
    } 

Avec cela nous pouvons utiliser des contextes associés à nos processus. Il suffit dans les actions du processus d’appeler _createContexte avec un nom choisi pour ce processus. Un contexte est donc finalement quelque chose de relativement simple.

Contexte et historique.

Dans l’article précédent, je parlais de la dynamique de l’application et proposais d’utiliser un historique. Était souhaitable de combiner historique et contexte ? La réponse est donnée dans l’article précédent. Lorsqu’on revient dans la pile de l’historique on en profite pour faire du ménage. Si on a combiné la gestion de contexte à celle de l’historique on sera capable de faire le ménage dans l’historique mais aussi dans les contextes. Nous avons utilisé le chemin de l’action pour la repérer dans l’historique. Très souvent ce nom fait l’affaire pour définir un contexte. En l’utilisant on peut simplifier l’utilisation du contexte. Enfin en plaçant une référence d’un contexte dans toutes les entrées d’historique le concernant, on a un contexte associé à tout le processus de façon quasi automatique. Il nous faut donc revoir un peu la création de contexte et de l’historique. Il nous faut aussi une méthode pour récupérer le contexte de l’action précédente pour gérer les enchainements.

    protected function _openHistory()
    {
        if (((is_array($this->_useHistory)) &&
        (in_array(ereg_replace('Action$', '', $this->_request->getActionName()) , $this->_useHistory)))||
        true === $this->_useHistory)
        {
            $history = new Zend_Session_Namespace('History');
            $key = $this->makeActionPath();
            if (!$this->_inHistory($key)) {
                $keys = array_keys(iterator_to_array($history->getIterator()));
                if ($keys) $previous = array_pop($keys);
                $history->{$key} =  new StdClass();
                $history->{$key}->path =  $key;
                if (isset($previous)) {
                    $history->{$key}->previous = &$history->$previous;
                } else {
                    $history->{$key}->previous = (object)array('path' => '/');
                }
            } else {
                if (isset($history->{$key}->context))
                $context = $history->{$key}->context;
                $keys = array_reverse(array_keys(iterator_to_array($history->getIterator())));
                foreach ($keys as $akey) {
                    if ($akey == $key) {
                        break;
                    } else {
                        if (isset($history->{$akey}->context) && (
                        (isset($context)&&$history->{$akey}->context != $context)||
                        (!isset($context)))
                        )
                        {
                            $this->_deleteContext($history->{$akey}->context);
                        }
                        unset($history->{$akey});
                    }
                }
            }
            $this->history = $history->{$key};
        } else {
            return null;
        }
    }

    /* methode contexte */
    /**
    * Créé un espace associé à l'action courante dans la session
    * et l'attache au controller.
    *
    */
    protected function _createContext($name = NULL)
    {
        if (!$name) $name = $this->makeActionPath();
        $this->context = new Zend_Session_Namespace($name);
        if ($this->_useHistory) {
            $history = new Zend_Session_Namespace('History');
            #$history->{$this->makeActionPath()} = new StdClass();
            $this->history->context = $name;
        }
    }

    /**
    * Supprime un contexte par son nom
    *
    */
    protected function _deleteContext($name)
    {
        if ($name) {
            $acontext = new Zend_Session_Namespace($name);
            $acontext->unsetAll();
        }
    }

    /**
    * trouve l'espace associé à l'action précédente dans la session
    * l'associe à l'action courante et l'attache au controller.
    *
    */
    protected function _linkContext($name = NULL)
    {
        if ($this->_useHistory) {
            if (!$name) {
                if (isset($this->history->previous->context)) {
                    $name = $this->history->previous->context;
                } else {
                    $name = $this->makeActionPath();
                }
            }
            $this->context = self::_getContext($name);
            if (isset($this->history)) $this->history->context = $name;
        } else {
            Zend_Loader::loadClass('Fast_Controller_Exception');
            throw new Fast_Controller_Exception(Fast_Controller_Exception::INVALID_HISTORY);
        }
    }

    /**
    * trouve l'espace associé à un autre processus dans la session
    *
    */
    protected function _getContext($name = NULL)
    {
        if (!$name) {
            if ($this->_useHistory && isset($this->history->previous->context)) {
                $name = $this->history->previous->context;
            } else {
                $name = $this->makeActionPath();
            }
        }
        return new Zend_Session_Namespace($name);
    } 

Ce code est à placer dans une classe commune à tous les contrôleurs. Chez moi elle s’appelle fast_action. Pour utiliser un contexte il suffit de faire appel à _createContext() dans l’action qui débute le processus. Toutes les action du processus font appel à _linkContext() le ménage étant assuré par la gestion de l’historique. J’ai conservé l’argument $name dans mes méthodes de gestion du contexte pour permettre au contrôleur de définir un autre nom que le chemin de l’action.

Conclusion.

Le contexte n’est finalement qu’une façon d’utiliser la session. Il ne permet pas de gérer toutes les possibilités, mais apporte un cadre clair et simple pour gérer les données d’un processus. J’ai développé cette approche il y a quelques années déjà. Et tout cela est le fruit d’un long murissement. De l’avis des utilisateurs de cette approche, elle permet de clarifier la session et d’en maitriser son contenu.
Pour ma part je dirais qu’elle permet surtout au développeur de se pencher explicitement sur la gestion de sa session. Négliger sa session dans une toute petite application, n’est pas très bon. Mais cela devient vite catastrophique dès que l’application prend de l’ampleur.

Gestion dynamique de la navigation.

Lundi 30 juin 2008

Contrairement à un client lourd, les applications web proposent en général une navigation relativement statique. Les actions et les écrans s’enchaînent selon la logique du développeur et non celle de l’utilisateur. On lui propose généralement un ensemble de choix qui mènent vers d’autres choix etc Mais on trouve rarement, comme pour une application sur le bureau, une liberté totale. L’approche Web2.0 tend à corriger ce défaut et redonne à l’utilisateur l’initiative. Le plus souvent on va donc trouver dans les écrans d’une application web des liens vers d’autres parties de l’application. Se posent alors quelques petits soucis. Le premier est l’évolutivité de l’application. En imaginant que mon utilisateur active l’action « éditer un client » dans l’écran je vais lui proposer un lien permettant d’annuler cette opération à n’importe quelle étape du processus d’édition. Que mettre comme valeur dans ce lien ? En effet, à priori, je ne sais pas dans quel état il était avant d’activer la fonctionnalité. Généralement ne sachant pas vers quoi le rediriger on l’envoie sur un écran d’accueil, un menu général. C’est plutôt frustrant. Où alors on s’arrange pour que cette fonctionnalité ne soit accessible que d’un seul endroit. Là encore c’est peu satisfaisant du point de vue de l’utilisateur. Il serait souhaitable d’annuler la fonctionnalité en cours et de revenir à celle qui précédait. Pour cela il est nécessaire de connaitre dynamiquement le point d’activation de la fonctionnalité. Par exemple depuis le menu principal, je peux rechercher puis éditer un client. Mais aussi lorsque je fais une facture (pour corriger une adresse par exemple). Mais si je permets à mon utilisateur d’activer les fonctions qu’il veut quant il veut, quand saurais-je que je peux nettoyer ma mémoire (session) des objets en cours ?

Gestion d’un historique côté serveur

Pour cela j’ai choisi de gérer côté serveur un historique des actions de l’utilisateur. Tout comme le navigateur liste les pages affichées, l’application va lister les actions. Ainsi à tout moment une action pourra chercher dans l’historique les actions précédentes et déterminer dynamiquement les enchaînements à réaliser. Reprenons l’opération « éditer un client ». Cela va se découper en plusieurs étapes : rechercher le client, afficher le formulaire, vérifier le formulaire, enregistrer le client. Dans chacune de ces actions, si l’annulation est demandée il faut revenir à l’action qui précédait la recherche du client. Il suffit donc dans cette dernière de regarder l’action précédente dans l’historique pour savoir à tout moment où se trouve le point de retour. De même si l’opération arrive à son terme : l’édition terminée on peut revenir à l’état antérieur. En procédant ainsi on a bien la possibilité de chaîner dynamiquement les actions, mais l’historique risque de grandir indéfiniment. En effet, si par exemple la vérification ne permet pas de passer à l’étape d’enregistrement on va revenir au formulaire, puis à la vérification. On va donc à chaque action ajouter une entrée dans l’historique. Bien sûr, l’utilisateur ne bouclera pas de façon infinie, mais cela peut devenir important. Il serait plus judicieux de gérer l’historique en permettant de revenir sur nos pas. Que faire alors de ce qui suit dans l’historique ? Si je suis revenu sur l’affichage du formulaire, j’ai dans l’historique une étape de plus qui est celle de la vérification. Les informations de cette action ne me sont plus d’aucune utilité. Si je poste de nouveau le formulaire, je recommence la vérification avec de nouvelles données. Si je pars sur une autre action, celle-ci prendra la place dans l’historique. J’ai donc deux possibilités : soit je mets systématiquement dans l’historique, et je ne gère pas les retours, l’historique ne me sert que de mémoire. Soit je décide de pouvoir revenir sur mes pas, et je perds de la mémoire, mais mon historique représente l’état d’avancement de mon processus. Auquel cas il est géré comme une pile. Un problème qui va se poser est la gestion multifenêtre. Si j’ai un historique côté serveur, il doit me permettre de suivre l’activité de mon utilisateur. Mais si ce dernier ouvre plusieurs fenêtres sur mon application je ne peux pas savoir quelle fenêtre fait quelle action. Avant de travailler avec le Zend_Framework, le cadre que j’utilisais n’avait qu’une seule url sans argument. L’utilisateur ouvrait toujours la même url. Du coup, s’il ouvrait une nouvelle fenêtre, il faisait varier l’historique dans l’une ou dans l’autre. Mais le framework ne s’occupait que de la pile : une actualisation dans l’une des deux fenêtres donnait toujours le résultat de l’action en sommet de pile. Avec Zend_Framework il en va légèrement différemment. Si je rafraîchi la première fenêtre alors que j’ai des opérations en cours dans la deuxième, je vais ré-exécuter l’action courante et perturber l’historique. Il n’y a pas de solution simple. Il est impossible de savoir si on a une ou plusieurs fenêtres. Les actions devront donc vérifier qu’elles s’exécutent dans un ordre approprié. Ce n’est pas propre à la gestion d’historique côté serveur. Si un utilisateur ouvre un bookmark sur l’action « afficher client » alors qu’aucun client n’est présent dans la mémoire, l’action ne peut se dérouler correctement.

La pile historique et processus

J’ai choisi de gérer mon historique comme une pile. Chaque nouvelle action est placée au sommet et revenir à une étape antérieure supprime toutes les actions postérieures dans la pile. Reprenons l’opération « éditer un client. » Comme nous l’avons vu précédemment, cette opération se découpe en plusieurs actions. Toutes ces actions marchent ensemble, elles forment un processus. Lorsque l’utilisateur invoque cette opération, il entre dans le processus par l’action de recherche du client. Cette action va donc être placée sur la pile. On a vu qu’elle a besoin de connaitre l’action précédente, qui n’est autre que le sommet de la pile avant empilage. Si la recherche est infructueuse, on revient à l’action précédente et cette action est supprimée de la pile. Si la recherche est fructueuse, on passe à l’affichage du formulaire (sans supprimer l’action de recherche), et on empile l’action d’affichage. Puis on enchaîne sur la vérification qui est placée sur la pile. On va empiler/dépiler autant de fois que le formulaire sera invalide. Puis on enchaîne sur l’enregistrement, qui sera empilé à son tour et on revient enfin au point de retour, le processus terminé. On dépile alors toutes les actions de ce processus et notre historique est nettoyé. Mais cette approche à une contrainte forte : je ne peux pas commencer un processus A, puis alors qu’il n’est pas terminé commencer un processus B, pour commencer de nouveau un autre processus A. En effet si je commence une deuxième fois le processus A je vais revenir dans la pile dans l’état où il était lorsque je l’ai suspendu la première fois. Et retirer de la pile tout le processus B, ce n’est pas nécessairement ce que je souhaite. Pour parvenir à empiler plusieurs fois le même processus, il faudrait pouvoir placer sur la pile la même action alors que j’ai choisi, lorsque je revenais sur une action de remonter dans l’historique. Pour palier à cette difficulté, il est nécessaire de pouvoir placer une action dans la pile en choisissant explicitement son identifiant : la clef qui permet de la repérer. J’avoue ne pas être tombé sur ce problème depuis que je pratique cette approche. Mais oublier le fait que l’historique est une pile, peut mener à des situations qui paraissent improbables.

Les processus

J’ai volontairement utilisé ce terme ci-dessus car cette notion va me permettre d’organiser ma navigation. Un processus est un ensemble d’actions et d’enchaînements qui forment une étape. « Rechercher des clients » se décompose ainsi en : « afficher un formulaire de recherche » « vérifier les informations du formulaire » « rechercher la liste des clients » « afficher la liste des clients ». Toutes ces actions ont un enchainement logique entre elles, et seul cela importe. Quid de ce qu’il y a avant ou après comme action, elles forment un processus. Ce processus peut être interrompu ou mené à son terme, mais les relations entres ces actions là sont clairement définies. Elles ne dépendent nullement des choix de l’utilisateur. Par contre l’activation du processus, son annulation, la poursuite vers un autre processus est de son ressort. On voit clairement qu’un processus a un point d’entrée et un ou plusieurs points de sortie. Dans le point d’entrée il est très facile de lire la pile pour garder l’action qui précédait. Lorsque le processus est annulé on revient sur cette dernière. La pile se vide libérant les informations du processus et reprenant le processus précédent dans l’état où il était. Lorsque le processus se termine, il revient au programmeur de décider de la suite. S’offre à lui alors une facilité pour revenir à des actions antérieures. Très souvent mes processus se terminent par un retour au processus précédent avec un message de confirmation.

Et la session ?

En effet, je garde une pile des actions de l’utilisateur. Il me faut la conserver dans la session sinon je ne saurais retrouver mes petits. Depuis le début je parle de revenir sur des actions laissées dans la pile et de récupérer les infos associées. Cela signifie que je garde dans la session les informations nécessaires au fonctionnement de mes actions. Nous savons tous que la session peut rapidement devenir un casse tête. Or j’ai mis en place une gestion de l’historique sous forme d’une pile qui fait le ménage de façon automatique. Placer des informations en session c’est bien, mais les retirer au moment approprié c’est mieux. Et c’est bien souvent là que les difficultés arrivent. Car il faut, soit passer par un script particulier qui sera appelé un peu partout pour faire le ménage, sans grande garantie, soit restreindre la liberté de mouvement de l’utilisateur pour être sûr qu’il passera par le chemin qui nettoie au bon moment. Avec l’historique nous savons à tout moment où nous en sommes. Ainsi lorsque nous retirons de la pile des actions, nous savons que nous pouvons libérer les informations les concernant. Reste à les connaitre.

Gestion de contexte associé à l’historique

La solution que j’ai retenue après l’avoir longuement expérimentée, est de créer un namespace associé à l’action et référencé dans l’historique. Je l’ai nommé « contexte ». Ainsi l’action n’a pas à se soucier de la session, elle place ses infos dans le contexte et les y retrouve. Lorsque je dépile l’action, je supprime le contexte et le ménage est fait. Un espace de session par action c’est pratique mais c’est lourd et souvent inutile. De plus, dans un même processus, on a besoin de partager des informations entre actions. J’ai donc choisi de ne pas rendre la création du contexte automatique, mais sur demande. Ainsi lorsque je rentre dans un processus qui va avoir besoin de gérer SA session, je crée le contexte. Et dans toutes les autres actions du processus j’utilise le même. Je peux ainsi partager l’information tout en gardant le nettoyage automatique. Mais cela va plus loin : toute action pouvant visiter l’historique, peut si besoin lire ou écrire dans le contexte d’une autre et donc par ce biais, lui fournir de l’information. Toutes les informations de la session sont rangées, organisées, et accessibles. Si dans un processus j’empile un certain nombre d’action et qu’il me faut aller chercher la première pour retrouver mon contexte, cela peut s’avérer compliqué. Je pourrais être tenté de rechercher l’identifiant de l’action, point d’entrée dans l’historique, mais cela impliquerait de coder en dur cet identifiant. Du coup, l’évolution de l’application en serait limitée. Une solution évolutive et simple à mettre en œuvre est d’utiliser le contexte de l’action précédente. Le point d’entrée quel qu’il soit, créé un contexte. L’action suivante le référence, la suivante va chercher le contexte précédent donc celui du point d’entrée et ainsi de suite. De cette façon toutes les actions du processus, pour propager le contexte et l’utiliser, n’ont qu’à référencer le contexte précédent. Seul le point d’entrée, doit en créer un.

Suspendre un processus

Il n’est pas rare dans une activité professionnelle de devoir suspendre son travail pour répondre à une demande urgente. Très souvent, dans les applications web l’unique possibilité est de finir ou d’annuler le travail en cours pour commencer un autre traitement. Imaginez : je suis dans le processus « consolidation factures », c’est un travail long et précis. On me demande de modifier une fiche client ou d’intervenir sur un tout autre processus. J’ai donc dans ma pile le processus « consolidation factures » avec quelques actions et le contexte associé. J’ouvre l’action « Éditer client » : cette action va se placer sur la pile et créer un nouveau contexte. Je vais pouvoir enchainer sur l’affichage du formulaire et continuer le traitement d’édition. Lorsque le traitement sera fini, je vais revenir sur l’action qui précédait le point d’entrée. Toutes les informations d’édition seront supprimées et je me retrouverais exactement dans la même situation que celle que j’avais avant d’éditer mon client. Mon processus n’a pas été perdu il est juste resté en retrait le temps de faire le traitement urgent. Il y a tout de même une limite à ce fonctionnement : si j’édite un client puis que je suspends cette édition pour consolider mes factures, lorsque je vais une fois encore suspendre ma consolidation pour éditer un autre client, je vais me retrouver sur le contexte de la première édition. Ce n’est pas du tout l’effet escompté. Pire au passage le contexte de la consolidation est parti à la poubelle. En effet lancer l’action d’édition revient à revenir sur celle qui était en cours. On touche là, la limite d’un traitement entièrement automatique de la pile d’historique basé sur l’action. Pour résoudre ce genre de problème, il faudrait pouvoir placer une clef dans l’historique qui prenne en compte l’action mais aussi d’autres paramètres. Mais justement lesquels ? Cela dépend entièrement de l’application et du processus. Impossible de faire ça de façon générique. La seule possibilité est donc de gérer, non plus automatiquement la mise en historique, mais à la demande des actions. Reste que dans la grande majorité des cas la gestion automatique est suffisante. Il peut être intéressant de la rendre automatique par une déclaration dans le contrôleur. Ainsi si le contrôleur déclare la mise en historique automatique il n’est pas nécessaire d’en faire la demande dans chaque action. Et si elle ne l’est pas, le développeur à tout loisir de choisir sa méthode de mise en historique.

Récapitulatif

Les actions sont placées dans un historique. On a donc besoin d’une méthode pour les y placer _openHistory. Les actions doivent pouvoir lire l’historique _getHistory pour lire une rentrée particulière de l’historique et _popHistory pour récupérer et retirer la dernière entrée dans l’historique. Une méthode pour vérifier qu’une action est dans l’historique ne fait pas de mal _inHistory. Il n’y a pas de raison de détruire l’historique, il doit disparaitre avec la session lorsque l’utilisateur quitte l’application.
Les actions doivent aussi pouvoir créer un contexte _createContext ou alors référencer le contexte de l’action précédente. _linkContext. Lorsqu’on dépile les actions il est nécessaire de détruire le contexte associé _deleteContext. Enfin, il est nécessaire d’indiquer si on utilise ou pas la mise automatique en historique $_useHistory. Toutes ces méthodes concernent les actions de tous nos contrôleurs. L’idéal est de les faire porter par une classe commune à tous les contrôleurs. Dans Zend_Framework tel qu’il est fait de base cette classe s’appelle Zend_Controller_Action. Il ne faut surtout pas la modifier. Par contre la programmation orientée objet nous permet de la dériver, comme je l’ai fait pour le messager. Je vais donc utiliser la classe Fast_Controller_Action. Elle sera enrichie de nouveaux membres : $_useHistory qui indique l’utilisation automatique de l’historique, $history qui est l’entrée dans l’historique de l’action courante, $context qui est le contexte de l’action courante. Et de toutes les méthodes vues précédemment.

Un exemple

class Client_ManagerController
extends Fast_Controllers_Action
{
    //utiliser automatiquement l’historique
    protected $_useHistory = true;

    public function editAction() {
        //créer un contexte pour le processus
        //d’édition client
        $this->_createContext();
        $this->context->returnPath =
            $this->history->previous->path;
        $id = $this->_request->get('id');
        $this->context->fromData =
            $this->model->getClientById($id);
        $this->context->saveMethod = 'update';
        ...
    }

    public function showFormAction()
    {
        //utiliser le contexte du processus
        //d’édition client
        $this->_linkContext();
        if (isset($this->context->saveMethod)) {
            //si le contexte contient le nécessaire
            //on affiche le formulaire
            ...
            $this->view->cancelButton =
                $this->_buttons['cancel'];
            $this->view->saveButton =
                $this->_buttons['save'];
            ...
        } else {
            //l’action à été appelée alors qu’on
            //n’a pas les éléments nécessaire
            //on annule l’action, et on revient
            //à l’action précédente. Mais on ne
            //peut pas savoir si un point de retour
            //est dans le contexte.
            trigger_error(
                'Invalid context to call showFormAction',
                E_USER_WARNING);
            $redirect = $this->history->previous->path;
            $this->_redirect($redirect);
        }
    }
    ...
 }

Le contrôleur client utilise donc l’historique automatique à chaque appel d’une action. L’action est placée automatiquement dans la pile par _openHistory. Au passage le membre $history du contrôleur est renseigné. Lorsque l’action appelle _createContext ou _linkContext le membre $contexte est renseigné. L’action n’a plus à se soucier de l’organisation de la session, seules les clefs la concernant sont de son ressort.
Il peut être intéressant d’agir sur le contexte d’une autre action dans l’historique. J’utilise souvent des listes alphabétiques ; par exemple pour les utilisateurs, j’ai un index et je n’affiche que les utilisateurs dont le nom commence par la lettre sélectionnée. Lorsque l’utilisateur clique sur « ajouter utilisateur » j’entre dans un nouveau processus, et lorsque celui-ci est fini je reviens sur l’affichage de la liste. Dans la dernière action du processus « ajouter utilisateur » je regarde le contexte de mon point de retour. S’il y a un index alphabétique j’y mets la première lettre du nom de l’utilisateur que je viens de créer. Ainsi lorsque la liste s’affiche je suis dans la page qui contient l’utilisateur que je viens de créer.

Conclusion

Je pratique cette méthode depuis plusieurs années. Elle se montre efficace. Mais je dois avouer qu’elle n’est pas parfaite. La possibilité de la rendre débrayable fut une grande étape. Mais surtout elle me permet une grande souplesse. Mettre à disposition de l’utilisateur un processus à une étape de l’application à laquelle il n’était pas prévu initialement est grandement simplifié. Il suffit de s’assurer qu’il n’est pas déjà dans la pile. De même la conception de la dynamique générale de l’application est facilitée. Le travail consiste essentiellement à définir les processus et les enchaînements des actions dans le processus. Les enchaînements de processus étant eux du ressort de l’utilisateur. Le nettoyage de la session qui est toujours difficile se trouve là grandement facilité. J’avoue le plus souvent ne pas m’en préoccuper. Reste des contraintes importantes : impossible de gérer plusieurs fenêtres et si l’utilisateur cherche à jouer avec les urls il va au devant de surprises. Il faut être vigilant sur le contenu de contexte lorsqu’une action est invoquée. Il faut bien vérifier qu’on va pouvoir l’exécuter correctement, mais cela est vrai dès qu’on utilise la session.

Effacer ses traces

Mardi 23 octobre 2007

Le sujet de ce propos sera beaucoup plus large de portée que mes articles sur les Zend_Framework. Que celui qui n’a jamais mis un petit echo dans sont code pour voir ce qu’il se passait me jette la première pierre.
C’est une pratique courante car simple à mettre en œuvre, et facile à comprendre. Mais voilà si on met une trace, il faut ensuite l’enlever. Dans un monde parfait, on ne devrait pas avoir besoin de mettre ainsi des traces dans son code. L’usage d’un débuggeur pas à pas, remplaçant avantageusement cette pratique. Reste qu’il n’est pas du tout évident de s’en servir, ni même de l’installer.
La pratique de la trace existant et étant bien ancrée, autant ne pas fermer les yeux dessus. Mettre une trace permet de répondre à deux problématiques : « mon code passe-t-il par là ? » et « Quelle est la valeur de cette variable à cet endroit ? » On le voit un simple écho ou print_r ou var_dump permet de faire cela. Lorsque les traces se multiplient il est intéressant de les repérer. On ne va plus alors faire un simple écho, mais on l’accompagnera d’un message permettant de savoir quelle trace à provoqué l’affichage. « État de ma variable avant la boucle : $mavar »
Comment lorsque j’ai quelques dizaines de milliers de lignes de code, retrouver toutes les traces pour les supprimer ? Un refactoring général ne permet pas d’obtenir le résultat car que ce soit echo print_r var_dump il est impossible à priori de savoir si c’est une trace ou un affichage.

Une fonction debug

Une première approche serait d’utiliser une fonction debug, ainsi toute les traces sont repérables, pour les supprimer il suffit de remplacer dans son code tous les debug par #debug et plus rien ne s’affiche. Au passage on peut définir cette fonction avec deux paramètres un message pour repérer la trace et un autre pour afficher la valeur. Je peux dès lors poser une trace ainsi

debug(' État de ma variable avant la boucle', $mavar);

ou

debug(' je suis passé ici');

Continuons car ça ne mange pas de pain. La lisibilité d’une trace est essentielle. Sinon elle ne sert à rien. Avoir un gros tas de valeur en vrac à l’écran n’aide pas beaucoup à comprendre. Si à la place d’un simple echo ou print_r, je procède ainsi :

echo '<pre>'.$message . ' => ' ;
print_r($variable) :
echo '</pre>';

Mon affichage va devenir beaucoup plus lisible. Le tout associé à une feuille de style est ça devient du luxe. Mais je suis gourmand. Il est parfois intéressant d’avoir des informations complémentaires comme le nom du fichier la ligne où à été posé la trace ou encore quelles sont les fonctions qui ont été appelées.
On le voit vite le fait d’avoir une fonction pour gérer ces traces permet rapidement d’en avoir plus pour peu.

Et les sessions alors ?

Que viennent faire les sessions dans les traces me direz-vous. C’est très simple. Le mécanisme de session de php ne peut fonctionner que si aucun élément n’a été transmit au client. Et une trace est justement une réponse au client. De même il est impossible de faire une redirection si une trace à été posée. C’est bigrement embêtant. Car on va immanquablement provoquer une erreur. Il n’y a rien à faire, la trace posée et transmise, ni l’ouverture de la session, ni les headers, ne fonctionneront. Le mieux que nous pouvons faire est de prévenir le développeur qu’il ne pourra exécuter ses appels. Un moyen simple est de définir une constante. Dès la première trace on définit une constante DEBUG_STARTED et ainsi on pourra en tenir compte. Quitte à avoir un mécanisme général pourquoi ne pas le mettre en place aussi pour supprimer tous les affichages de traces. Une constante pour dire à la fonction d’afficher ou non les trace. Ainsi un paramètre de configuration pour supprimer toutes les traces d’un coup.
Une classe statique
Une classe statique permet de répondre à tous ces besoins. L’indicateur d’affichage étant un membre statique et la fonction débug une méthode statique. Ainsi tout est regroupé dans une même unité fonctionnelle.
Je vous donne ma classe
La méthode Fast_Debug::displayDebug(true) ; active ou désactive l’affichage. Qui et inactif par défaut.
La méthode Fast_Debug::show(’mon message’, $var); affiche une trace.
Elle indique le nom du fichier et la ligne sur la quelle la trace à été posée.
Puis le message et si elle est fourni la valeur de la variable. Quelque soit le type de donnée elle l’affiche comme le fait un print_r avec un objet.
Puis elle affiche la trace de tous les appels de fonction avec le nom du fichier et la ligne d’appel.
Je l’ai intégré à mon chargeur de configuration et peut donc l’activer par un simple debug = true dans le fichier de configuration.

un exemple d’affichage

Dans GroupController.php:55
user => stdClass Object
(
    [usr_id] => 1
    [usr_ident] => juat6340
    [usr_name] => Terrien
    [usr_firstname] => Jean-Yves
    [usr_mail] => jyterrien@orange.com
)

    * Appel de : Adm_GroupController::showListAction()
      dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Action.php:357
    * Appel de : Fast_Controller_Action::dispatch()
      dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Dispatcher.php:78
    * Appel de : Fast_Controller_Dispatcher::dispatch()
      dans E:\Htdocs\Fast_Framework\library\Zend\Controller\Front.php:911
    * Appel de : Zend_Controller_Front::dispatch()
      dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Front.php:117
    * Appel de : Fast_Controller_Front::run()
      dans E:\Htdocs\Fast_Framework\index.php:62

A+JYT