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.