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

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é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.

    <?php
    class FormController extends Zend_Controller_Action
    {
       /**
        * Affiche la liste des éléments
        *
        * @return null
        */
       public function showListAction(){
          $messenger = new Zend_Session_Namespace('messenger');
          $this->view->list = $this->model->getObjectList();
          $this->view->messages = $messenger;
          unset($messenger); //on supprime les messages ils sont affiché on en a plus besoin en session
       }
    
       /**
        * Prépare un enregistrement pour l'afficher dans le formulaire
        * redirige vers showForm
        * @see showForm
        */
       public function addAction() {
          $context = new Zend_Session_Namespace('context');
          $messenger = new Zend_Session_Namespace('messenger');
    
          $context->returnPath = '/FormController/showList';
          $context->saveMethod = 'add';
          // Demander au model un nouvel enregistrement avec les valeurs par défaut
          $context->formData = $this->model->newObject();
          $this->_redirect('/FormController/showForm');
       }
    
       /**
        * Recherche l'enregistrement pour l'afficher dans le formulaire
        * redirige vers showForm
        * @see showForm
        */
       public function editAction() {
          $context = new Zend_Session_Namespace('context');
          $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) {
             // revenir au point de retour
             $messenger = 'no object for: '.$id;
             $redirect = $context->returnPath;
          } else {
             $redirect = '/FormController/showForm';
          }
          $this->_redirect($redirect);
       }
    
       /**
        * Affiche le formulaire d'édition d'un élément.
        *
        * @return null
        */
       public function showFormAction(){
          $context = new Zend_Session_Namespace('context');
          $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); //on supprime les messages ils sont affiché on en a plus besoin en session
       }
    
       /**
        * Récupère les données du formulaire les filtres et les vérifie
        * redirige vers save si le formulaire est valide showFrom sinon
        * @see save
        */
       public function checkFormAction() {
          $context = new Zend_Session_Namespace('context');
          if ($context->formData = $this->_request->get('form'))
          // vérification
          $ok = true;
          if ($ok) {
             $redirect = '/FormController/save';
          } else {
             $messenger = new Zend_Session_Namespace('messenger');
             $messenger = 'invalid datas';
             $redirect = '/FormController/showForm';
          }
          $this->_redirect($redirect);
       }
    
       /**
        * enregistre l'élément dans la collection
        * redirige vers l'action qui précédé l'action add ou edit en cas de succès
        * vers showForm en cas d'échec
        * @see add
        * @see edit
        * @see showForm
        */
       public function saveAction($perform = true) {
          $context = new Zend_Session_Namespace('context');
          $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 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 réponses à “De la granularité des actions.”

    1. 2mx dit :

      Pourquoi ne pas utiliser _forward à la place de _redirect ?

    2. sekaijin dit :

      Je n’ai pas creusé complètement l’API Mais effectivement _forward semble plus approprié
      si j’ai bien saisi _forward prends en argument un action, un contrôleur un module et des paramètres
      et _redirect une chaine de caractère

      Merci de la remarque
      A+JYT

    3. Mr.MoOx dit :

      En plus, je pense qu’un _forward à l’intérêt de pouvoir se passer de la session, car aujourd’hui c’est vrai que c’est rare, mais il arrive qu’elle ne fonctionne pas (pas de cookie chez l’utiliasteur par exemple).
      Et si pas session, pas formulaire :(.

    4. sekaijin dit :

      il n’est pas nécessaire d’avoir un cookie pour la session mais effectivement cette approche dépends du fonctionnement de la session.

      le redirect à un avantage sur le forward
      si tu prends l’enchaînement edit -> showForm édit va chercher les données dnas la base et les mets en session puis un header est envoyé au client qui arrive alors sur le showForm

      s’il recharge sa page il recharge showForm qui ne fait que régénérer l’affichage
      avec un forward il serait toujours sur l’url de l’édit et rechargerait alors les données.

      après la soumission du formulaire après le check il y a enchaînement vers save puis vers un showQuelquechose un F5 ne fait que refaire le show. avec des forward l’url est celle du check
      un F5 re-soumet donc le formulaire.

      mais effectivement il y a des moments où le forward est plus intéressant. il faut regarder ça de près.

      une petite note dans mon ancien framework il n’y avait qu’une seule url un F5 ou un History(-1) revenait toujours à recharger l’action courante puisqu’elle avaient toutes la même url.

      A+JYT

    5. Tomtom dit :

      Comment tu fait avec ce système dans le cas d’un formulaire envoyé en ajax ? tu n’a pas besoin de ré afficher le formulaire … je vois pas bien comment faire avec cette approche.

    6. sekaijin dit :

      Je n’utilise pas la même action.
      une action ajax se réduit à la lecture des donnée traitement et réponse JSON (dans mon cas)

      si je dois enchainer les action comme dans le cas d’un formulaire
      la dernière action n’e retourne pas sur showForm mais un sendResponse qui ne fait qu’invoyer la réponse en json.

      si je veux réutiliser le checkForm et le save qui serait commun à un traitement de formulaire et un appel ajax

      j’utilise dans le check le isXmlHttpRequest() dans ce cas je garde l’info sous le coude et à la place d’un redirect j’utilise un forward et à la fin je renvois vers le sendResponse

      je ne sais pas très bien comment se comporte xmlHttpRequest côté client sur un redirect. je ne suis pas sur qu’il ne permette pas d’empècher les redirection. par sécurité je réponds directement dans la même requête.

      A+JYT

    7. cortex dit :

      Pour ne pas induire les débutants en erreur. Il ne faut surtout pas utilisé de redirection de manières abusives telle qu’expressement induit par ce découpage. Une action ainsi découpée en cinq va abusivement nécessiter 5 requetes HTTP extremement (et le mot est très faible) plus couteuse qu’avec une classique structure en if avec factorisation de code (regroupement dans une fonction de code appelé à aux moins 2 endroits, car 2 fois c’est déjà trop :p). En effet, un appel de méthode représente une poignée de jeux d’instructions CPU alors qu’une redirection va demander le renvoi du header de redirection, la cloture de la connection en cours (avec tout ce que ça comporte au niveau applicatif et reseau et libération de ressources), le traitement par le client de la réponse (si tant est qu’il est capable de le faire), l’ouverture d’une nouvelle connection, l’envoi de la requete, à nouveau tout le parsing du code PHP et le chargement des fichiers inclus (et y’en a une palanquée avec le ZF), le traitement et le routage de la requete pour arriver à ma nouvelle action qui, à peine après trois lignes de code, me refait déjà une redirection. A la grande limite, un _forward serait plus justifié mais alors et tu le montres finalement très bien, mon if qui me servait à savoir si mon formulaire était valide est non seulement resté mais augmenté d’un if supplémentaire dans l’action cible pour tester si l’action est elligible (par sécurité bien sur, ce qu’une fonction privée ne nécessite à priori pas !). Pas sur qu’on gagne en lisibilité/maintenabilité pour tout ce que l’on perd en performance car meme un _forward est couteux comparé à un if (surtout si tes règles de Routing sont complexes). Il y a des compromis à faire entre performance/maintenabilité/réutilisabilité et je suis loin de penser que cette solution en fasse pour chacun.
      Non, le routing n’est pas et ne sera jamais l’implementation d’un pattern commande ré-entrant ou non.

      cortex

    8. sekaijin dit :

      effectivement cette approche nécessite des redirection. c’est à la fois sont point faible (coût) et sont point fort

      il deviens beaucoup plus facile de redéfinir les enchaînements d’une application lorsque les actions sont atomique. par exemple j’ai eu a modifier deux crud qui étaient indépendant en les chaînant l’un à l’autre. avec des if et des fonctions impossible de chainer cela. les fonctions étant dans deux contrôleur différents. il m’a fallu changer deux redirecte faire un appel supplémentaire au modèle et modifier une vue pour changer complètement l’organisation de l’application.

      dans une entreprise dans une application complexe pouvoir redéfinir la logique d’interface c’est à dire la cinématique de l’application est un gros avantage.

      question évolutivité il n’y a pas photo avoir une code figer et un frein.

      pour ce qui est de la maintenabilité cette approche à déjà plusieurs années et là encore ça c’est avéré payant. en effet chaque actions ayant un rôle bien identifié étant courte la maintenance est facilité.

      quant à la question de la réutilisabilité il n’y a pas photo non plus. j’ai un crud qui ne demande quasiment pas de dev
      dériver une classse donner le nom des méthode métier et définir les vues. pas une ligne de code pour faire un crud.
      et cette façon de faire et reproductible pour tout type de pattern d’interaction.
      mieux si le modèle à produire est proche d’un pattern déjà implémenté mais qu’il doive se combiner avec un autre il est simple de redéfinir les enchaînement pour le construire sans avoir à tout refaire.

      pour info vici un peu de littérature sur le sujet
      http://en.wikipedia.org/wiki/Post/Redirect/Get

      A+JYT

    9. Euclide dit :

      Bonjour sekaijin,
      est-il préférable d’utiliser un formulaire Zend_Form ou un formulaire « normal »? Étant donnée que le formulaire validerait lui même…

    10. sekaijin dit :

      ce que j’ai écrit là est basé sur la 1.0.3 qui ne contenais pas encore Zend_Form cela ne change pas fondamentalement le discours mais effectivement on peut se demander quelle est la meilleure approche.
      Je dois avouer que je ne saurais pas répondre.
      la solution Zend_Form à l’avantage de se baser sur un code maintenu et supporté. l’approche que j’ai décrite est elle issue de longues années de pratique.

      il y a aussi les habitudes et les pratiques communes qu’il convient de prendre en compte. le changement est toujours plus difficile lorsqu’on travaille à plusieurs. il est alors nécessaire que tout le monde franchisse le pas.

      enfin la solution Zend_Form pour moi implique un inconvénient qui fait que j’ai beaucoup de mal à m’y lancer.
      C’est en effet le contrôleur qui génère le HTML pour le donner à la vue. et là c’est plutôt problématique. mes vues ne sont pas toujours en html.

      si les vues étaient des classes php le code de Zend_Form y trouverait toute sa place. mais du coup la vérification qui relève soit du contrôleur (vérification de types part exemple) soit du modèle (vérification métier) se retrouve associé à la vue.

      Voilà pour moi le principal défaut de Zend_View à part ça je n’ai pas de penchant ni vers la solution Zend ni vers celle que je proposais.

      A+JYT

    11. Euclide dit :

      Merci, pour ces réponses(sur les autres articles :))

      Si j’ai bien compris pour la validation, c’est le contrôleur qui doit s’assurer de passer, par exemple, un int à la méthode getItembyId($id)

      C’est au modèle de vérifier que ses dont sont valide pour ça sauvegarde, donc dans checkform, mon modèle devrait appeler la méthode validateClientData($data) et lui retourne un boolean pour dire s’ils sont valide et par la suite, on pourrait avoir une méthode filterClientData($data) qui elle retournerait les données filtrées?

      Peux-tu confirmer mes dires svp :)

      Merci beaucoup, ton blog est super

    12. sekaijin dit :

      si on s’en tien à la théorie du design pattern mvc le contrôleur est le seul à connaitre le format des données fournies par la vue et celui qu’attend le modèle.

      c’est donc normalement à lui d’assurer les trans-typages qui s’imposent.
      le modèle est lui celui qui connait le métier c’est donc à lui de dire si ce qu’on lui propose est conforme.

      le contrôler devrait donc être porteur d’une méthode _filterClientData (protected car il n’y a pas de raison qu’elle soit appelée hors du contrôleur) et le modèle devrait quant à lui fournir une ou plusieurs méthodes de validation comme validateClientData (publique car utilisé par tout un chacun)

      dans la pratique on rencontre un peu de tout.
      toutes les vérifications faîtes par le contrôleur (pb si un autre contrôleur dois assurer une fonction similaire et a donc besoin de faire la même vérification)

      le filtrage et une partie des vérifications faites par le contrôleur. on trouve la plus part du temps dans ce cas les vérifications de type et de présences. les vérifications purement métier étant déléguées au modèle. (cela permet d’avoir des méthodes de vérification métier simples et claires mais lors d’une évolution ou d’un bug il faut chercher et dans les contrôleur et dans le modèle)

      le filtrage dans le contrôleur et la vérification dans le modèle (l’idéal selon la théorie.Mais cela nécessite une méthode de vérification robuste qui prenne en compte une grande variété de donnée d’entrée. on ne sais jamais ce qu’on reçois)

      un filtrage basique dans le contrôleur généralement trim et pb d’injection de code le modèle s’occupant de tout le reste. (pour arriver à cela il faut que le modèle digère tout et n’importe quoi. chose très difficile à obtenir- tout évolution de la vue entraine alors une évolution du modèle pour traiter un nouveau format ou type de donnée)

      en php les types ne sont pas fort et cela amène de la souplesse du coup on peut assez facilement passer un string à une fonction qui attend un entier mais du coup il n’est pas toujours évident de savoir de qui relève la responsabilité.

      A+JYT

    13. Euclide dit :

      Bonjour Sekaijin,
      j’utilise ton système CRUD pour un petit blog que je suis en train de créer et j’essaie dans l’action « show » du contrôleur ticket qui permet d’afficher un billet, ses commentaires ainsi qu’un formulaire permettant d’ajouter un commentaire qui j’aimerais, provienne de mon action create. Cependant si j’essaie avec l’aide de vue action() d’afficher mon formulaire qui est généré via « create » qui redirige vers « showform », je suis redirigé vers celui-ci.

      Dois-je refaire une action qui me permette de générer mon formulaire sans redirection?

      P.S.: J’utilise Zend_Form pour générer mes formulaires, j’ai donc du retirer checkform pour des problèmes d’affichages d’erreur automatique.

    14. sekaijin dit :

      Bonjour
      je suis désolé mais je n’ai pas testé ce crud avec Zend_Form
      pour des raisons techniques la version sur la quelle je travaille est ma 1.0.4 au maximum
      du coup je n’avais pas les Zend_Form à dispo

      pour moi le create ne dois pas générer quoi que ce soit pour la vue. c’est au showForm de le faire
      les actions qui précèdent le showform sont des action qui préparent les donnée à afficher dans le formulaire seul le showForm se charge de générer cette vue. je pense donc que c’est dans le showForm qu’il faut utiliser les Zend_Form
      A+JYT

    15. Euclide dit :

      Bonjour,
      je me suis mal exprimé, je voulais dire que mon action create prépare un objet par défaut et redirige vers showForm qui lui affiche le formulaire, mais il est là mon problème lorsqu’une redirection est effectué via l’aide de vue action(), je me retrouve aussi redirigé. Donc dans la page qui me permet d’afficher un billet ainsi que les commentaires associés, j’aimerais afficher se formulaire, sans être redirigé et si possible sans réécrire le même code que pour cette action.

    16. Your post De la granularité des actions. | 世界人のBlog was very interesting when I found it over google on Friday by my search for sauvegarde donnee en ligne. I have your blog now in my bookmarks and I visit your blog again, soon. Take care. Parejaspareja.es

    17. sekaijin dit :

      muchas gracias por su visita
      A+JYT

    Laisser un commentaire