Chainer les décorateurs

Si vous avez bien suivi la section précédente, vous avez pu remarquer que la méthode render() prend un argument, $content. Il est de type chaîne de caractères. render() va utiliser cette chaîne et décider de la remplacer, de rajouter ou de faire précéder du contenu à celle-ci. Ceci permet de chaîner les décorateurs -- ce qui ouvre des possibilités de créer ses propres décorateurs qui vont rendre chacun une petite partie des données d'un élément -- c'est la chaîne complète de décorateurs qui déterminera le rendu final réel de l'élément.

Voyons voir en pratique comment ça fonctionne.

Pour la plupart des éléments, les décorateurs suivants sont chargés par défaut :

  • ViewHelper : utilise une aide de vue pour rendre l'élément balise de formulaire à proprement parlé.

  • Errors : utilise l'aide de vue FormErrors pour afficher les erreurs de validation éventuelles.

  • Description : utilise l'aide de vue FormNote afin de rendre la description éventuelle de l'élément.

  • HtmlTag : encapsule les trois objets ci-dessus dans un tag <dd>.

  • Label : rend l'intitulé de l'élément en utilisant l'aide de vue FormLabel (et en encapsulant le tout dans un tag <dt>).

Notez bien que chaque décorateur n'a qu'une petite tâche particulière et opère sur une partie spécifique des données de l'élément auquel il est rattaché, le décorateur Errors récupère les messages de validation de l'élément et effectue leur rendu, le décorateur Label rend simplement le libellé. Ceci fait que chaque décorateur est très petit, réutilisable, et surtout testable.

Cet argument $content vient de là aussi : chaque décorateur travaille avec sa méthode render() sur un contenu (généralement généré par le décorateur immédiatement précédent dans la pile globale) et embellit ce contenu en lui rajoutant ou en lui faisant précéder des informations. Il peut aussi remplacer totalement son contenu.

Ainsi, pensez au mécanisme des décorateurs comme la conception d'un oignon de l'intérieur vers l'extérieur.

Voyons voir un exemple, le même que celuide la section précédente :

  1. class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
  2. {
  3.     protected $_format = '<label for="%s">%s</label>'
  4.                        . '<input id="%s" name="%s" type="text" value="%s"/>';
  5.  
  6.     public function render($content)
  7.     {
  8.         $element = $this->getElement();
  9.         $name    = htmlentities($element->getFullyQualifiedName());
  10.         $label   = htmlentities($element->getLabel());
  11.         $id      = htmlentities($element->getId());
  12.         $value   = htmlentities($element->getValue());
  13.  
  14.         $markup  = sprintf($this->_format, $id, $label, $id, $name, $value);
  15.         return $markup;
  16.     }
  17. }

Supprimons la fonctionnalité libellé (label) et créons un décorateur spécifique pour lui.

  1. class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
  2. {
  3.     protected $_format = '<input id="%s" name="%s" type="text" value="%s"/>';
  4.  
  5.     public function render($content)
  6.     {
  7.         $element = $this->getElement();
  8.         $name    = htmlentities($element->getFullyQualifiedName());
  9.         $id      = htmlentities($element->getId());
  10.         $value   = htmlentities($element->getValue());
  11.  
  12.         $markup  = sprintf($this->_format, $id, $name, $value);
  13.         return $markup;
  14.     }
  15. }
  16.  
  17. class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
  18. {
  19.     protected $_format = '<label for="%s">%s</label>';
  20.  
  21.     public function render($content)
  22.     {
  23.         $element = $this->getElement();
  24.         $id      = htmlentities($element->getId());
  25.         $label   = htmlentities($element->getLabel());
  26.  
  27.         $markup = sprint($this->_format, $id, $label);
  28.         return $markup;
  29.     }
  30. }

Ok, ca semble bon mais il y a un problème : le dernier décorateur va l'emporter. Vous allez vous retrouver avec comme seul rendu, celui du dernier décorateur.

Pour faire fonctionner le tout comme il se doit, concaténez simplement le contenu précédent $content avec le contenu généré :

  1. return $content . $markup;

