世界人のBlog

Les expériences Zend de Sekaijin

Gestion de contexte applicatif.

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.

  1.    /* methode contexte */
  2.     /**
  3.     * Crée un espace associé à l’action courante dans la session
  4.     * et l’attache au controller.
  5.     *
  6.     */
  7.     protected function _createContext($name)
  8.     {
  9.         $this->context = new Zend_Session_Namespace($name);
  10.     }

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.

  1.    /**
  2.     * trouve l’espace associé à un autre processus dans la session
  3.     *
  4.     */
  5.     protected function _getContext($name)
  6.     {
  7.         return new Zend_Session_Namespace($name);
  8.     }

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

  1.    /**
  2.     * Supprime un contexte par son nom
  3.     *
  4.     */
  5.     protected function _deleteContext($name)
  6.     {
  7.         if ($name) {
  8.             $acontext = new Zend_Session_Namespace($name);
  9.             $acontext->unsetAll();
  10.         }
  11.     }

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.

  1.    protected function _openHistory()
  2.     {
  3.         if (((is_array($this->_useHistory)) &&
  4.         (in_array(ereg_replace(‘Action$’, , $this->_request->getActionName()) , $this->_useHistory)))||
  5.         true === $this->_useHistory)
  6.         {
  7.             $history = new Zend_Session_Namespace(‘History’);
  8.             $key = $this->makeActionPath();
  9.             if (!$this->_inHistory($key)) {
  10.                 $keys = array_keys(iterator_to_array($history->getIterator()));
  11.                 if ($keys) $previous = array_pop($keys);
  12.                 $history->{$key} =  new StdClass();
  13.                 $history->{$key}->path =  $key;
  14.                 if (isset($previous)) {
  15.                     $history->{$key}->previous = &$history->$previous;
  16.                 } else {
  17.                     $history->{$key}->previous = (object)array(‘path’ => ‘/’);
  18.                 }
  19.             } else {
  20.                 if (isset($history->{$key}->context))
  21.                 $context = $history->{$key}->context;
  22.                 $keys = array_reverse(array_keys(iterator_to_array($history->getIterator())));
  23.                 foreach ($keys as $akey) {
  24.                     if ($akey == $key) {
  25.                         break;
  26.                     } else {
  27.                         if (isset($history->{$akey}->context) && (
  28.                         (isset($context)&&$history->{$akey}->context != $context)||
  29.                         (!isset($context)))
  30.                         )
  31.                         {
  32.                             $this->_deleteContext($history->{$akey}->context);
  33.                         }
  34.                         unset($history->{$akey});
  35.                     }
  36.                 }
  37.             }
  38.             $this->history = $history->{$key};
  39.         } else {
  40.             return null;
  41.         }
  42.     }
  43.  
  44.     /* methode contexte */
  45.     /**
  46.     * Créé un espace associé à l’action courante dans la session
  47.     * et l’attache au controller.
  48.     *
  49.     */
  50.     protected function _createContext($name = NULL)
  51.     {
  52.         if (!$name) $name = $this->makeActionPath();
  53.         $this->context = new Zend_Session_Namespace($name);
  54.         if ($this->_useHistory) {
  55.             $history = new Zend_Session_Namespace(‘History’);
  56.             #$history->{$this->makeActionPath()} = new StdClass();
  57.             $this->history->context = $name;
  58.         }
  59.     }
  60.  
  61.     /**
  62.     * Supprime un contexte par son nom
  63.     *
  64.     */
  65.     protected function _deleteContext($name)
  66.     {
  67.         if ($name) {
  68.             $acontext = new Zend_Session_Namespace($name);
  69.             $acontext->unsetAll();
  70.         }
  71.     }
  72.  
  73.     /**
  74.     * trouve l’espace associé à l’action précédente dans la session
  75.     * l’associe à l’action courante et l’attache au controller.
  76.     *
  77.     */
  78.     protected function _linkContext($name = NULL)
  79.     {
  80.         if ($this->_useHistory) {
  81.             if (!$name) {
  82.                 if (isset($this->history->previous->context)) {
  83.                     $name = $this->history->previous->context;
  84.                 } else {
  85.                     $name = $this->makeActionPath();
  86.                 }
  87.             }
  88.             $this->context = self::_getContext($name);
  89.             if (isset($this->history)) $this->history->context = $name;
  90.         } else {
  91.             Zend_Loader::loadClass(‘Fast_Controller_Exception’);
  92.             throw new Fast_Controller_Exception(Fast_Controller_Exception::INVALID_HISTORY);
  93.         }
  94.     }
  95.  
  96.     /**
  97.     * trouve l’espace associé à un autre processus dans la session
  98.     *
  99.     */
  100.     protected function _getContext($name = NULL)
  101.     {
  102.         if (!$name) {
  103.             if ($this->_useHistory && isset($this->history->previous->context)) {
  104.                 $name = $this->history->previous->context;
  105.             } else {
  106.                 $name = $this->makeActionPath();
  107.             }
  108.         }
  109.         return new Zend_Session_Namespace($name);
  110.     }

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.

4 Comments
taintedsong.com taintedsong.com taintedsong.com

Gestion dynamique de la navigation.

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

  1. class Client_ManagerController
  2. extends Fast_Controllers_Action
  3. {
  4.     //utiliser automatiquement l’historique
  5.     protected $_useHistory = true;
  6.  
  7.     public function editAction() {
  8.         //créer un contexte pour le processus
  9.         //d’édition client
  10.         $this->_createContext();
  11.         $this->context->returnPath =
  12.             $this->history->previous->path;
  13.         $id = $this->_request->get(‘id’);
  14.         $this->context->fromData =
  15.             $this->model->getClientById($id);
  16.         $this->context->saveMethod = ‘update’;
  17.         …
  18.     }
  19.  
  20.     public function showFormAction()
  21.     {
  22.         //utiliser le contexte du processus
  23.         //d’édition client
  24.         $this->_linkContext();
  25.         if (isset($this->context->saveMethod)) {
  26.             //si le contexte contient le nécessaire
  27.             //on affiche le formulaire
  28.             …
  29.             $this->view->cancelButton =
  30.                 $this->_buttons[‘cancel’];
  31.             $this->view->saveButton =
  32.                 $this->_buttons[’save’];
  33.             …
  34.         } else {
  35.             //l’action à été appelée alors qu’on
  36.             //n’a pas les éléments nécessaire
  37.             //on annule l’action, et on revient
  38.             //à l’action précédente. Mais on ne
  39.             //peut pas savoir si un point de retour
  40.             //est dans le contexte.
  41.             trigger_error(
  42.                 ‘Invalid context to call showFormAction’,
  43.                 E_USER_WARNING);
  44.             $redirect = $this->history->previous->path;
  45.             $this->_redirect($redirect);
  46.         }
  47.     }
  48.     …
  49.  }

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.

