Les bases des décorateurs

Aperçu du pattern décorateur

Pour commencer, nous allons voir un peu de théorie sur » le pattern décorateur . Une technique consiste à définir une interface commune que les objets originaux et les décorateurs implémentent ; les décorateurs ayant comme dépendance les objets originaux, ils vont alors pouvoir redéfinir ou proxier les appels à leurs méthodes. Voyons un peu de code afin de mieux comprendre :

  1. interface Window
  2. {
  3.     public function isOpen();
  4.     public function open();
  5.     public function close();
  6. }
  7.  
  8. class StandardWindow implements Window
  9. {
  10.     protected $_open = false;
  11.  
  12.     public function isOpen()
  13.     {
  14.         return $this->_open;
  15.     }
  16.  
  17.     public function open()
  18.     {
  19.         if (!$this->_open) {
  20.             $this->_open = true;
  21.         }
  22.     }
  23.  
  24.     public function close()
  25.     {
  26.         if ($this->_open) {
  27.             $this->_open = false;
  28.         }
  29.     }
  30. }
  31.  
  32. class LockedWindow implements Window
  33. {
  34.     protected $_window;
  35.  
  36.     public function __construct(Window $window)
  37.     {
  38.         $this->_window = $window;
  39.         $this->_window->close();
  40.     }
  41.  
  42.     public function isOpen()
  43.     {
  44.         return false;
  45.     }
  46.  
  47.     public function open()
  48.     {
  49.         throw new Exception('Cannot open locked windows');
  50.     }
  51.  
  52.     public function close()
  53.     {
  54.         $this->_window->close();
  55.     }
  56. }

Nous créons un objet de type StandardWindow, le passons au constructeur de LockedWindow, et le comportement de notre fenêtre a maintenant changé. Le point fort ici est que nous n'avons pas besoin d'implémenter une fonctionnalité de verrou ("lock") dans l'objet de base (StandardWindow) -- le décorateur s'occupe de cela. En plus, vous pouvez utiliser votre fenêtre décorée à la place de la fenêtre standard : elles implémentent la même interface.

Une utilisation particulièrement pratique du pattern décorateur est pour tout ce qui concerne la représentation des objets. Par exemple un objet "Personne" qui en lui-même n'a aucune représentation textuelle. Grâce au pattern décorateur, vous pouvez créer un objet qui va agir comme une Personne mais qui pourra aussi représenter textuellement cette Personne.

Dans cet exemple particulier, nous allons utiliser le » duck typing plutôt qu'une interface explicite. Ceci permet à notre implémentation d'être un peu plus fléxible tout en gardant l'utilisation de la décoration intacte.

  1. class Person
  2. {
  3.     public function setFirstName($name) {}
  4.     public function getFirstName() {}
  5.     public function setLastName($name) {}
  6.     public function getLastName() {}
  7.     public function setTitle($title) {}
  8.     public function getTitle() {}
  9. }
  10.  
  11. class TextPerson
  12. {
  13.     protected $_person;
  14.  
  15.     public function __construct(Person $person)
  16.     {
  17.         $this->_person = $person;
  18.     }
  19.  
  20.     public function __call($method, $args)
  21.     {
  22.         if (!method_exists($this->_person, $method)) {
  23.             throw new Exception('Invalid method called on HtmlPerson: '
  24.                 .  $method);
  25.         }
  26.         return call_user_func_array(array($this->_person, $method), $args);
  27.     }
  28.  
  29.     public function __toString()
  30.     {
  31.         return $this->_person->getTitle() . ' '
  32.             . $this->_person->getFirstName() . ' '
  33.             . $this->_person->getLastName();
  34.     }
  35. }

Dans cet exemple, nous passons une instance Person au constructeur de TextPerson. Grâce à la surcharge des méthodes, nous pouvons continuer d'appeler les méthodes de Person -- affecter un nom, un prénom, ... -- mais nous pouvons en plus récupérer une représentation sous forme de chaîne grâce à __toString().

Cet exemple est proche du fonctionnement interne des décorateurs de Zend_Form. La différence est qu'au lieu que le décorateur n'encapsule l'objet initial, c'est l'objet élément qui possède en lui un ou plusieurs decorateurs à qui il passe lui-même pour effectuer le rendu visuel. Les décorateurs peuvent ainsi accéder à l'élément et en créer une représentation.

Créer votre premier décorateur

Les décorateurs de Zend_Form implémentent tous, Zend_Form_Decorator_Interface. Cette interface permet de régler les options du décorateur, enregistrer en lui l'élément ainsi qu'effectuer le rendu. Une classe de base, Zend_Form_Decorator_Abstract, propose une implémentation de cette logique de base dont vous aurez besoin, à l'exception du rendu que vous devrez définir.

Imaginons une situation dans laquelle nous souhaitons simplement rendre un élément comme un tag html text avec un libellé (label). Juste la base, nous verrons plus tard la gestion des erreurs et les éventuels autres tags html. Un tel décorateur pourrait ressembler à ça :

  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, $name, $label, $id, $name, $value);
  15.         return $markup;
  16.     }
  17. }

Créons un élément qui utilise ce décorateur :

  1. $decorator = new My_Decorator_SimpleInput();
  2. $element   = new Zend_Form_Element('foo', array(
  3.     'label'      => 'Foo',
  4.     'belongsTo'  => 'bar',
  5.     'value'      => 'test',
  6.     'decorators' => array($decorator),
  7. ));

Le visuel de cet élément donne :

  1. <label for="bar[foo]">Foo</label>
  2. <input id="bar-foo" name="bar[foo]" type="text" value="test"/>

Nous pourrions aussi ranger cette classe dans un dossier de librairie, il faut alors informer l'élément du chemin vers ce dossier, et ensuite faire référence au décorateur comme "SimpleInput" :

  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('SimpleInput'),
  9. ));

Ceci permet de partager du code entre projets et ouvre aussi la possibilité d'étendre dans le futur les classes rangées.

Dans le chapitre suivant, nous allons voir comment combiner les décorateurs afin de créer un affichage par morceaux (composite).

blog comments powered by Disqus