Le problème avec cette approche est que vous ne pouvez pas choisir où se place le contenu du décorateur en question. Heureusement, un mécanisme standard existe ; Zend_Form_Decorator_Abstract possède le concept de place et définit des constantes pour le régler. Aussi, il permet de préciser un séparateur à placer entre les 2. Voyons celà :

  1. class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
  2. {
  3.     protected $_format = '<input id="%s" name="%s" type="text" value="%s"/>';
  4.  
  5.     public function render($content)
  6.     {
  7.         $element = $this->getElement();
  8.         $name    = htmlentities($element->getFullyQualifiedName());
  9.         $id      = htmlentities($element->getId());
  10.         $value   = htmlentities($element->getValue());
  11.  
  12.         $markup  = sprintf($this->_format, $id, $name, $value);
  13.  
  14.         $placement = $this->getPlacement();
  15.         $separator = $this->getSeparator();
  16.         switch ($placement) {
  17.             case self::PREPEND:
  18.                 return $markup . $separator . $content;
  19.             case self::APPEND:
  20.             default:
  21.                 return $content . $separator . $markup;
  22.         }
  23.     }
  24. }
  25.  
  26. class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
  27. {
  28.     protected $_format = '<label for="%s">%s</label>';
  29.  
  30.     public function render($content)
  31.     {
  32.         $element = $this->getElement();
  33.         $id      = htmlentities($element->getId());
  34.         $label   = htmlentities($element->getLabel());
  35.  
  36.         $markup = sprintf($this->_format, $id, $label);
  37.  
  38.         $placement = $this->getPlacement();
  39.         $separator = $this->getSeparator();
  40.         switch ($placement) {
  41.             case self::APPEND:
  42.                 return $markup . $separator . $content;
  43.             case self::PREPEND:
  44.             default:
  45.                 return $content . $separator . $markup;
  46.         }
  47.     }
  48. }

Notez que dans l'exemple ci-dessus, nous intervertissons les comportements par défaut avec append et prepend.

Créons dès lors un élément de formulaire qui va utiliser tout celà :

  1. $element = new Zend_Form_Element('foo', array(
  2.     'label'      => 'Foo',
  3.     'belongsTo'  => 'bar',
  4.     'value'      => 'test',
  5.     'prefixPath' => array('decorator' => array(
  6.         'My_Decorator' => 'path/to/decorators/',
  7.     )),
  8.     'decorators' => array(
  9.         'SimpleInput',
  10.         'SimpleLabel',
  11.     ),
  12. ));

Comment ça fonctionne ? et bien nous appelons render(), l'élément va alors commencer une itération sur tous ses décorateurs, en appelant render() sur chacun. Il va passer une chaîne vide comme contenu pour le premier décorateur, et le rendu de chaque décorateur va servir de contenu pour le suivant, ainsi de suite :

  • Contenu initial : chaîne vide: ''.

  • Chaîne vide ('') est passée au décorateur SimpleInput, qui génère un tag de formulaire de type input qu'il ajoute à la chaîne vide : <input id="bar-foo" name="bar[foo]" type="text" value="test"/>.

  • Ce contenu généré est alors passé comme contenu original pour le décorateur SimpleLabel qui génère un libellé et le place avant le contenu original avec comme séparateur PHP_EOL, ce qui donne : <label for="bar-foo">\n<input id="bar-foo" name="bar[foo]" type="text" value="test"/>.

Mais attendez une minute ! Et si nous voulions que le libellé soit rendu après le tag de formulaire pour une raison quelconque ? Vous souvenez-vous de l'option "placement" ? Vous pouvez la préciser comme option de décorateur, et le plus simple est alors de la passer à la création de l'élément :

  1. $element = new Zend_Form_Element('foo', array(
  2.     'label'      => 'Foo',
  3.     'belongsTo'  => 'bar',
  4.     'value'      => 'test',
  5.     'prefixPath' => array('decorator' => array(
  6.         'My_Decorator' => 'path/to/decorators/',
  7.     )),
  8.     'decorators' => array(
  9.         'SimpleInput'
  10.         array('SimpleLabel', array('placement' => 'append')),
  11.     ),
  12. ));

Notez que passer des options vous oblige à préciser le nom du décorateur dans un tableau en tant que premier élément, le deuxième élément est un tableau d'options.

Le code ci-dessus propose un rendu : <input id="bar-foo" name="bar[foo]" type="text" value="test"/>\n<label for="bar-foo">.

Grâce à cette technique, vous pouvez avoir plusieurs décorateurs dont chacun s'occupe de rendre une petite partie d'un élément ; et c'est en utilisant plusieurs décorateurs et en les chaînant correctement que vous obtiendrez un rendu complet : l'oignon final.

Avantages et inconvénients d'une telle technique, commençons par les inconvénients :

  • C'est plus complexe qu'un rendu simple. Vous devez faire attention à chaque décorateur mais en plus à l'ordre dans lequel ils agissent.

  • Ça consomme plus de ressources. Plus de décorateurs, plus d'objets, multipliés par le nombre d'éléments dans un formulaire et la consommation en ressources augmente. La mise en cache peut aider.

Les avantages sont :

  • Réutilisabilité. Vous pouvez créer des décorateurs complètement réutilisables car vous ne vous souciez pas du rendu final, mais de chaque petit bout de rendu.

  • Fléxibilité. Il est en théorie possible d'arriver au rendu final voulu très exactement, et ceci avec une petite poignée de décorateurs.

Les exemples ci-dessus montrent l'utilisation de décorateurs au sein même d'un objet Zend_Form et nous avons vu comment les décorateurs jouent les uns avec les autres pour arriver au rendu final. Afin de pouvoir les utiliser de manière indépendante, a version 1.7 a ajouté des méthodes flexibles rendant les formulaires ressemblant au style Rail. Nous allons nous pencher sur ce fait dans la section suivante.

blog comments powered by Disqus