10 Comments
taintedsong.com taintedsong.com taintedsong.com

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.

  1.         $messenger = new Zend_Session_Namespace(‘messenger’);
  2.          $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.

  1.      $messenger = new Zend_Session_Namespace(‘messenger’);
  2.       $this->view->messages = $messenger;
  3.       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.

  1. <?php
  2. /**
  3.  * Centralisation des messages utilisateur.
  4.  * Fabrication d’une collection de messages typés.
  5.  * Le messager garde les Messages en session tant qu’il n’y a pas d’affichage.
  6.  * Lors une vue est généré l’ensemble des messages gardés par le messager sont passé à la vue.
  7.  *
  8.  * @package Fast
  9.  * @copyright  ftgroup
  10.  * @author Jean-Yves Terrien
  11.  * @author Patrick Dubois
  12.  * @subpackage Fast_Controller
  13.  */
  14. class Fast_Controller_Messenger
  15. {
  16.     /**
  17.      * Liste des messages
  18.      *
  19.      * @var array of stdClass
  20.      */
  21.     private $_messages = array();
  22.  
  23.     /**
  24.      * espace de stockage des messages
  25.      *
  26.      * @var Zend_Session_Namespace
  27.      */
  28.     private $_messenger;
  29.  
  30.     const NORMAL  = ‘NORMAL’;
  31.     const NOTICE  = ‘NOTICE’;
  32.     const WARNING = ‘WARNING’;
  33.     const ERROR   = ‘ERROR’;
  34.     const OK      = ‘OK’;
  35.  
  36.  
  37.     /**
  38.     * Constructeur.
  39.     *
  40.     */
  41.     public function __construct()
  42.     {
  43.         $this->_messenger = new Zend_Session_Namespace(‘Fast_Controller_Messenger’);
  44.         if (isset($this->_messenger->messages)) $this->_messages = &$this->_messenger->messages;
  45.     }
  46.  
  47.     /**
  48.     * met en session un objet standard contenant tous les membres
  49.     * de l’objet Messenger.
  50.     *
  51.     * @return null
  52.     */
  53.     public function write()
  54.     {
  55.         $this->_messenger->messages = $this->_messages;
  56.     }
  57.  
  58.     /**
  59.     * vide tous les messages de l’objet Messenger.
  60.     *
  61.     * @return null
  62.     */
  63.     public function clear()
  64.     {
  65.         $this->_messenger->messages = null;
  66.         $this->_messenger->messages = $this->_messages = array();
  67.     }
  68.  
  69.     /**
  70.     * Ajoute un message à la collection de messages.
  71.     *
  72.     * @param string $msg Texte du message
  73.     * @param string $type Type du message (Messenger::NORMAL, Messenger::NOTICE, Messenger::WARNING, Messenger::ERROR, Messenger::OK)
  74.     */
  75.     public function add($msg, $type=self::NORMAL)
  76.     {
  77.         $obj = new StdClass();
  78.         $obj->label = $msg;
  79.         $obj->type = $type;
  80.         $this->_messages[] = $obj;
  81.     }
  82.  
  83.     /**
  84.     * Ajoute un message de simple information.
  85.     *
  86.     * Alias de add($msg)
  87.     *
  88.     * @param string $msg Texte du message
  89.     */
  90.     public function addNormal($msg)
  91.     {
  92.         $this->add($msg, self::NORMAL);
  93.     }
  94.  
  95.     /**
  96.     * Ajoute un message indiquant que les opérations se sont
  97.     * déroulées avec succès.
  98.     *
  99.     * Alias de add($msg, Messenger::OK)
  100.     *
  101.     * @param string $msg Texte du message
  102.     */
  103.     public function addOk($msg)
  104.     {
  105.         $this->add($msg, self::OK);
  106.     }
  107.  
  108.     /**
  109.     * Ajoute un message indiquant un avertissement, sans conséquence
  110.     * sur le déroulement des opérations.
  111.     *
  112.     * Alias de add($msg, Messenger::NOTICE)
  113.     *
  114.     * @param string $msg Texte du message
  115.     */
  116.     public function addNotice($msg)
  117.     {
  118.         $this->add($msg, self::NOTICE);
  119.     }
  120.  
  121.     /**
  122.     * Ajoute un message indiquant un avertissement fort, la suite
  123.     * des opérations pouvant être compromise.
  124.     *
  125.     * Alias de add($msg, Messenger::WARNING)
  126.     *
  127.     * @param string $msg Texte du message
  128.     */
  129.     public function addWarning($msg)
  130.     {
  131.         $this->add($msg, self::WARNING);
  132.     }
  133.  
  134.     /**
  135.     * Ajoute un message indiquant une erreur, la suite
  136.     * des opérations pouvant être gravement compromise, voire arrêtée.
  137.     *
  138.     * Alias de add($msg, Messenger::ERROR)
  139.     *
  140.     * @param string $msg Texte du message
  141.     */
  142.     public function addError($msg)
  143.     {
  144.         $this->add($msg, self::ERROR);
  145.     }
  146.  
  147.     /**
  148.     * Retourne le tableau de messages.
  149.     *
  150.     * @return array
  151.     */
  152.     public function getAll()
  153.     {
  154.         return $this->_messages;
  155.     }
  156. }

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.

  1. <?php
  2. Zend_Loader::loadClass("Zend_Controller_Action");
  3. Zend_Loader::loadClass(‘Zend_Session_Namespace’);
  4. Zend_Loader::loadClass(‘Fast_Controller_Messenger’);
  5. /**
  6. *
  7. *
  8. * @package Fast
  9. * @copyright  ftgroup
  10. * @uses Zend_Controller_Front
  11. * @subpackage Fast_Controller
  12. * @author Patrick Dubois
  13. * @author Jean-Yves Terrien
  14. */
  15. class Fast_Controller_Action extends Zend_Controller_Action
  16. {
  17.     protected $_messenger;   /* @var $_messenger Messenger */

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

  1.    public function init()
  2.     {
  3.         parent::init();
  4.         $this->_messenger = new Fast_Controller_Messenger();
  5.     }

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

  1.    function postDispatch()
  2.     {
  3.         if (!$this->_redirected) {
  4.             $this->view->messages = $this->_messenger->getAll();
  5.             $this->_messenger->clear();
  6.         }
  7.         parent::postDispatch();
  8.     }

La méthode render pour les appels explicites

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

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.

  1. <?php
  2. class FormController extends Zend_Controller_Action
  3. {
  4.    /**
  5.     * Affiche la liste des éléments
  6.     *
  7.     * @return null
  8.     */
  9.    public function showListAction(){
  10.       $this->view->list = $this->model->getObjectList();
  11.    }
  12.  
  13.    /**
  14.     * Prépare un enregistrement pour l’afficher dans le formulaire
  15.     * redirige vers showForm
  16.     * @see showForm
  17.     */
  18.    public function addAction() {
  19.       $context = new Zend_Session_Namespace(‘context’);
  20.  
  21.       $context->returnPath = ‘/FormController/showList’;
  22.       $context->saveMethod = ‘add’;
  23.       // Demander au model un nouvel enregistrement avec les valeurs par défaut
  24.       $context->formData = $this->model->newObject();
  25.       $this->_redirect(‘/FormController/showForm’);
  26.    }
  27.  
  28.    /**
  29.     * Recherche l’enregistrement pour l’afficher dans le formulaire
  30.     * redirige vers showForm
  31.     * @see showForm
  32.     */
  33.    public function editAction() {
  34.       $context = new Zend_Session_Namespace(‘context’);
  35.  
  36.       $context->returnPath = ‘/FormController/showList’;
  37.       $context->saveMethod = ‘update’;
  38.  
  39.       $id = $this->_request->get(‘id’);
  40.       $context->formData = $this->model->getItemById($id);
  41.       if (!$context->formData) {
  42.          // revenir au point de retour
  43.          $this->_messenger->addError(‘no object for: ‘.$id);
  44.          $redirect = $context->returnPath;
  45.       } else {
  46.          $redirect = ‘/FormController/showForm’;
  47.       }
  48.       $this->_redirect($redirect);
  49.    }
  50.  
  51.  
  52.    /**
  53.     * Affiche le formulaire d’édition d’un élément.
  54.     *
  55.     * @return null
  56.     */
  57.    public function showFormAction(){
  58.       $context = new Zend_Session_Namespace(‘context’);
  59.  
  60.       $this->view->cancelAction = $context->returnPath;
  61.       $this->view->saveAction = ‘/FormController/checkForm/’;
  62.  
  63.       $this->view->form = clone($context->formData);
  64.    }
  65.  
  66.    /**
  67.     * Récupère les données du formulaire les filtres et les vérifie
  68.     * redirige vers save si le formulaire est valide showFrom sinon
  69.     * @see save
  70.     */
  71.    public function checkFormAction() {
  72.       $context = new Zend_Session_Namespace(‘context’);
  73.       if ($context->formData = $this->_request->get(‘form’))
  74.       // vérification
  75.       $ok = true;
  76.       if ($ok) {
  77.          $redirect = ‘/FormController/save’;
  78.       } else {
  79.          $this->_messenger->addError(‘invalid datas’);
  80.          $redirect = ‘/FormController/showForm’;
  81.       }
  82.       $this->_redirect($redirect);
  83.    }
  84.  
  85.    /**
  86.     * enregistre l’élément dans la collection
  87.     * redirige vers l’action qui précédé l’action add ou edit en cas de succès
  88.     * vers showForm en cas d’échec
  89.     * @see add
  90.     * @see edit
  91.     * @see showForm
  92.     */
  93.    public function saveAction($perform = true) {
  94.       $context = new Zend_Session_Namespace(‘context’);
  95.  
  96.       $data = $context->formData;
  97.       $method = $context->saveMethod;
  98.  
  99.       $ok = $this->model->saveObject($data, $method);
  100.       if ($ok) {
  101.          $this->_messenger->addOk(‘data saved’);
  102.          $redirect = $context->returnPath;
  103.       } else {
  104.          $this->_messenger->addError(‘error data could not be saved’);
  105.          $redirect = ‘/FormController/showForm’;
  106.       }
  107.       $this->_redirect($redirect);
  108.    }
  109. }

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 Comments
taintedsong.com taintedsong.com taintedsong.com

Auto-Jointure

Dans mon article « Ajouter un champ calculé dans une table. » Je parlais d’une méthode pour ajouter des champs dans les objets row issue d’une table qui ne les contient pas base.
La version 1.5 de ZF a introduit une nouvelle classe qui facilite ce genre de manipulation.

L’Auto Jointure simple

Cette petite nouveauté introduit de façon claire les select qui sous-tend la classe Zend_Db_Table. Pour voir comment cette introduction change la donne je me suis posé le problème de l’auto jointure. J’ai une table principale et une où plusieurs table de références.
Le but de l’autojointure et de remonter avec les éléments de la table les valeurs de références. Par exemple les éléments de ma table principale sont typés, il contient un id sur la table de référence des types. Je veux lorsque je lis un enregistrement pouvoir remonter le label de sont type sans avoir systématiquement à faire une deuxième requête.
Les données complémentaires étant des données de référence leur valeur ne change pas (pas lorsqu’on manipule un élément de la table principale) je ne poserais donc pas ici le problème de l’écriture en base.

Zend_Db_Table_Select

Voyons comment fonction les classes de Zend_Db lorsque nous lisons dans la table. L’appel des méthodes fetch, fetchCols, fetchRow, fetchAll, et find font tous appels à _fetch. Dans les versions antérieures à la 1.5 la méthode _fetch recevais les paramètres de la requête. Where et autres order ou limit. La méthode _fetch fabriquait donc un select avec $this->db->select() et y appliquait les clauses adéquates.
Avec la version 1.5 les fonctions suscitées vont-elles-même créer l’objet select et lui ajouter les clauses nécessaires. Pour éviter que chaque fonction réinvente la roue elles ne vont pas créer un simple select mais un Zend_Db_Table_Select dont la base est simplement
Select * From tablename. Pour cela une nouvelle méthode à été ajouté à Zend_Db_Table : select il est donc possible de demander à la table un select préconfiguré pour le manipuler à sa sauce. Nous allons donc nous arranger pour que ce select ne soit pas un simple Zend_Db_Table_Select mais pour qu’il ajoute automatiquement une ou plusieurs jointures à la table.

Fast_Db_Table

Une classe table acceptant l’autojointure. Notre but est de définir une classe comme Zend_Db_Table_Abstract que nous pourrons dériver pour la mapper sur les éléments de la base. On va donc introduire un membre qui listera les jointures à effectuer dans les requêtes. Et tant que nous y sommes nous allons ajouter des restrictions automatique (clauses where ajouté systématiquement)

  1. /**
  2.  * Definition de base d’une table Fast
  3.  * elle étend la classe Zend_Table et lui adjoint un classe spécifique pour les enregistrement
  4.  * ainsi que les méthode courantes d’accès au données
  5.  *
  6.  * @see  Zend_Db_Table
  7.  * @see  Fast_Exception_Db
  8.  * @see  Fast_Db_Row
  9.  * @author Jean-Yves Terrien
  10.  */
  11. Class Fast_Db_Table extends Zend_Db_Table_Abstract {
  12.  
  13.    const FAST_RESTRICT = ‘fast_restrict’;
  14.    const FAST_AUTOJOIN = ‘fast_autojoin’;
  15.    /**
  16.    * Classname for select , Zend_Db_Table_Select,…
  17.    *
  18.    * @var string
  19.    */
  20.    protected $_selectClass = ‘Fast_Db_Table_Select’;
  21.    /**
  22.    * Restriction for query
  23.    *
  24.    * @var string
  25.    */
  26.    protected $_restrict = null;
  27.    /**
  28.    * Auto Joined table for query
  29.    *
  30.    * @var string
  31.    */
  32.    protected $_autojoin = NULL;
  33.    /**
  34.     * Returns an instance of a Zend_Db_Table_Select object.
  35.     *
  36.     * @return Zend_Db_Table_Select
  37.     */
  38.    public function select()
  39.    {
  40.       if (‘Zend_Db_Table_Select’ == $this->_selectClass) {
  41.          $select = parent::select();
  42.       } else {
  43.          Zend_Loader::loadClass($this->_selectClass);
  44.          $select = new $this->_selectClass($this);
  45.       }
  46.       return $select;
  47.    }
  48.     /**
  49.      * Returns table information.
  50.      *
  51.      * @return array
  52.      */
  53.     public function info()
  54.     {
  55.         $info = parent::info();
  56.         $info[self::FAST_RESTRICT] = $this->_restrict;
  57.         $info[self::FAST_AUTOJOIN] = $this->_autojoin;
  58.         return $info;
  59.     }
  60. }

Voilà la base de notre classe on va lui indiquer la classe Select à utiliser (il faudra qu’elle dérive de Zend_Db_Table_Select) cela permettra de la surcharger. On redéfinit la méthode Select() pour tenir compte de notre classe Select et on redéfini la méthode info pour que la classes Select connaisse les jointures à faire.
La définition de la requête étant faite dans la classe Select c’est tout pour la classe Table.

Fast_Db_Table_Select

Un select pour table à auto jointure.
Il est un peu étonnant de voir que la classe Zend_Db_Table_Select ne crée pas la partie from de la table par défaut. Elle ne le fait qu’au moment de la transformation en chaine. Du coup si on tente de faire le join avant on obtient une exception. Il faut donc respecter cette mécanique pour nous insérer au mieux dans ZF

  1. class Fast_Db_Table_Select extends Zend_Db_Table_Select
  2. {
  3.    private $_autojoined = false;
  4.    private $_useRestrict = true;
  5.    /**
  6.    * Performs a validation on the select query before passing back to the parent class.
  7.    * Ensures that only columns from the primary Zend_Db_Table are returned in the result.
  8.    *
  9.    * @return string This object as a SELECT string.
  10.    */
  11.    public function __toString()
  12.    {
  13.       if (!$this->_autojoined) {
  14.          $this->_autojoined = true;
  15.  
  16.          $fields  = $this->getPart(Zend_Db_Table_Select::COLUMNS);
  17.          $primary = $this->_info[Zend_Db_Table_Abstract::NAME];
  18.          $schema  = $this->_info[Zend_Db_Table_Abstract::SCHEMA];
  19.  
  20.          // If no fields are specified we assume all fields from primary table
  21.          if (!count($fields)) {
  22.             $this->from($primary, ‘*’, $schema);
  23.             $fields = $this->getPart(Zend_Db_Table_Select::COLUMNS);
  24.          }
  25.  
  26.          if ($this->_useRestrict) {
  27.             if (is_string($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
  28.                $restricts[] = $this->_info[Fast_Db_Table::FAST_RESTRICT];
  29.             } elseif (is_array($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
  30.                $restricts = $this->_info[Fast_Db_Table::FAST_RESTRICT];
  31.             } else {
  32.                $restricts = array();
  33.             }
  34.             foreach ($restricts as $restrict) {
  35.                $this->where($restrict);
  36.             }
  37.          }
  38.          if (is_array($this->_info[Fast_Db_Table::FAST_AUTOJOIN])) {
  39.             $this->setIntegrityCheck(false);
  40.             foreach ($this->_info[Fast_Db_Table::FAST_AUTOJOIN] as $join) {
  41.                if (is_array($join)) {
  42.                   $this->join($join[‘table’], $join[‘on’], $join[‘fields’]);
  43.                }
  44.             }
  45.          }
  46.       }
  47.       return parent::__toString();
  48.    }
  49.    /**
  50.       @function setRestrict()
  51.       @param boolean $restrict Use restriction for this select
  52.       @return Fast_Db_Table_Select Description
  53.    */
  54.    function setRestrict($restrict) {
  55.       $this->_useRestrict = $restrict;
  56.       return $this;
  57.    } // end function setRestrict
  58. }

Le premier membre private $_autojoined indique que la jointure à déjà été faite. Vu que nous l’ajoutons lors de la transformation en chaine il ne faudrait pas que le soit plusieurs fois. Un simple echo sur l’objet appelle __toString()
Le second private $_useRestrict = true indique que nous devons ou pas utiliser les clauses restrictives
La méthode setRestrict permet de changer ce mode.
La méthode __toString est semblable à celle de sa classe parente elle ne fait que parcourir le tableau d’auto jointure pour ajouter les clauses join.

Une utilisation

La classe user. Nous allons remonter le champ label du profil en même temps que l’utilisateur.

  1. Class Adm_Model_User_Table extends Fast_Db_Table {
  2.    /**
  3.    * The table name.
  4.    *
  5.    * @var array
  6.    */
  7.    protected $_name = ‘user’;
  8.  
  9.    /**
  10.    * Classname for row
  11.    *
  12.    * @var string
  13.    */
  14.    protected $_rowClass = ‘Adm_Model_User_Row’;
  15.    /**
  16.    * Auto Joined table for query
  17.    *
  18.    * @var string
  19.    */
  20.    protected $_autojoin = array(
  21.       array(‘table’ => ‘profile’,
  22.             ‘on’ =>  ‘profile.prf_id = user.prf_id’,
  23.             ‘fields’ => array(‘prf_label’)
  24.       )
  25.    );
  26.    /**
  27.    * Restriction for query
  28.    *
  29.    * @var string
  30.    */
  31.    protected $_restrict = array(‘profile.prf_valid = 1′);
  32. }

Lorsque nous utiliserons cette classe la requête générée par défaut sera

  1. Select
  2.    user.*,
  3.    profile.prf_label
  4. FROM user
  5. INNER JOIN profile ON (profile.prf_id = user.prf_id)
  6. WHERE profile.prf_valid = 1

Conclusion

Zend_Db_Table_Select apporte une ouverture nouvelle pour adapter au mieux à ses besoins le mapping objet de ZF. Les classes ci-dessus ne sont que des premières versions. Elles sont grandement améliorables. Par exemple pour gérer les jointures plus complexes comme left rigth mais aussi avec des clauses de recoupement etc.

19 Comments
taintedsong.com taintedsong.com taintedsong.com

Ajouter un champ calculé dans une table.

Il arrive parfois qu’il soit intéressant de véhiculer un champ dans un objet de mapping qui n’est pas conservé en base. C’est le cas entre autre des champs calculés. Je veux par exemple un objet facture. Lorsque je manipule ma facture un élément important est le total de la facture. Mais une facture en elle-même est composée de champs qui lui sont propres et de lignes de facturations. Chaque ligne véhicule une partie du total de la facture. Lorsque je manipule la facture je n’ai pas nécessairement besoin de ses lignes de facturations. Par exemple lorsque je vérifie que le montant payé et bien le bon. Inutile de remonter toutes les lignes seul le total m’intéresse. Je peux alors décider de garder en base le total. Il me faudra alors veiller à ce que ce total soit tenu à jour en adéquation avec mes lignes de facturations. Je peux aussi décider de ne pas le garder en base. Mais alors il me faudra le calculer et donc faire deux accès à la base pour obtenir ma facture (sans ses lignes mais avec) son total

Ajouter un champ à un objet de mapping

Une solution consiste à calculer ce champ lors de la lecture en base et de le garder dans un coin. Si je le mets directement dans mon objet de mapping je vais me heurter à quelques difficultés. Par exemple si j’enregistre cet objet l’ORM de Zend va au mieux supprimer le champ, au pire lever une exception et il aura raison. Il ne sait qu’en faire. Le but de cet article est de voir les points à lever pour arriver à cette solution.

la table facture

La toute première étape consiste à crée un classe pour la table facture

  1. Class Model_Facture_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘facture’;
  3.    protected $_rowClass = ‘ Model_Facture_Row’;
  4.    public function __construct($config = array())
  5.    {
  6.       parent::__construct($config);
  7.    $this->_cols[] = ‘fac_total’;
  8.    }
  9. }

Et d’y ajouter une colonne. Ainsi lorsque je sortirais ou entrerait une facture dans ma table le champ fac_total ne sera pas inconnu.
Mais il va falloir aller un peu plus loin si on ne veut pas se retrouver avec des Exception de partout. La première étape passe par le calcul de ce champ. Donc lorsqu’on lit un enregistrement dans la base. ZF est ainsi fait que quelque soit la façon dont vous interrogez votre table il passe toujours par la même méthode. Sauf évidement si vous écrivez vous-même une requête. La méthode qui définit la requête à effectuer sur la base pour lire un ou plusieurs enregistrements s’appelle _fetch. Il nous faut donc la modifier pour obtenir le résultat que nous cherchons. Ainsi toute lecture prendra en compte notre modification. Pour bien comprendre ce que fait cette méthode il suffit de se pencher sur le fonctionnement d’une Zend_Db_Table. De façon générale une Zend_Db_Table c’est

  1. SELECT * FROM tableName ;

Les autres méthodes de recherche ne font qu’ajouter des clauses WHERE ORDER ETC. le but de la méthode _fetch est de construire cette requête.
Moi, je voudrais à la place

  1. SELECT
  2.    facture .*,
  3.    SUM(lig_prix*lig_qte) AS fac_total
  4. FROM facture
  5. INNER JOIN lignes USING (fac_id)
  6. GROUP BY fac_id;

modifier la méthode _fetch

Tout d’abord voyons comment est faite la méthode de Zend

  1.    /**
  2.      * Support method for fetching rows.
  3.      *
  4.      * @param  string|array $where  OPTIONAL An SQL WHERE clause.
  5.      * @param  string|array $order  OPTIONAL An SQL ORDER clause.
  6.      * @param  int          $count  OPTIONAL An SQL LIMIT count.
  7.      * @param  int          $offset OPTIONAL An SQL LIMIT offset.
  8.      * @return array The row results, in FETCH_ASSOC mode.
  9.      */
  10.     protected function _fetch($where = null,
  11.                               $order = null,
  12.                               $count = null,
  13.                               $offset = null)
  14.     {
  15.         // selection tool
  16.         $select = $this->_db->select();
  17.  
  18.         // the FROM clause
  19.         $select->from($this->_name, $this->_cols, $this->_schema);
  20.  
  21.         // the WHERE clause
  22.         $where = (array) $where;
  23.         foreach ($where as $key => $val) {
  24.             // is $key an int?
  25.             if (is_int($key)) {
  26.                 // $val is the full condition
  27.                 $select->where($val);
  28.             } else {
  29.                 // $key is the condition with placeholder,
  30.                 // and $val is quoted into the condition
  31.                 $select->where($key, $val);
  32.             }
  33.         }
  34.  
  35.         // the ORDER clause
  36.         if (!is_array($order)) {
  37.             $order = array($order);
  38.         }
  39.         foreach ($order as $val) {
  40.             $select->order($val);
  41.         }
  42.  
  43.         // the LIMIT clause
  44.         $select->limit($count, $offset);
  45.         // return the results
  46.         $stmt = $this->_db->query($select);
  47.         $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
  48.         return $data;
  49.     }

Cette méthode est, un peu, longue mais au final par très complexe. On voit vite que notre SELECT * est à la ligne

  1. $select->from($this->_name, $this->_cols, $this->_schema);

Et que c’est là qu’il faut intervenir. En effet le reste n’est que l’ajout de clause diverse.
Remplaçons donc

  1. $cols = $this->_cols;
  2. unset($cols[array_search(‘fac_total’,$cols)]);
  3. $select->from($this->_name, $cols, $this->_schema)
  4. ->join(‘lignes’,’ligne.fac_id = facture.fac_id’, array(‘fac_total’ =>Zend_Db_Exp(‘SUM(lig_prix * lig_qte)’)))
  5. ->group(‘fac_id’);

Nous avons maintenant une table qui lit des factures avec leur total.

L’écriture

Tant que nous ne faisons que lire dans la table avec cet objet, nous n’auront pas de problème.
Mais si nous tentons un update ou un insert nous allons avoir un problème. En effet nous allons essayer de mettre à jour dans la base un champ qui n’y est pas. Il nous faut donc retirer ce champ de l’objet. Avant l’enregistrement.

  1.   public function insert(array $data) {
  2.       unset($data[‘fac_total’]);
  3.       parent::insert($data);
  4.    }

Cela n’est en fait pas bien compliqué. La méthode insert va en accord avec la liste des colonnes de la table tenter d’ajouter l’enregistrement avec tous les champs présents dans $this->_cols si un champ de data n’est pas dans la liste, il sera supprimé, mais fac_total y est puisque nous l’avons ajouté. Mais ce champ n’est pas dans la table le moteur SQL va donc rejeter la requête. Il suffit donc de supprimer ce champ des données.
La méthode update à un comportement équivalent mais légèrement différent. En ce sens que update va tenter de mettre à jour même les champs qui ne son pas présent dans $data. Si je retire simplement le champ fac_total la méthode update tentera de le mettre à null dans la base. Il faut donc retirer le champ des données mais aussi de la liste des colonnes. Et le restituer ensuite car sinon notre objet table sera incohérent.

  1.    public function update(array $data, $where)
  2.     {
  3.       unset($this->_cols[array_search(‘fac_total’,$this->_cols)]);
  4.       unset($data[‘fac_total’]);
  5.       $res = parent::update($data, $where);
  6.       $this->_cols[] = ‘fac_total’;
  7.       return $res;
  8.     }

Il est ainsi possible d’ajouter de nombreux champs dans un objet table qui ne serons pas stockés en base. Il est assez simple de généraliser cette méthode. En se basant sur la description des relations dans les Zend_Db_Table on peut imaginer une classe abstraite qui contiendrait un tableau des autoJoinnedTable et qui implémenterait ce principe.

Un exemple généralisé

Pour ma part je l’utilise dans un tout autre contexte. J’utilise des tables hiérarchiques en POO il est simple et efficace d’utiliser une référence sur l’objet parent pour constituer une hiérarchie. Mais cette approche n’est pas performante dans une base de données. Une représentation intervallaire l’est bien mieux. Je vous conseille de lire les articles sur le sujet.
Dans le cas qui m’intéresse, j’ais donc des tables qui on un id numérique unique comme clef et des données. Le principe de la représentation intervallaire consiste à ajouter une borne droite et une borne gauche, j’ai aussi très souvent besoin de niveau hiérarchique relatif. Par exemple tous les nœuds au rand N+1, N+2 et N+3 d’un nœud donné. J’ajoute donc dans ma table un champ level.
Mais du côté PHP il est plus simple d’utiliser une relation père fils que la notion d’intervalle. Ma classe va donc masquer la représentation intervallaire à PHP. Elle va gérer elle-même les transactions nécessaires pour maintenir à jours les bornes des éléments de la table.

  1. Zend_Loader::loadClass(‘Zend_Db_Table’);
  2.  
  3. Class Fast_Db_Hierarchical extends Zend _Db_Table {
  4.  
  5.    /**
  6.    * left field name in table
  7.    *
  8.    * @var string
  9.    */
  10.    protected $_left = NULL;
  11.  
  12.    /**
  13.    * right field name in table
  14.    *
  15.    * @var string
  16.    */
  17.    protected $_right = NULL;
  18.  
  19.    /**
  20.    * level field name in table
  21.    *
  22.    * @var string
  23.    */
  24.    protected $_level = NULL;
  25.  
  26.    /**
  27.    * virtual field name used has id of parent
  28.    *
  29.    * @var string
  30.    */
  31.    protected $_parent = NULL;
  32.  
  33.    public function __construct($config = array())
  34.    {
  35.       parent::__construct($config);
  36.       if (null == $this->_left)
  37.          throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEFT_KEY);
  38.       if (null == $this->_right)
  39.          throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_RIGHT_KEY);
  40.       if (null == $this->_level)
  41.          throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEVEL_KEY);
  42.       if (null == $this->_parent)
  43.          throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_PARENT);
  44.       $this->_cols[] = $this->_parent;
  45.    }
  46.  
  47.    public function getById($id) {
  48.       $rows = $this->find($id);
  49.       if ($rows) {
  50.          return $rows->current();
  51.       }
  52.       return false;
  53.    }
  54.  
  55.    public function deleteById($id) {
  56.       if ($id == 1) return false; // on ne peut supprimer la racine
  57.  
  58.       $this->_db->beginTransaction();
  59.       $parent = $this->_db->select();
  60.       $parent->from($this->_name, array(‘delete_left’ => $this->_left,
  61.                                         ‘delete_right’ => $this->_right))
  62.              ->where($this->_primary[1].‘ = :_deleteId’);
  63.       $statement = $this->_db->prepare($parent);
  64.       $statement->execute(array(‘_deleteId’ => $id));
  65.       list($deleteLeft, $deleteRight) = array_values($statement->fetch());
  66.       $res = false;
  67.       if ($deleteLeft) {
  68.          $row = $this->getById($id);
  69.          $res = $row->delete();
  70.  
  71.          if ($res) {
  72.             $statement = $this->_db->prepare(
  73.                ‘UPDATE ‘.$this->_name.
  74.                SET ‘.$this->_left.‘ = ‘.$this->_left.‘ - 1
  75.                WHERE ‘.$this->_left.‘ >= ‘.$deleteLeft.
  76.                AND ‘.$this->_right.‘ < ‘.$deleteRight.‘;’);
  77.             $statement->execute();
  78.          }
  79.  
  80.          if ($res) {
  81.             $statement = $this->_db->prepare(
  82.                ‘UPDATE ‘.$this->_name.
  83.                SET ‘.$this->_left.‘ = ‘.$this->_left.‘ - 2
  84.                WHERE ‘.$this->_left.‘ >= ‘.$deleteLeft.
  85.                AND ‘.$this->_right.‘ > ‘.$deleteRight.‘;’);
  86.             $statement->execute();
  87.          }
  88.  
  89.          if ($res) {
  90.             $statement = $this->_db->prepare(
  91.                ‘UPDATE ‘.$this->_name.
  92.                SET ‘.$this->_right.‘ = ‘.$this->_right.‘ - 1
  93.                WHERE ‘.$this->_right.‘ >= ‘.$deleteLeft.
  94.                AND ‘.$this->_right.‘ < ‘.$deleteRight.‘;’);
  95.             $statement->execute();
  96.          }
  97.  
  98.          if ($res) {
  99.             $statement = $this->_db->prepare(
  100.                ‘UPDATE ‘.$this->_name.
  101.                SET ‘.$this->_right.‘ = ‘.$this->_right.‘ - 2
  102.                WHERE ‘.$this->_right.‘ >= ‘.$deleteLeft.
  103.                AND ‘.$this->_right.‘ > ‘.$deleteRight.‘;’);
  104.             $statement->execute();
  105.          }
  106.  
  107.          if ($res) {
  108.             $this->_db->commit();
  109.          } else {
  110.             $this->_db->rollback();
  111.          }
  112.       }
  113.       return $res;
  114.    }
  115.  
  116.    public function UpdateById($data) {
  117.       // on ne peut mettre à jour les donnée hiérarchique
  118.       // ie on ne peut déplacer un noeud dans l’arbre.
  119.       unset($data[$this->_parent]); // ne fait pas partie de la table
  120.       unset($data[$this->_left]);   //ne peut être changé
  121.       unset($data[$this->_right]);  //ne peut être changé
  122.       unset($data[$this->_level]);  //ne peut être changé
  123.       $res =  parent::UpdateById($data);
  124.       return $res;
  125.    }
  126.  
  127.    public function insert(array $data) {
  128.       # select left and level of parent
  129.       $parentId = $data[$this->_parent];
  130.  
  131.       $this->_db->beginTransaction();
  132.       $parent = $this->_db->select();
  133.       if (null != $this->_level) {
  134.          $fields = array(‘parent_left’ => $this->_left,
  135.                          ‘parent_level’ => $this->_level);
  136.       } else {
  137.          $fields = array(‘parent_left’ => $this->_left,);
  138.       }
  139.  
  140.       $parent->from($this->_name, $fields)
  141.              ->where($this->_primary[1].‘ = :_parentId’);
  142.       $statement = $this->_db->prepare($parent);
  143.       $statement->execute(array(‘_parentId’ => $parentId));
  144.       list($parentLeft, $parentLevel) = array_values($statement->fetch());
  145.  
  146.       $res = false;
  147.       if ($parentLeft) {
  148.          #update tree
  149.          $statement = $this->_db->prepare(
  150.             ‘UPDATE ‘.$this->_name.
  151.             SET ‘.$this->_left.‘ = ‘.$this->_left.‘ + 2
  152.             WHERE ‘.$this->_left.‘ > ‘.$parentLeft.‘;’);
  153.          $res = $statement->execute();
  154.          if ($res) {
  155.             $statement = $this->_db->prepare(
  156.                ‘UPDATE ‘.$this->_name.
  157.                SET ‘.$this->_right.‘ = ‘.$this->_right.‘ + 2
  158.                WHERE ‘.$this->_right.‘ > ‘.$parentLeft.‘;’);
  159.             $statement->execute();
  160.          }
  161.  
  162.          #insert node
  163.          if ($res) {
  164.             unset($data[$this->_parent]);
  165.             $data[$this->_left] = $parentLeft + 1;
  166.             $data[$this->_right] = $parentLeft + 2;
  167.             if (null != $this->_level)
  168.                $data[$this->_level] = $parentLevel + 1;
  169.             $res =  parent::insert($data);
  170.          }
  171.          if ($res) {
  172.             $this->_db->commit();
  173.          } else {
  174.             $this->_db->rollback();
  175.          }
  176.       }
  177.       return $res;
  178.    }
  179.  
  180.     /**
  181.      * Support method for fetching rows.
  182.      *
  183.      * @param  string|array $where  OPTIONAL An SQL WHERE clause.
  184.      * @param  string|array $order  OPTIONAL An SQL ORDER clause.
  185.      * @param  int          $count  OPTIONAL An SQL LIMIT count.
  186.      * @param  int          $offset OPTIONAL An SQL LIMIT offset.
  187.      * @return array The row results, in FETCH_ASSOC mode.
  188.      */
  189.     protected function _fetch($where = null, $order = null, $count = null, $offset = null)
  190.     {
  191.         // selection tool
  192.         $select = $this->_db->select();
  193.  
  194.         //no _parent col on master table
  195.         $cols = $this->_cols;
  196.         unset($cols[array_search($this->_parent,$cols)]);
  197.  
  198.         // the FROM clause
  199.         $select->from($this->_name, $cols, $this->_schema);
  200.         // add the parent col
  201.         $select->join(array(‘parent’ => $this->_name),
  202.                       ‘(parent.’.$this->_left.‘ < workgroup.’.$this->_left.‘) AND
  203.                       (parent.’.$this->_right.‘ > workgroup.’.$this->_right.‘) AND
  204.                       (parent.’.$this->_level.‘ = workgroup.’.$this->_level.‘ -1)’,
  205.                       array(‘parent_id’ => ‘parent.’.$this->_primary[1].));
  206.  
  207.         // the WHERE clause
  208.         $where = (array) $where;
  209.         foreach ($where as $key => $val) {
  210.             // is $key an int?
  211.             if (is_int($key)) {
  212.                 // $val is the full condition
  213.                 $select->where($val);
  214.             } else {
  215.                 // $key is the condition with placeholder,
  216.                 // and $val is quoted into the condition
  217.                 $select->where($key, $val);
  218.             }
  219.         }
  220.  
  221.         // the ORDER clause
  222.         if (!is_array($order)) {
  223.             $order = array($order);
  224.         }
  225.         foreach ($order as $val) {
  226.             $select->order($val);
  227.         }
  228.  
  229.         // the LIMIT clause
  230.         $select->limit($count, $offset);
  231.         // return the results
  232.         $stmt = $this->_db->query($select);
  233.         $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
  234.         return $data;
  235.     }
  236.  
  237.     public function update(array $data, $where)
  238.     {
  239.       unset($this->_cols[array_search($this->_parent,$this->_cols)]);
  240.       unset($data[$this->_parent]);
  241.       $res = parent::update($data, $where);
  242.       $this->_cols[] = $this->_parent;
  243.       return $res;
  244.     }
  245.  
  246.    protected function _parent($row, $fiels) {
  247.       $parent = $this->_parents($row, $fiels)
  248.              ->order($this->_right)
  249.              ->limit(1);
  250.       return $parent;
  251.    }
  252.    protected function _parents($row, $fiels) {
  253.       $parent = $this->_db->select();
  254.       $parent->from($this->_name, $fiels)
  255.              ->where($this->_left.‘  < ‘.$row->{$this->_left})
  256.              ->where($this->_right.‘ > ‘.$row->{$this->_right});
  257.       return $parent;
  258.    }
  259.    protected function _childs($row, $fiels) {
  260.       $childs = $this->_db->select();
  261.       $childs->from($this->_name, $fiels)
  262.              ->where($this->_left.‘  > ‘.$row->{$this->_left})
  263.              ->where($this->_right.‘ < ‘.$row->{$this->_right});
  264.       return $childs;
  265.    }
  266.  
  267.     /**
  268.      * This is the find Zend_Db_Table Abstract method
  269.      * But the where closes are prefixed by the table name
  270.      *
  271.      * Fetches rows by primary key.
  272.      * The arguments specify the primary key values.
  273.      * If the table has a multi-column primary key, you must
  274.      * pass as many arguments as the count of column in the
  275.      * primary key.
  276.      *
  277.      * To find multiple rows by primary key, the argument
  278.      * should be an array.  If the table has a multi-column
  279.      * primary key, all arguments must be arrays with the
  280.      * same number of elements.
  281.      *
  282.      * The find() method always returns a Rowset object,
  283.      * even if only one row was found.
  284.      *
  285.      * @param  mixed                         The value(s) of the primary key.
  286.      * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria.
  287.      * @throws Zend_Db_Table_Exception
  288.      */
  289.     public function find()
  290.     {
  291.         $args = func_get_args();
  292.         $keyNames = array_values((array) $this->_primary);
  293.  
  294.         if (empty($args)) {
  295.             require_once ‘Zend/Db/Table/Exception.php’;
  296.             throw new Zend_Db_Table_Exception("No value(s) specified for the primary key");
  297.         }
  298.  
  299.         if (count($args) != count($keyNames)) {
  300.             require_once ‘Zend/Db/Table/Exception.php’;
  301.             throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
  302.         }
  303.  
  304.         $whereList = array();
  305.         $numberTerms = 0;
  306.         foreach ($args as $keyPosition => $keyValues) {
  307.             // Coerce the values to an array.
  308.             // Don’t simply typecast to array, because the values
  309.             // might be Zend_Db_Expr objects.
  310.             if (!is_array($keyValues)) {
  311.                 $keyValues = array($keyValues);
  312.             }
  313.             if ($numberTerms == 0) {
  314.                 $numberTerms = count($keyValues);
  315.             } else if (count($keyValues) != $numberTerms) {
  316.                 require_once ‘Zend/Db/Table/Exception.php’;
  317.                 throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
  318.             }
  319.             for ($i = 0; $i < count($keyValues); ++$i) {
  320.                 $whereList[$i][$keyPosition] = $keyValues[$i];
  321.             }
  322.         }
  323.         $whereClause = null;
  324.         if (count($whereList)) {
  325.             $whereOrTerms = array();
  326.             foreach ($whereList as $keyValueSets) {
  327.                 $whereAndTerms = array();
  328.                 foreach ($keyValueSets as $keyPosition => $keyValue) {
  329.                     $whereAndTerms[] = $this->_db->quoteInto(
  330.                         $this->_db->quoteIdentifier($this->_name).‘.’
  331.                        .$this->_db->quoteIdentifier($keyNames[$keyPosition], true) . ‘ = ?’,
  332.                         $keyValue
  333.                     );
  334.                 }
  335.                 $whereOrTerms[] = ‘(’ . implode(‘ AND ‘, $whereAndTerms) . ‘)’;
  336.             }
  337.             $whereClause = ‘(’ . implode(‘ OR ‘, $whereOrTerms) . ‘)’;
  338.         }
  339.  
  340.         return $this->fetchAll($whereClause);
  341.     }
  342.  
  343. }

