Zend Framework - De la granularité des actions
Date de publication : 10 octobre 2007
Par
JYT (Les expériences Zend de Sekaijin) (Blog)
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 ?
I. Introduction
II. Une action atomique
III. Tout cela sert-il à quelque chose ?
IV. Transmission des données
V. Un prototype de contrôleur
I. Introduction
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 conception qui permettent des découpages logiques 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 ces 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 n'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 est là pour assurer cet aiguillage. Au niveau de notre contrôleur, nous pouvons faire une seule action qui à son 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 :
if ($this ->_request ->isPost ()) {
$ formvar = $f ->filter ($this ->_request ->getPost (' varname ' ));
if (empty($ formvar )) {
$this ->view ->message = ' Please provide a ... ' ;
} else {
. . .
if ($result ->isValid ()) {
. . .
} else {
. . .
}
}
} else {
. . .
|
Dans cet exemple, on va utiliser la présence ou pas de données de formulaire pour savoir où on en est au cours du processus de gestion d'un formulaire. Dans le cas où on a des données, on va alors tenter de faire le nécessaire ; dans le cas contraire, on va afficher le formulaire pour la saisie. À ce moment, on va devoir déterminer si on est en cours d'une édition ou 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 par action, ce qui deviendrait plutôt compliqué. Là encore, il faut trouver un juste milieu.
II. 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ôleurs. 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.
Voici déjà un découpage que l'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 qu'elles sont acceptables. Traiter les données sera par exemple l'enregistrement en base. Logiquement, on voit que les actions Préparer, Vérifier et Traiter n'affichent 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 bien avons-nous atteint un niveau logique unitaire ? En clair, chaque action ainsi déterminée 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 induits par les données. Ces choix ne relèvent 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 selon la valididité des données.
On peut donc afiner ce 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, 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.
III. Tout cela sert-il à quelque chose ?
On peut 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 vers la fonction à effectuer, les enchaînements de fonctions se faisant simplement. Or le dispatcher de MVC sert justement à faire les aiguillages. En reportant les fonctions vers des actions, on va utiliser le dispatcher plutôt qu'écrire l'aiguillage. En imaginant qu'une autre partie de l'application n'ait besoin que d'une partie des fonctions, il suffira d'enchaîner vers cette partie de l'application pour obtenir le résultat.
Si une nouvelle étape intermédiaire (une confirmation par exemple) venait à être insérée, il suffirait de changer les redirections.
Bref, passer par les actions va permettre :
- De ne pas écrire de code pour aiguiller les actions, le dispatcher le faisant très bien ;
- D'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 discouru sur l'objet que traitait mon formulaire. Or je peux déjà écrire le contrôleur et ses enchaînements.
IV. Transmission des données
Reste qu'à changer d'action, j'ai introduit une complexité nouvelle. 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 afficher un message, je le mets dans la vue. Lorsque j'en ai plusieurs, il me faut les garder dans la session.
V. Un prototype de contrôleur
Je vais ici donner un prototype d'un tel contrôleur. Je reviendrai 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. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. | 61. | 62. | 63. | 64. | 65. | 66. | 67. | 68. | 69. | 70. | 71. | 72. | 73. | 74. | 75. | 76. | 77. | 78. | 79. | 80. | 81. | 82. | 83. | 84. | 85. | 86. | 87. | 88. | 89. | 90. | 91. | 92. | 93. | 94. | 95. | 96. | 97. | 98. | 99. | 100. | 101. | 102. | 103. | 104. | 105. | 106. | 107. | 108. | 109. | 110. | 111. | 112. | 113. | 114. | 115. | 116. | 117. | 118. |
| <?php
| class FormController extends Zend_Controller_Action
| {
|
|
|
| @return null
|
| public function showListAction(){
| $messenger = new Zend_Session_Namespace(' messenger ' );
| $this ->view ->list = $this ->model ->getObjectList ();
| $this ->view ->messages = $ messenger ;
| unset($ messenger );
| }
|
|
|
|
| @see
|
| public function addAction() {
| $context = new Zend_Session_Namespace(' context ' );
| $messenger = new Zend_Session_Namespace(' messenger ' );
|
| $context ->returnPath = ' /FormController/showList ' ;
| $context ->saveMethod = ' add ' ;
|
| $context ->formData = $this ->model ->newObject ();
| $this ->_redirect (' /FormController/showForm ' );
| }
|
|
|
|
| @see
|
| public function editAction() {
| $context = new Zend_Session_Namespace(' context ' );
| $messenger = new Zend_Session_Namespace(' messenger ' );
|
| $context ->returnPath = ' /FormController/showList ' ;
| $context ->saveMethod = ' update ' ;
|
| $ id = $this ->_request ->get (' id ' );
| $context ->formData = $this ->model ->getItemById ($ id );
| if (! $context ->formData ) {
|
| $ messenger = ' no object for: ' . $ id ;
| $ redirect = $context ->returnPath ;
| } else {
| $ redirect = ' /FormController/showForm ' ;
| }
| $this ->_redirect ($ redirect );
| }
|
|
|
|
| @return null
|
| public function showFormAction(){
| $context = new Zend_Session_Namespace(' context ' );
| $messenger = new Zend_Session_Namespace(' messenger ' );
|
| $this ->view ->cancelAction = $context ->returnPath ;
| $this ->view ->saveAction = ' /FormController/checkForm/ ' ;
|
| $this ->view ->form = clone ($context ->formData );
| $this ->view ->messages = $ messenger ;
| unset($ messenger );
| }
|
|
|
|
| @see
|
| public function checkFormAction() {
| $context = new Zend_Session_Namespace(' context ' );
| if ($context ->formData = $this ->_request ->get (' form ' ))
|
| $ ok = true ;
| if ($ ok ) {
| $ redirect = ' /FormController/save ' ;
| } else {
| $messenger = new Zend_Session_Namespace(' messenger ' );
| $ messenger = ' invalid datas ' ;
| $ redirect = ' /FormController/showForm ' ;
| }
| $this ->_redirect ($ redirect );
| }
|
|
|
|
|
| @see
| @see
| @see
|
| public function saveAction($ perform = true ) {
| $context = new Zend_Session_Namespace(' context ' );
| $messenger = new Zend_Session_Namespace(' messenger ' );
|
| $ data = $context ->formData ;
| $ method = $context ->saveMethod ;
|
| $ ok = $this ->model ->saveObject ($ data , $ method );
| if ($ ok ) {
| $ messenger = ' data saved ' ;
| $ redirect = $context ->returnPath ;
| } else {
| $ messenger = ' error data could not be saved ' ;
| $ redirect = ' /FormController/showForm ' ;
| }
| $this ->_redirect ($ redirect );
| }
| }
|
|
|
On voit ici l'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 formulaires à 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 instances 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 a 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'utilisateurs, puis tout le processus d'ajout-modification. Puis pour chaque fiche utilisateur, un lien vers la liste de ses profils qui eux-mêmes avaient ces enchaînements.
Une évolution qui n'a quasiment rien coûté fut de reporter la liste des profils de l'utilisateur dans sa fiche. 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étaillerai dans les prochains articles.
A+JYT
Copyright © 2007 Sekaijin.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.