Vous aurrez noté la présence de la méthode find alors qu’elle est disponible dans la classe Zend_Db_Table. Cela vient du fait que je fais une auto-jointure je joins la table sur elle-même. Du coup tous les champs de la table sont en double dans la requête. Or la méthode find construit des closes where simple. Il est nécessaire dans ce cas de les préfixer du nom de la table c’est ce que j’ai ajouté à la méthode find.

Conclusion

Cette façon de dériver la classe Zend_Db_Table permet d’imaginer toute sorte de mapping entre un objet et un ensemble de tables dans la base. Par exemple un modèle ou les adresses sont dans une table à part des clients alors que l’objet de mapping remonte toujours l’ensemble. Ou la remonté systématique des valeurs des tables de références etc.

A+JYT
ZIP File : Hierarchical Table

7 Comments
taintedsong.com taintedsong.com taintedsong.com

Utiliser une base de données avec Zend Framwork

Zend Framework contient déjà tout le nécessaire pour accéder à de nombreuse base de données. Je vais aborder ici son utilisation. La littérature sur la connexion à une base est suffisamment dense pour que je considère que celle-ci est déjà faite. Je vais donc me concentrer sur l’accès aux données et leurs manipulations.

Approche conceptuelle

Une base de données est avant tout un gisement organisé de données structurée. Que ce soit une base de donnée relationnelle ou un annuaire, ou encore un fichier de donnée structurée, on se trouve devant deux notion fondamentales. Une collection et des éléments de cette dernière. Dans une base de données les collections sont appelées Tables et les éléments Rows. Une base de donnée relationnelle peut comporter bien d’autre objets mais dans sont fondement elle est constituée de Tables qui contiennent des Rows. Cette séparation distincte de ces deux éléments et primordiale pour obtenir une conception claire de son application. En effet n ne fait pas les mêmes opérations sur une table que sur un row. En même temps on voit que ces deux types d’objet sont intimement liés.

La représentation Objet

Zend Framwork nous fournit deux classe pour représenter ses éléments. Il s’agit de Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract. Comme leur nom l’indique ces classes étant abstraites, ne sont pas utilisables directement. Pour aller au plus simple ZF fournit aussi un classe Zend_Db_Table dérivant de Zend_Db_Table_Abstract qui par simple instanciation permet de manipuler une table. Cette classe fournit déjà pas mal de possibilités mais on peut constater qu’il n’existe pas de classe pour les row correspondante. Simplement parce que de façon très générales un classe Zend_Db_Table_Row ne comporterait rien de particulièrement intéressent. En effet les opérations que l’on peut faire sur un row dépendant complètement du type de données qu’il contient. Et a priori il peut s’agit de n’importe quoi.

Approche par dérivation

Si on définit des classe abstraite c’est pour qu’elles soient dérivées en classes concrètes. Et si les concepteurs de ZF s’en sont donné la peine ce n’est surement pas pour la beauté de la chose. Que peut nous apporter la dérivation des classes Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract ? La première chose que nous apporte la dérivation c’est d’associer une classe drivée de Zend_Db_Table_Abstract à une table particulière de notre base. Par exemple Client_Table lié à la table client de la base. Pour cela il suffit de très peut de chose

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘client’;
  3. }

Après instanciation de cette classe un appel aux méthodes de consultation de la table remontera des rows de la table client.
Ces dernier étant les objets qu’est sensé manipuler mon modèle. La conception de mon application m’a normalement amené à la manipulation de clients et donc un ensemble de méthode pour parvenir à me fins. Mais ainsi définit ma Client_Table me retourne juste des données. Il serait intéressant qu’elle me donne des objets clients c’est justement ce que permet la dérivation de Zend_Db_Table_Row_Abstract.je peux ainsi définir ma classe

  1. Class Client_Row extends Zend_Db_Table_Row_Abstract {
  2. }

J’ai donc une classe pour mes clients à la quelle je vais pouvoir attacher les méthodes que ma conception ma permit de définir. Reste à associer la classe Client_Table à la classe Client_Row cela se fait très simplement.

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘client’;
  3.    protected $_rowClass = ‘Client_Row’;
  4.  }

Je bénéficie maintenant de tous les méthodes de manipulation de ma table définit par ZF et à chaque fois que je récupère un row j’ai entre mes mains un client qui possède toutes les méthodes de manipulation de celui-ci.

le lien inverse

On vient de voir que la classe Client_Table et en relation avec la classe Client_Row en ce sens que tout row sortit de la table est un Client_Row. Mais qu’en est-il dans l’autre sens ? La réponse est La relation existe aussi. Intrinsèquement, la classe Client_Row connait la table qui à donné naissance à ses instances. En clair chaque Client_Row sortit d’une instance de Client_Table contient un référence à cette instance.
Il devient alors simple d’accéder à la table depuis le row. On peut se demander pourquoi. Simplement pour se faciliter la vie.

  1. //trouver l’enregistrement client d’id 25
  2. $aclient = $clientTable->find(25);
  3. $aClient->maMethode();
  4. $aClient->save();

Ce n’est qu’une facilité on pourrait très bien en repasser par une instance de la table pour faire cette mise à jour. Mais le client ayant sa propre instance de la table pourquoi ne pas en profiter. ZF a d’ailleurs prévu cela et fournit la méthode save et quelques autres.
On le voit il n’est pas bien compliqué d’utiliser la table à partir du row.

Et l’ajout de donnée ?

Si je crée une instance de Client_Row directement je n’aurais pas associé la table à mon nouvel objet. J’aurais bien toutes les méthodes définies mais je ne pourrais mettre mon objet en base sans en passer par une instance de la table. Pour recréer se lient on peut, soit, passer part la méthode setTable, soit par le tableau de paramètre du constructeur. Personnellement j’ai l’habitude de demander mes rows à la Table pourquoi ne pas demander les nouveaux rows à la table.

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘client’;
  3.    protected $_rowClass = ‘Client_Row’;
  4.  
  5.    /**
  6.     * Make new row associated to this table.
  7.     *
  8.     * @param StdClass|array $obj OPTIONAL object to cast
  9.     * @return Fast_Db_Row
  10.     */
  11.     public function newRow($obj = null) {
  12.       if ($obj)
  13.       {
  14.          if (is_object($obj))
  15.             $obj = get_object_vars($obj);
  16.          $row = $this->createRow($obj);
  17.       } else {
  18.          $row = $this->createRow();
  19.       }
  20.       return $row;
  21.    }
  22.  }

Ainsi j’ai le même fonctionnement que précédemment.

  1. //Créer un nouvel enregistrement client.
  2. $aclient = $clientTable-> newRow();
  3. $aClient->maMethode();
  4. $aClient->save();

Est-ce tout ?

Nous venons de voir que cette approche permet de faire un mapping d’une classe issue de ma conception sur les enregistrements d’une table de ma base. ZF offre au passage toute la manipulation des relations. Et toutes les fonctionnalités de manipulation des données sur de la table ajout, mise à jour, suppression. Mais cette approche permet aussi d’enrichir cette couverture déjà riche de méthode spécifique à notre modèle de données. Ainsi la classe Client_Table peut être enrichie de toutes les méthodes qui me sont nécessaire et qui doivent me retourner un Client_Row. J’ai un réceptacle naturel pour toutes les requêtes qui retourne un ou des client_Row mais aussi des données ayant rapport direct avec la table client.
Par exemple la méthode getBoutiqueList qui me retourne la liste des noms de boutique trouve sa place dans Boutique_Table.
Je bénéficie au passage d’un nettoyage automatique des champs de mes objets. Il arrive parfois qu’on associe des valeurs à l’ensemble des données d’un formulaire pour se simplifier la vie. Si je donne tous ses champs de formulaire à la méthode newRow mon objet ne contiendra que les valeurs correspondant à un champ de la table les autres étant supprimé au passage. Notez que si vous utilisez des tableaux de donnée à la place d’objet la méthode createRow est là pour vous. De même pour obtenir le tableau des données d’un objet métier vous avez la méthode toArray. Pour ma part j’ai ajouté la méthode toStdClass pour obtenir un objet standard qui est parfois intéressant (pour la mise en session par exemple).

Restrinction d’accès

Les ACL permettent de restreindre l’accès aux fonctionnalités de votre application. Pour restreindre l’étendu des données il est nécessaire d’en passer par des restrictions sur les requêtes. Il est parfaitement possible d’intégrer cette restriction dans la table elle-même

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘client’;
  3.    protected $_rowClass = ‘Client_Row’;
  4.    /**
  5.    * Restriction for query
  6.    * @var string
  7.    */
  8.    protected $_restrict = array(‘cli_level > 0′);
  9.    public function __construct($config = array())
  10.    {
  11.       parent::__construct($config);
  12.       $user = Zend_Auth::getInstance()->getIdentity();
  13.       if ($user) {
  14.          $this->_restrict[] = ‘cli_group IN
  15.          (SELECT grp_id FROM group WHERE usr_id = ‘.$user->usr_id.‘)’;
  16.       } else {
  17.          //sans identité on ne peut rien voir dans la base
  18.          $this->_restrict = ‘false’;
  19.       }
  20.    }
  21.  
  22.     /**
  23.      * Fetches one row in an object of type Zend_Db_Table_Row_Abstract,
  24.      * or returns Boolean false if no row matches the specified criteria.
  25.      *
  26.      * @param string|array $where  OPTIONAL An SQL WHERE clause.
  27.      * @param string|array $order   OPTIONAL An SQL ORDER clause.
  28.      * @param boolean $restrict     OPTIONAL use restrict SQL clause.
  29.      * @return Fast_Db_Row  The row results per the
  30.      *     Zend_Db_Adapter fetch mode, or null if no row found.
  31.      */
  32.    public function fetchRow($where = null,
  33.                             $order = null,
  34.                             $restrict = true)
  35.    {
  36.       if ($restrict&&
  37.           isset($this->_restrict)&&
  38.           is_string($this->_restrict))
  39.       {
  40.          if (is_array($where))
  41.          {
  42.             $where[] = $this->_restrict;
  43.          } else {
  44.             $where = ‘(’.$this->_restrict.‘) AND (’.$where.‘)’;
  45.          }
  46.       } elseif ($restrict&&
  47.                 isset($this->_restrict)&&
  48.                 is_array($this->_restrict))
  49.       {
  50.          if (is_array($where))
  51.          {
  52.             $where = array_merge($where, $this->_restrict);
  53.          } else {
  54.             foreach ($this->_restrict as $contraint) {
  55.                $where = ‘(’.$contraint.‘) AND (’.$where.‘)’;
  56.             }
  57.          }
  58.       }
  59.       $res = parent::fetchRow($where,$order);
  60.       return $res;
  61.    }
  62.  
  63.    /**
  64.     * Make new row associated to this table.
  65.     *
  66.     * @param StdClass|array $obj OPTIONAL object to cast
  67.     * @return Fast_Db_Row
  68.     */
  69.     public function newRow($obj = null) {
  70.       if ($obj)
  71.       {
  72.          if (is_object($obj))
  73.             $obj = get_object_vars($obj);
  74.          $row = $this->createRow($obj);
  75.       } else {
  76.          $row = $this->createRow();
  77.       }
  78.       return $row;
  79.    }
  80.  }

Ainsi lorsqu’on fait appel à une méthode d’extraction de données sur la table la restriction s’applique automatiquement. Notez que j’ai prévu de pouvoir débrailler cette restriction avec le paramètre supplémentaire $restrict cela peut parfois s’avérer utile.

Conclusion

Cette approche conceptuelle permet facilement de coller la conception en classe sur son modèle de donnée. Elle permet un découpage logique de la partie métier de son application. Mais elle ne résous pas tout. Il reste en effet des requêtes qu’on ne sait pas toujours où placer. Mais les possibilités d’extensions de ses classes est grand et laisse libre court à votre imagination.
A+JYT

20 Comments
taintedsong.com taintedsong.com taintedsong.com

De la granularité des actions.

Le modèle MVC propose de découper son application en actions regroupées dans des contrôleurs. Une question qu’on est amené naturellement à se poser dans ce contexte est : quel est le bon le découpage de l’application en divers contrôleurs. Pour répondre à cette question il existe des méthodes de conceptions qui permettent des découpages logique de l’application en sous ensembles connexes. De ces approches on trouve facilement les contrôleurs qui vont composer l’application. Mais quid des actions qui composent ses contrôleurs ? Quel niveau de granularité adopter pour décider des actions à implémenter ? Si l’on regarde globalement l’application, en imaginant que comme sous MVC nous ayons qu’un seul script nous aurions une espèce d’immense « Switch » pour déterminer quoi faire et dans quelles conditions. Heureusement avec MVC le dispatcher et là pour assurer cet aiguillage. Au niveau de notre contrôleur nous pouvons faire une seule actions qui à sont tour va devoir en fonction du contexte déterminer ce qu’il y a à faire. Nous n’aurons alors peut être pas un gros « Switch », mais probablement un ou plusieurs « If ». On trouve dans les exemples beaucoup de contrôleurs dont le code ressemble à ceci

  1. if ($this->_request->isPost()) {
  2.    $formvar = $f->filter($this->_request->getPost(‘varname’));
  3.    if (empty($formvar)) {
  4.       $this->view->message = ‘Please provide a …’;
  5.    } else {
  6.       …
  7.       if ($result->isValid()) {
  8.          …
  9.       } else {
  10.          …
  11.       }
  12.    }
  13. } else {

Dans cet exemple on va utiliser la présence ou pas de donnée de formulaire pour savoir où on en est au cours du processus de gestion d’un formulaire. Dans le cas où on à des données on va alors tenter de faire le nécessaire, et dans le cas contraire afficher le formulaire pour la saisie. À ce moment la on va devoir déterminer si on est en cours d’une édition où d’un ajout etc.
Au final suivant la complexité de l’objet à traiter on va se retrouver avec un gros paquet de « if » imbriqués. Ne pourrait-on pas tout comme on l’a fait au niveau général pour découper l’application en contrôleurs, découper la gestion de notre formulaire en une succession d’actions ? La réponse est oui évidemment. Mais dans ce cas quand s’arrêter ? Car si je commence à découper le gros algorithme de gestion de mon formulaire en plus petits bouts, je peux très bien découper et découper encore jusqu’à obtenir une seule instruction pas actions ce qui deviendrait plutôt compliqué. Là encore il faut trouver un juste milieu.

Une action atomique.

Avec le temps et l’expérience, j’en suis arrivé à la même conclusion que pour le découpage en contrôleur. Le bon niveau est celui qui permet d’avoir un découpage logique et connexe. Reprenons l’exemple de la gestion d’un formulaire. Au niveau général on peut découper le processus de gestion de la façon suivante :

  • Préparer le formulaire
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données
  • Voilà déjà un découpage qu’on peut faire et qui peut être déporté vers des actions différentes. La préparation consiste soit à récupérer l’enregistrement à éditer soit à pré remplir un enregistrement en vu d’un ajout, l’affichage ne fait que gérer la vue, la vérification récupère les données du formulaire et vérifie quelles sont acceptable. Et traiter les données sera par exemple l’enregistrement en base. Logiquement on voit que les actions Préparer, Vérifier et traiter n’affiche rien, elles se terminent donc par un « rediect » vers une autre action. Seule l’action Afficher utilise une vue. Peut-on aller plus loin, ou avons-nous atteint un niveau logique unitaire ? En clair chaque action ainsi déterminé fait-elle une action unique ou doit elle encore déterminer par rapport à son contexte des traitements différents à effectuer ?
    Si on regarde de plus près on voit que l’action préparer fait soit une édition soit un pré remplissage. On peut donc la couper en deux. Les autres n’ont pas de choix à faire autre que ceux induit par les données. Ses choix ne relève pas de la logique d’enchaînement mais des données traitées. Ainsi la vérification va faire un « redirect » soit vers le traitement soit vers l’affichage suivant que les données sont valides ou pas. On peut donc affiner se découpage ainsi :

  • Préparer formulaire pour un ajout
  • Rechercher les données de l’enregistrement à éditer
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données
  • À ce niveau là on a un découpage logique du traitement d’un formulaire. Il faut noter qu’on travaille sur un contrôleur qui doit traiter un formulaire. Donc il n’est pas question d’aborder le traitement du métier, ni la façon de coder la vue. On peut aussi remarquer qu’en agissant ainsi on a déterminé un algorithme général de traitement des formulaires.

    Tout cela sert-il à quelque chose ?

    On légitimement se poser la question. Rien ne m’empêche de faire cinq méthodes privées appelées par une seule action pour arriver au résultat. J’aurais un découpage clair et je n’aurais qu’une action. L’action contiendra alors un aiguillage vars la fonction à effectuer. Les enchaînements de fonction se faisant simplement. Or le dispatcher de MVC sert justement à faire les aiguillages. En reportant ses fonctions vers des actions on va utiliser le dispatcher plutôt que d’écrire l’aiguillage. En imaginant qu’une autre partie de l’application ait besoin que d’une partie de ses fonctions il suffira d’enchaîner vers celle-ci pour obtenir le résultat. Si une nouvelle étape intermédiaire (une confirmation par exemple) venait à devoir être insérer il suffirait de changer les redirections. Bref en passer par les actions va permettre un de ne pas écrire de code pour aiguiller les actions, le dispatcher le faisant très bien, et au passage ajouter de la souplesse dans l’évolution de l’application.
    Mais il y a un autre petit avantage. C’est que finalement il est possible d’écrire les enchaînements sans savoir quels objets un traite. Je n’ai à aucun moment discourus sur l’objet que traitait mon formulaire. Or je peux déjà écrire le contrôleur et ses enchaînements.

    Et la transmission des données

    Reste qu’à changer d’action, j’ai introduit une complexité nouvelle. En effet lorsque dans une fonction je passe les valeurs issues d’une fonction à une autre je n’ai pas de problème. Lorsque je passe d’une action à une autre je perds toutes mes variables. En effet une redirection est un nouvel appel au contrôleur. Je vais devoir utiliser la session pour les conserver. De même lorsque je n’ai qu’une action pour faire afficher un message, je le mets dans la vue. Lorsque j’en ai plusieurs il me faut les garder dans la session.

    Un prototype de contrôleur

    Je vais ici donner un prototype d’un tel contrôler. Je reviendrais dessus dans un article ultérieur car on va voir que la gestion des messages et de la session peut être rendu générique pour toute l’application. Ce qui fera l’objet d’un article prochain.

    1. <?php
    2. class FormController extends Zend_Controller_Action
    3. {
    4.    /**
    5.     * Affiche la liste des éléments
    6.     *
    7.     * @return null
    8.     */
    9.    public function showListAction(){
    10.       $messenger = new Zend_Session_Namespace(‘messenger’);
    11.       $this->view->list = $this->model->getObjectList();
    12.       $this->view->messages = $messenger;
    13.       unset($messenger); //on supprime les messages ils sont affiché on en a plus besoin en session
    14.    }
    15.  
    16.    /**
    17.     * Prépare un enregistrement pour l’afficher dans le formulaire
    18.     * redirige vers showForm
    19.     * @see showForm
    20.     */
    21.    public function addAction() {
    22.       $context = new Zend_Session_Namespace(‘context’);
    23.       $messenger = new Zend_Session_Namespace(‘messenger’);
    24.  
    25.       $context->returnPath = ‘/FormController/showList’;
    26.       $context->saveMethod = ‘add’;
    27.       // Demander au model un nouvel enregistrement avec les valeurs par défaut
    28.       $context->formData = $this->model->newObject();
    29.       $this->_redirect(‘/FormController/showForm’);
    30.    }
    31.  
    32.    /**
    33.     * Recherche l’enregistrement pour l’afficher dans le formulaire
    34.     * redirige vers showForm
    35.     * @see showForm
    36.     */
    37.    public function editAction() {
    38.       $context = new Zend_Session_Namespace(‘context’);
    39.       $messenger = new Zend_Session_Namespace(‘messenger’);
    40.  
    41.       $context->returnPath = ‘/FormController/showList’;
    42.       $context->saveMethod = ‘update’;
    43.  
    44.       $id = $this->_request->get(‘id’);
    45.       $context->formData = $this->model->getItemById($id);
    46.       if (!$context->formData) {
    47.          // revenir au point de retour
    48.          $messenger = ‘no object for: ‘.$id;
    49.          $redirect = $context->returnPath;
    50.       } else {
    51.          $redirect = ‘/FormController/showForm’;
    52.       }
    53.       $this->_redirect($redirect);
    54.    }
    55.  
    56.  
    57.    /**
    58.     * Affiche le formulaire d’édition d’un élément.
    59.     *
    60.     * @return null
    61.     */
    62.    public function showFormAction(){
    63.       $context = new Zend_Session_Namespace(‘context’);
    64.       $messenger = new Zend_Session_Namespace(‘messenger’);
    65.  
    66.       $this->view->cancelAction = $context->returnPath;
    67.       $this->view->saveAction = ‘/FormController/checkForm/’;
    68.  
    69.       $this->view->form = clone($context->formData);
    70.       $this->view->messages = $messenger;
    71.       unset($messenger); //on supprime les messages ils sont affiché on en a plus besoin en session
    72.    }
    73.  
    74.    /**
    75.     * Récupère les données du formulaire les filtres et les vérifie
    76.     * redirige vers save si le formulaire est valide showFrom sinon
    77.     * @see save
    78.     */
    79.    public function checkFormAction() {
    80.       $context = new Zend_Session_Namespace(‘context’);
    81.       if ($context->formData = $this->_request->get(‘form’))
    82.       // vérification
    83.       $ok = true;
    84.       if ($ok) {
    85.          $redirect = ‘/FormController/save’;
    86.       } else {
    87.          $messenger = new Zend_Session_Namespace(‘messenger’);
    88.          $messenger = ‘invalid datas’;
    89.          $redirect = ‘/FormController/showForm’;
    90.       }
    91.       $this->_redirect($redirect);
    92.    }
    93.  
    94.    /**
    95.     * enregistre l’élément dans la collection
    96.     * redirige vers l’action qui précédé l’action add ou edit en cas de succès
    97.     * vers showForm en cas d’échec
    98.     * @see add
    99.     * @see edit
    100.     * @see showForm
    101.     */
    102.    public function saveAction($perform = true) {
    103.       $context = new Zend_Session_Namespace(‘context’);
    104.       $messenger = new Zend_Session_Namespace(‘messenger’);
    105.  
    106.       $data = $context->formData;
    107.       $method = $context->saveMethod;
    108.  
    109.       $ok = $this->model->saveObject($data, $method);
    110.       if ($ok) {
    111.          $messenger = ‘data saved’;
    112.          $redirect = $context->returnPath;
    113.       } else {
    114.          $messenger = ‘error data could not be saved’;
    115.          $redirect = ‘/FormController/showForm’;
    116.       }
    117.       $this->_redirect($redirect);
    118.    }
    119. }

    On voit ici un des avantages de procéder ainsi. Le contrôleur peut être rendu générique. On pourrait en faire une classe abstraite et la dériver en autant de formulaire à traiter. Mais on peut aussi l’utiliser sans traitement pour présenter un prototype de l’application.

    Enfin ce contrôleur tel qu’il est écrit pose un petit problème en effet on met des choses dans la session dans un namespace nommé contexte mais si on utilise plusieurs instance de ce contrôleur on va avoir des conflits, de même avec le messager.
    Je pratique cette approche en php maintenant depuis plusieurs années et elle à montré de gros avantages. Par exemple dans une application les utilisateurs ont plusieurs profils. Et en calquant dans une première version les développements sur ce principe nous avions l’affichage d’une liste d’utilisateur, puis tout le processus d’ajout modification. Puis pour chaque fiche utilisateur un lien vers la liste de ses profils qui lui-même avait c’est enchainement. Une évolution qui n’a quasiment rien coûté fut de reporter le liste des profils de l’utilisateur dans la ficher de ce dernier. Le contenu du showList de ProfileController a été reporté dans showForm de UserController. Il a suffit de changer la valeur de returnPath dans profileController pour que tout soit opérationnel.
    Au fil du temps j’ai automatisé pas mal de choses liées à cette approche. Je vous les détaillerais dans les prochains articles.
    A+JYT

    17 Comments
    taintedsong.com taintedsong.com taintedsong.com

    Utiliser une façace pour accéder au modèle

    Parmi les design patterns que l’on trouve en programmation il en est un qu’on oublie un peu trop à mon avis c’est la notion de façade. Pour bien comprendre que quoi il s’agit un petit exemple sera plus parlant. Imaginé que vous travaillez dans un centre de conception automobile. Votre boulot consiste à concevoir toute la partie commande de la voiture. Vous allez collaborer avec les designers qui conçoivent l’habitacle et le tableau de bord (la vue) et avec les ingénieurs qui conçoivent la partie mécanique (le modèle ou métier). À vous de relier le tout (faire le contrôleur). Si les mécanos commence à vous parler de boulon de six ou de durite d’injection vous allez droit à la crise de nerf. Ce dont vous avez besoin c’est de savoir comment on fait marcher le tout. Par exemple comment accélérer. Que la commande d’accélération, action un régulateur électrique, ouvre un carburateur, ou action un injecteur de gasoil vous importe peut. Vous avez besoin de définir une API pour votre partie mécanique. Cette définition peut se présenter simplement par une documentation. Mais aussi par un point d’entrée unique qui vous cache la complexité du métier. Cela s’appelle une façade.
    Vous ne vous êtes jamais penché sur le problème de savoir comment votre banquier traitait vos chèques. Vous fournissez au guichet le bordereau dûment rempli comme le prévois la façade et ce qu’il se passe après vos importe peu du moment que votre argent va sur votre compte.
    C’est donc une pratique courante qui étrangement se perd lorsqu’on développe des logiciels. Bien souvent on fait ce qu’il y a à faire sans trop se demander s’il ne serait pas opportun d’isoler un sous ensemble derrière une API. En programmation à objet on encapsule ainsi des éléments. Mais rarement on pense que tout un ensemble pourrait être caché par une seule classe. La littérature sur ce sujet est pourtant vaste. Et utiliser une façade à bon escient est un gage pour l’avenir.

    Encore un exemple pour comprendre pourquoi.

    Sur L’A300 les commandes de vols étaient mécaniques. Pour les commandes moteur un levier au centre du poste de pilotage actionnait une série de cames qui finalement jouait sur les commandes du moteur : Directement. Les compagnies demandant à Airbus des moteurs différents il était nécessaire de changer une bonne partie de cette quincaillerie pour adapter le moteur à l’avion. Sur l’A320 l’ensemble des commandes à été dé-corrélées de leur support. Pour adapter un nouveau moteur il suffit alors de redéfinir l’interface.
    Il en va de même de vos développements. Si vous arrivés à bien cloisonner vos sous-ensembles alors en changer une partie n’impliquera que des changements mineurs sur le reste, tant que l’interface ne change pas.

    Utiliser une façade avec MVC

    Le modèle MVC propose une séparation nette et précise dans votre application. Dans Zend Framework, l’interface entre la vue et le contrôleur est bien définie. L’interaction entre la vue et le Modèle est à prohiber. Mais l’interface entre le contrôleur et le modèle est complètement à votre charge. Utiliser une façade comme modèle revient à n’avoir qu’un seul est unique objet à qui le contrôleur peut s’adresser. Comme pour votre banquier (le guichet). Cet objet ne va rien faire lui-même il va juste vous mettre à disposition vos objets métiers. Mais au passage il va vous cacher la complexité de leur organisation. Le contrôleur peut alors s’occuper de son travail la logique applicative, pendant que le modèle lui s’occupe de la logique métier. D’un côté vous parlez de clients de facture, d’automobile, d’avion, de téléphone, de livre et que sais-je d’autre. De l’autre vous parlez de table, de base de données, d’annuaire ldap, de ressources xml, de services web etc.
    Agissant ainsi vous allez être contrait de définir clairement les fonctionnalités que le modèle mets à disposition du contrôleur. Un peu de réflexion sur son travail n’est jamais mauvais en soit. De plus vous allez pouvoir définir toute l’interface sans avoir à l’implémenter. Ainsi le développement du contrôleur pour commencer sans attendre que le modèle soit prêt. Dans un travail en équipe c’est un bon moyen de répartir le travail. Cela permet de mettre en place toute la cinématique de l’application sans qu’elle ne fasse quoi que ce soit. La maitrise ouvrage peut alors la valider. De son côté le métier n’a pas à se préoccuper de savoir comment ses objets seront utilisées il suffit qui réponde à la définition de l’interface. Et enfin en cours de vie de l’application un pant entier peu changer dans le métier sans impacter le reste.
    Un autre point intéressant dans cette c’est que le contrôleur accédant à son modèle au travers de la façade, peu importe le contenu de cette façade, il devient possible tout comme pour la vue de prévoit à l’avance la relation entre le contrôleur et le modèle. Pouvoir accéder à sa vue juste au travers d’un membre et bien pratique. Pourquoi ne pas profiter de la façade et faire de même. Le contrôleur demande les services à son modèle et fournis les valeurs à sa vue.

    1. <?php
    2. $this->view->clientList = $this->model->getClientListOfCurrentUser() ;

    Il serait intéressant de rendre l’attachement du membre model transparent pour le développeur comme l’est l’attachement de la vue. A fin de garder un comportement du framework le plus standard possible, j’ai décidé de rendre cet attachement optionnel et commandé par un paramètre. Comme on l’a vu dans un article précédent.
    J’ai choisi le paramètre : « useModel »

    Une première approche simple

    Définir une classe Model au sein de l’application. Dans /appclication/Model.php,
    Puis dans chaque contrôleur dans la méthode ini on ajoute

    1.      if ($parameters = Fast_Registry::getParameters()) {
    2.          if (parameters->fast->get(‘useModel’, false)) {
    3.             Zend_Loader::loadClass(‘Model’);
    4.             $this->model = new Model();
    5.          }
    6.       }

    Reste au développeur à ajouter les méthodes spécifiques à son métier dans la classe Model. Peu importe ce qu’il mettra dans la méthode getClientListOfCurrentUser si celle-ci réponds au prototype alors le contrôleur vu plus haut sera fonctionnel.
    Mais devoir intervenir sur tous les contrôleurs pour bénéficier de l’attachement du modèle, n’est pas plus pratique que de devoir aller le chercher lorsqu’on en a besoin.
    Un attachement automatique et transparent est plus pertinent. Gardons dans un premier temps la classe model dans l’application et ajoutons un classe d’action générique qui aura pour but de collecter ce qui est commun aux différents contrôleurs. C’est ce que fait déjà Zend avec la classe Zend_Controller_Action. En faisant dériver cette classe de Zend_Controller_Action puis nos contrôleurs de cette classe nous aurons un endroit pour mettre toutes les parties communes.
    Un petit souci : où placer cette classe ? En suivant la nomenclature Zend je serais tenté de l’appeler Application_Controller_Action et elle se trouverait alors dans le dossier /application/controller/ or ce dossier n’existe pas. L’ajouter alourdirait la hiérarchie sans améliorer la lisibilité. Nous aurions cote à cote les dossiers controller et controllers. La différence sémantique entre les deux est subtile. Controller est le dossier contenant les éléments propre au mécanisme de contrôle et controllers est le dossier contenant les contrôleurs de l’application. Pour ne pas alourdir la hiérarchie, j’ai décidé de mettre ma classe dans controllers. Elle s’appellera donc Applications_Controllers_Action. Je déplace les quelques lignes de la méthode ini de mes contrôleurs dans celle de la classe action et je fais dériver mes contrôleurs de la classe Action.

    1. class IndexController extends Application_Controllers_Action

    Si le paramètre useModel est à true alors tous mes contrôleurs ont le membre model.

    Helpers et autre plugins

    J’avoue sincèrement ne pas avoir eut le temps de me pencher sur cette approche. Il me semble possible de reproduire un comportement similaire de cette façon. Se sera peut être une évolution future.

    Les modules et composants.

    Si j’utilise des modules je vais pouvoir reproduire dans le module le fonctionnement décrit ci-dessus. Mes modules auront alors leur propre façade sur la partie métier qui les concerne. C’est plutôt pas mal. Mais comment faire si un module à besoin de tout ou partie du modèle d’un autre ? S’il utilise que le modèle d’un autre module je peu m’arranger pour qu’il prenne comme façade celle de ce module. Il suffit pour cela de bien définir le chargement de la classe. Mais s’il doit utiliser son propre modèle plus des morceaux de celui d’un autre je suis obligé de recopier une partie de cette façade. Dupliquer du code n’est jamais une bonne chose. Surtout lorsqu’il n’y a aucun changement entre les deux copies. De plus on perd alors l’intérêt d’une façade dont le but est justement de n’avoir qu’un point d’accès.
    Pour répondre à cette problématique tout comme Zend l’a fait pour les vues, j’ai définis un classe modèle générale qui accepte des plugins, qui dans mon cas seront des Composants de modèle, des partie de modèle. Permettant aux contrôleurs ainsi de ne charger que les composants dont ils ont besoin. Chaque composant étant une façade sur un sous ensemble du modèle. Le remplacement d’un composant par un autre est ainsi facilités et l’ajout de composant dans l’application aussi. De plus les modules peuvent toujours définir leurs propres modèles, tout en laissant la possibilité à d’autres de les utiliser.
    Reprenant le fonctionnement décrit plus haut j’ai cette fois définis une classe Action générale. Fast_Controller_Action qui dérive de Zend_Controller_Action. Mes contrôleurs ou Application_Contollers_Action peuvent maintenant dériver de celle-ci. C’est elle qui embarque le chargement de la classe Fast_Model dont une instance est attachée aux contrôleurs. Elle dispose d’une méthode addComponent qui prend comme paramètres le nom d’une classe composant et d’un chemin optionnel. Elle charge alors cette classe et ajoute ses méthodes au modèle. La classe modèle à fournir doit dériver de Fast_Model_Component. Un composant pouvant avoir besoin d’interroger le modèle un membre protégé est disponible.

    Exemple d’utilisation

    Parammeters.ini

    1. useModel = true

    Classe client

    1. <?php
    2. Zend_Loader::loadClass(‘Fast_Model_Component’);
    3. class Model_Client extends Fast_Model_Component
    4. {
    5.    public function getClientList($nameStart) {
    6.        …
    7.    }
    8. }

    Contrôleur

    1. Zend_Loader::loadClass(‘Fast_Controller_Action’);
    2. class ClientController extends Fast_Controller_Action
    3. {
    4.    public function showListAction(){
    5.       //charger le composant model_Client du dossier model de ce module
    6.       $this->model->addComponent(‘Model_Client’ , dirname(dirname(__FILE__)));
    7.       $this->view->clientList = $this->model->getClientList() ;
    8.    }
    9. }

    Il y a fort à parier que le ClientController devra utiliser ce composant dans la majorité des ses méthodes. Le plus simple est alors de le charger dans la méthode ini. Mais il est parfaitement possible de charger un composant pour tout le module et en même temps d’en avoir un autre qui n’est chargé que pour une action.

    1. Zend_Loader::loadClass(‘Fast_Controller_Action’);
    2. class ClientController extends Fast_Controller_Action
    3. {
    4.    public function ini(){
    5.       //charger le composant model_Client du dossier model de ce module
    6.       $this->model->addComponent(‘Model_Client’ , dirname(dirname(__FILE__)));
    7.    }
    8.    public function showListAction(){
    9.       //charger le composant model_Article du dossier model de ce module
    10.       $this->model->addComponent(‘Model_Article’ , dirname(dirname(__FILE__)));
    11.       $this->view->clientList = $this->model->getClientList() ;
    12.       $this->view->articleList = $this->model->getArticleList() ;
    13.    }
    14. }

    Notez au passage qu’il est très simple ici de faire un bouchon. Il suffit dans la méthode getClientList de retourner une valeur du type attendue sans brancher réellement celle-ci sur le vrai modèle. Lorsque le modèle est prêt les branchements sont relativement simple puis qu’il n’y a plus à chercher où sont utilisé les objets métier. Ils ne le sont qu’au travers de la façade.
    A+JYT

    42 Comments
    taintedsong.com taintedsong.com taintedsong.com

    Utiliser Le moteur de template de son choix

    Je fais partit de ces gens qui utilisent un moteur de template. Les raison en sont multiples. La première est qu’il est particulièrement compliqué dans un moteur de template de faire du traitement. Ainsi on n’est pas tenté de faire faire à la vue des choses qui ne lui sont pas dévolue.
    La vue présente un point c’est tout. Au contrôleur de lui fournir la matière. À chacun son boulot. Il est parfaitement possible de faire de même avec le système que propose Zend_Framework. Mais il est tout aussi facile de mettre directement dans sa présentation quelque chose comme

    1. <?php echo Zend_Db:: getInstance()->getMyUserName() ?>

    Voir des choses encore plus complexes. Si la vue à besoin de myUserName c’est au contrôleur de lui donner. L’évolutivité et la maintenabilité de l’application en dépendent. Je n’ai pas été le premier à me pencher sur ce problème. Et force est de constater que les moteurs de templates on leur aficionados.
    J’utilise le plus fréquemment, le moteur ETS (Easy Template System) c’est le moteur utilisé par Marcopoly entre autre. J’ai aussi usé de Smarty. Les deux sont relativement proches dans leur syntaxe. ETS est un moteur ultra léger (89 521 octets) il n’utilise pas de système de cache. Je développe des applications qui sont très dynamique et dans mon cas le cache est plus un handicap qu’un avantage. Ce n’est pas le cas de toutes les applications et utiliser un cache peut s’avérer efficace.

    Comme je l’ai dis je travaille en équipe et suivant les projets un moteur ou un autre voire pas du tout est plus opportun. Je me suis donc demandé comment intégrer efficacement des moteurs à ZF. La littérature de ZF sur le sujet est plutôt légère et la solution proposée est totalement inefficace. Elle revient à demander au programmeur d’écrire autant de fichier phml comme on le ferait avec ZF sans template. Plus tout autant de templates plus du code pour relier le tout.
    On trouve sur internet quelques implémentations plus probantes. Je dois à un collègue (Patrick Dubois) une première intégration d’ETS dans ZF qui avait l’avantage de ne pas avoir à changer une seule ligne dans son contrôleur. Mais pour y parvenir nous avions fait un méchant hack qui ne me paraissait pas viable. À la suite de cela nous avons trouvé sur internet une intégration de Smarty particulièrement bien faite. Mais qui elle nécessitait de changer le code du contrôleur.
    Je trouvais la solution de Patrick séduisante et celle de Philippe Le Van (KitPage) élégante. Concilier les deux serait une bonne chose. Je dois vous avouer que j’ai déroulé ZF en pas à pas pour arriver à comprendre comment il fonctionnait en interne et particulièrement Zend_View. Après avoir échauffé les neurones. J’espère ne pas en avoir perdu de trop au passage. Je suis enfin arrivé à une solution. Qui a demandé une petite demi-journée de travail de mise au point. Dans la foulée, avec l’aide de Patrick (on pense mieux à deux têtes) nous avons intégré à ZF cinq moteurs dans une demi-journée.

    Notre approche consiste à dériver Zend_View et à remplacer l’instance de cette dernière dans le contrôleur, par la notre. Pour y parvenir il va falloir respecter le travail de Zend. Lors de la phase préparatoire au lancement de l’action de votre contrôleur, ZF va instancier un objet Zend_View. Puis dans votre action vous allez lui indiquer les valeurs à afficher. Éventuellement vous donnez l’ordre à la vue d’effectuer un rendu, puis vous rendez la main au Front contrôleur qui lui va passer au rendu final. Donc si comme moi vous mettez toujours que le minimum dans votre action, votre code se contente de faire des affectations de valeurs dans la vue. N’indiquant pas de rendu dans l’action ZF va demander à la vue de rendre le modèle de vue correspondant au nom du contrôleur et de l’action. La proposition de Philippe Le Van nécessite d’appeler explicitement un rendu dans l’action. Pour ma part je serais content de conserver le fonctionnement par défaut de ZF.

    Include ou main page ?

    Une autre chose que je trouve pratique avec les templates c’est de pouvoir les imbriquer. Avec phtml tel que proposé par ZF pour avoir une application qui a toujours la même apparence et don seul le contenu change, il est nécessaire d’en passer par un include d’un entête et d’un pied de page. Solution simple qui existait bien avant php lui-même. Mais cette approche implique de gérer la cohérence entre ce qui est ouvert dans l’entête et doit être refermé dans le pied. Avec un modèle principal qui inclut le contenu on a un système de boites, qui est homogène. Ainsi tout ce qui est ouvert dans un fichier est fermé dans ce fichier. On peu résumer ces deux approches ainsi

    1. <?php
    2. include ‘header.phtml’;
    3. <h2>contenu de ma page</h2>
    4. include ‘footer.phtml’;

    et

    1. <html>
    2. <body><h1>ma belle application</h1>
    3. < ?php include ‘content.phtml’; ?>
    4. </body>
    5. </html>

    Incontestablement je préfère l’approche main page, qui comme on le voit peut très bien être mise en œuvre en phtml. Mais on a vu que ZF allait chercher à rendre le fichier controller/action.phtml. Si on adopte l’approche main page, on va être en contradiction avec ZF. Il va falloir adapter la vue pour qu’elle accepte de fonctionner ainsi tout laissant croire au contrôleur qu’elle fonctionne exactement comme une Zend_View.
    En supposant qu’on y parvienne, on a introduit alors un nouveau problème. Comment faire si on a un rendu à faire qui n’utilise pas la même main page, ou qui n’en utilise pas du tout ? De ce côté-là inutile de fouiller la doc de ZF, il n’y a rien car Zend_View ne connais pas cette notion. Il sera alors nécessaire d’introduire une nouvelle fonctionnalité à la vue : setMainTemplate.

    Un exemple Simple

    En tout premier lieu, je vais essayer de faire une vue qui accepte la notion de main template, et qui respecte le fonctionnement de ZF. A savoir pas d’intervention dans le contrôleur et pas d’appel sauvage dans un fichier de script comme le propose ZF. On va garder exactement la même architecture les mêmes fichiers de script que ce que propose ZF mais au lieu d’avoir à inclure l’entête et le pied dans toutes les pages, on fournira un fichier main.phtml
    Première étape : dériver de Zend_View

    1. <?php
    2. /**
    3.  *
    4.  * @author Jean-Yves Terrien
    5.  *
    6.  */
    7.  
    8. Zend_Loader::loadClass(‘Zend_View’);
    9. class Fast_View_Phtml extends Zend_View

    Prévoyant plusieurs moteurs, il me fallait trouver une façon de les nommer. Se sera Fast_View_Engine. Ces vues ne sont pas les moteur de template mais l’interface entre ZF et le moteur qui lui doit rester inchangé. Il n’est pas question ni de modifier le code d’un moteur ni de modifier le code de ZF
    J’ai dit que je voulais pouvoir écrire : include ‘content.phtml’; or mon content va dépendre du script à rendre. J’ai donc besoin d’une variable dans ma vue pour identifier ce script. J’ai aussi dit que je devais pouvoir changer de main template. Et au passage suivant le moteur mes fichiers ne doivent pas toujours avoir le même type phtml pour php tpl pour Smarty et html pour ETS. La façon d’écrire un include dépend du moteur de template. Je ne me suis pas penché sur tous les moteurs pour mettre en œuvre l’approche main template. Je l’ai préparé dans mes moteurs mais il faut pour smarty et phptal trouver comment on fait un include d’un fichier dont le nom est fournit par une variable. Dans le fichier joint vous trouverez donc des exemple avec ou sans main page en fonction du moteur de template.

    1.   public $_content;
    2.    public $_mainTemplate;
    3.    protected $_suffix = ‘phtml’;

    Arrivé là il me faut surcharger une seule méthode.

    1.   /**
    2.      * Includes the view script in a scope with only public $this variables.
    3.      *
    4.      * @param string The view script to execute.
    5.      */
    6.    protected function _run()
    7.    {
    8.       // récupère le chemin complet du template demandé
    9.       $name = func_get_arg(0);
    10.       // le template principal est considéré à la racine des templates
    11.       // de l’application (ou du module).
    12.       if (!isset($this->_mainTemplate)) $this->setMainTemplate(dirname(dirname($name)) . ‘/main.phtml’);
    13.       // initialiser ets
    14.       // on indique à la quel est le template à inclure
    15.       $this->_content = $name;
    16.       // rendu de la page
    17.      include $this->_mainTemplate;
    18.    }

    Un copier coller de la méthode _run de Zend_View, une petite adaptation et le tour est joué. On va chercher la main page à la racine des vues. La méthode _run reçoit en argument le chemin complet vers le script à rendre. Il suffit donc de remonter d’un répertoire pour se trouver à la racine des vues soit de l’application soit du module. Pour fixer la main page j’ai dit que j’allais ajouter une méthode setMainTemplate je l’ai donc utilisé. Comme pour la page principale il peut être utile de permettre à qui en aurait besoin de connaitre le suffixe utilisé. J’ajoute donc une méthode getSuffix et pour faciliter l’écriture des scripts avoir un membre qui donne le chemin d’inclusion n’est pas mal non plus. Ne nous privons pas. Voici la classe en entier.

    1. <?php
    2. /**
    3.  *
    4.  * @author Jean-Yves Terrien
    5.  *
    6.  */
    7.  
    8. Zend_Loader::loadClass(‘Zend_View’);
    9. class Fast_View_Phtml extends Zend_View
    10. {
    11.    public $_content;
    12.    public $_templatesDir;
    13.    public $_mainTemplate;
    14.    protected $_suffix = ‘phtml’;
    15.  
    16.    public function setMainTemplate($main)
    17.    {
    18.       $this->_mainTemplate = str_replace(chr(92), ‘/’, $main);
    19.    }
    20.  
    21.    public function getSuffix() {
    22.       return $this->_suffix;
    23.    }
    24.  
    25.    /**
    26.      * Includes the view script in a scope with only public $this variables.
    27.      *
    28.      * @param string The view script to execute.
    29.      */
    30.    protected function _run()
    31.    {
    32.       // récupère le chemin complet du template demandé
    33.       $name = func_get_arg(0);
    34.       // le template principal est considéré à la racine des templates
    35.       // de l’application (ou du module).
    36.       if (!isset($this->_mainTemplate)) $this->setMainTemplate(dirname(dirname($name)) . ‘/main.phtml’);
    37.       // initialiser ets
    38.       // on indique à la vue quel est le template à inclure
    39.       $this->_content = $name;
    40.       $this->_templatesDir = dirname(dirname($name)) . ‘/’;
    41.  
    42.       // rendu de la page
    43.      include $this->_mainTemplate;
    44.    }
    45. }

    Un exemple de main page

    1. <html>
    2. <body><h1>ma belle application</h1>
    3. <?php include $this->_content; ?>
    4. </body>
    5. </html>

    Ajouter la vue au Framework

    Reste maintenant à indiquer au front contrôleur d’utiliser notre vue. ZF n’a rien prévu pour ce remplacement. Impossible de lui indiquer quelle classe utiliser comme vue. Impossible aussi d’ajouter des plugins comme pour les actions. La seule solution instancier la vue soit même et la donner au contrôleur. Cela se passe au démarrage.

    1. Zend_Loader::loadClass(‘Fast_View_Phtml’);
    2. self::$_instance->_view = new $‘Fast_View_Phtml’ ();
    3. $suffix = self::$_instance->_view->getSuffix();
    4. $viewManager = Zend_Controller_Action_HelperBroker::getStaticHelper(‘viewRenderer’);
    5. $viewManager->setView(self::$_instance->_view)->setViewSuffix($suffix);

    J’ai justement prévu dans mon front contrôleur la possibilité d’ajouter des éléments de ce type en fonction de la configuration. J’ai donc ajouté une entrée dans mon fichier de paramètre et l’ai ajouté le nécessaire dans mon front contrôleur.

    1.   public static function setViewEngine ($name = null) {
    2.       $className = ucfirst($name);
    3.       if (null == $className) {
    4.          $className = ‘Zend_View’;
    5.          $suffix = ‘phtml’;
    6.       } else {
    7.  
    8.          if (substr($className, 0, 10) != ‘Fast_View_’) {
    9.             $className = ‘Fast_View_’ . $className;
    10.          }
    11.       }
    12.       try {
    13.          Zend_Loader::loadClass($className);
    14.          self::$_instance->_view = new $className();
    15.          self::$_instance->_templateEngine = strtolower($name);
    16.          if (!isset($suffix)) $suffix = self::$_instance->_view->getSuffix();
    17.          $viewManager = Zend_Controller_Action_HelperBroker::getStaticHelper(‘viewRenderer’);
    18.          $viewManager->setView(self::$_instance->_view)->setViewSuffix($suffix);
    19.       } catch (Exception $e) {
    20.          Zend_Loader::loadClass(‘Fast_Exception_View’);
    21.          throw new Fast_Exception_View(‘Invalid View Engine: ‘. $className);
    22.       }
    23.    }

    Toujours une exception typée pour mieux gérer les problèmes et au passage un petit membre privé qui conserve le nom du moteur ça mange pas de pain.
    Et dans la méthode run

    1.      if ($parameters&&$config) {
    2.          $engine = $parameters->fast->get(‘templateEngine’, null);
    3.          $controller = self::getInstance();
    4.          Fast_Controller_Front::setViewEngine($engine);

    C’est fini. Ma Fast_View_Phtml remplace Zend_View et utilise main.phtml. Les fichiers touchées sont donc parameters.ini pour le paramètre ‘templateEngine’, Fast_Controller_Front pour charger la vue, Fast_Exception_View, et Fast_View_Phtml.

    D’autres moteurs

    Maintenant tout est en place.
    Pour ajouter Ets par exemple je crée le fichier Fast_View_Ets que je place dans le dossier library/Fast/View/. Il me faudra aussi garder quelque part le moteur ets lui-même. Je les places tous dans library/TemplatesEngines/
    En gros la vue Ets va préparer le moteur et faire un appel à printt comme Zend_View fait un include pour lancer le rendu. Cette classe aura la charge de gérer tous ce qui relève du paramétrage du moteur. S’il y a besoin de paramètre de configuration elle peut obtenir les valeurs avec Fast_Registry.
    Ets à besoin d’un arbre de valeur. On ajoute donc à la vue un membre pour les stocker : _ets on va utiliser les setters et getters de php pour affecter les valeurs tout comme le fait Zend_View on va aussi fournir la méthode assign qui a ses adeptes et qui existe aussi dans Zend_View. Et c’est dans la méthode run qu’on va faire le gros du travail.

    1.   /**
    2.      *
    3.      * @param string The view script to execute.
    4.      */
    5.    protected function _run()
    6.    {
    7.       // récupère le chemin complet du template demandé
    8.       $name = func_get_arg(0);
    9.       // le template principal est considéré à la racine des templates
    10.       // de l’application (ou du module).
    11.       if (!isset($this->_ets->_mainTemplate))
    12.          $this->setMainTemplate(str_replace(chr(92), ‘/’, dirname(dirname($name)) . ‘/main.html’));
    13.       // initialiser ets
    14.       require_once("TemplatesEngines/Ets/Ets.php");
    15.       // on indique à l’arbre de données d’Ets quel est le template à inclure
    16.       $this->_ets->_content = str_replace(chr(92), ‘/’, $name);
    17.       $this->_ets->_templatesDir = dirname(dirname($name)) . ‘/’;
    18.       // rendu de la page
    19.       printt($this->_ets, $this->_ets->_mainTemplate);
    20.    }

    Elle ressemble à celle de Phtml qui n’est qu’une copie de celle de Zend légèrement amélioré. La différence on charge le moteur (require) et on l’invoque (printt)
    Vous trouverez dans le fichier joint Phtml (phtml script avec main page) Ets, Smarty et Phptal. Ajouter un moteur est devenu quelque chose d’abordable et je vous laisse le loisir d’ajouter le votre.
    Une petite note à propos de Phptal : TAL est le système de template de Zope. Son moteur a été porté dans plusieurs langages. Il n’est pas très gros (en ko) mais il est très structuré au niveau de son code. Mais surtout il utilise des fichiers conforme xhtml la structure XML du document n’est pas altérée. Tous les éléments de tal étant dans un namespace. Du coup tous les templates TAL peuvent être édité par un designer web avec l’outil de son choix. Il est même possible d’embarque dans le modèle des données d’exemples. Imaginé que vous demandez à votre designer en chez de vous faire vos écran et qu’il doive présenter un tableau de donnée s’il l’édite sans aucune valeur dedans il ne pourra se faire une idée du rendu final. Il va donc naturellement mettre des lignes dans sa table. Avec TAL ces ligne ne sont pas gênantes Tal sait en tenir compte pour afficher les valeurs réelles de l’application à lors qu’il y a des lignes d’exemples dans la table. Bref il est très souple, propre et bien structuré. Je vous conseil d’y jeter un œil, même si vous n’envisagez pas l’usage d’un tel système.

    Zend Layout

    Pendant que je faisais cette intégration, Zend Layout est apparu. Je n’ai pas encore eu le temps de me pencher dessus. Mais à première vu cela me parait intéressant. Mais je ne peux en dire plus pour le moment.

    Helper

    Une autre approche est d’utiliser les helpers. Je n’ai pas nom plus eu le temps de tester la chose. Une chose est sure si je le fait je garderais en vue que l’utilisation d’un moteur ne doit en aucun cas nécessiter un changement quelconque dans mes contrôler. Ce n’est pas parce qu’on change de présentation qu’on doit changer la logique de l’application. C’est le principe de fondement de MVC. Et je ne veux pas le remettre en question.

    Pour tester

    A fin de vous permettre de tester je vous ai fait une copie de la chose. il faut pour la rendre fonctionnelle placer la librarie Zend dans le library.
    vous pouvez alors jouer avec les paramettre debug et template dans parameters.ini

    A+JYT

    4 Comments
    taintedsong.com taintedsong.com taintedsong.com

    Effacer ses traces

    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

    1. debug(‘ État de ma variable avant la boucle’, $mavar);

    ou

    1. 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 :

    1. echo ‘<pre>’.$message . ‘ => ‘ ;
    2. print_r($variable) :
    3. 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

    1. Dans GroupController.php:55
    2. user => stdClass Object
    3. (
    4.     [usr_id] => 1
    5.     [usr_ident] => juat6340
    6.     [usr_name] => Terrien
    7.     [usr_firstname] => Jean-Yves
    8.     [usr_mail] => jyterrien@orange.com
    9. )
    10.  
    11.     * Appel de : Adm_GroupController::showListAction()
    12.       dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Action.php:357
    13.     * Appel de : Fast_Controller_Action::dispatch()
    14.       dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Dispatcher.php:78
    15.     * Appel de : Fast_Controller_Dispatcher::dispatch()
    16.       dans E:\Htdocs\Fast_Framework\library\Zend\Controller\Front.php:911
    17.     * Appel de : Zend_Controller_Front::dispatch()
    18.       dans E:\Htdocs\Fast_Framework\library\Fast\Controller\Front.php:117
    19.     * Appel de : Fast_Controller_Front::run()
    20.       dans E:\Htdocs\Fast_Framework\index.php:62

    A+JYT

    7 Comments
    taintedsong.com taintedsong.com taintedsong.com