TigerZF
🌐Español

8.2. Fundamentos de los decoradores

8.2.1. Resumen del patrón decorador

Para empezar, cubriremos algunos antecedentes sobre el patrón de diseño Decorator. Una técnica común consiste en definir una interfaz común que tanto el objeto original como el decorador implementarán; su decorador acepta entonces el objeto original como una dependencia, y ya sea que le haga de proxy o sobrescriba sus métodos. Vamos a plasmar esto en código para entenderlo más fácilmente:

interface Window
{
    public function isOpen();
    public function open();
    public function close();
}

class StandardWindow implements Window
{
    protected $_open = false;

    public function isOpen()
    {
        return $this->_open;
    }

    public function open()
    {
        if (!$this->_open) {
            $this->_open = true;
        }
    }

    public function close()
    {
        if ($this->_open) {
            $this->_open = false;
        }
    }
}

class LockedWindow implements Window
{
    protected $_window;

    public function __construct(Window $window)
    {
        $this->_window = $window;
        $this->_window->close();
    }

    public function isOpen()
    {
        return false;
    }

    public function open()
    {
        throw new Exception('Cannot open locked windows');
    }

    public function close()
    {
        $this->_window->close();
    }
}

Luego crea un objeto de tipo StandardWindow, lo pasa al constructor de LockedWindow, y su instancia de ventana ahora tiene un comportamiento diferente. Lo hermoso es que no tiene que implementar ningún tipo de funcionalidad de "bloqueo" en su clase de ventana estándar: el decorador se encarga de eso por usted. Mientras tanto, puede pasar su ventana bloqueada como si fuera simplemente otra ventana.

Un lugar en particular donde el patrón decorador resulta útil es para crear representaciones textuales de objetos. Como ejemplo, podría tener un objeto "Person" que, por sí mismo, no tiene representación textual. Usando el patrón Decorator, puede crear un objeto que actuará como si fuera una Person, pero que también proporcione la capacidad de representar a esa Person de forma textual.

En este ejemplo en particular, vamos a usar duck typing en lugar de una interfaz explícita. Esto permite que nuestra implementación sea un poco más flexible, mientras sigue permitiendo que el objeto decorador actúe exactamente como si fuera un objeto Person.

class Person
{
    public function setFirstName($name) {}
    public function getFirstName() {}
    public function setLastName($name) {}
    public function getLastName() {}
    public function setTitle($title) {}
    public function getTitle() {}
}

class TextPerson
{
    protected $_person;

    public function __construct(Person $person)
    {
        $this->_person = $person;
    }

    public function __call($method, $args)
    {
        if (!method_exists($this->_person, $method)) {
            throw new Exception('Invalid method called on HtmlPerson: '
                .  $method);
        }
        return call_user_func_array(array($this->_person, $method), $args);
    }

    public function __toString()
    {
        return $this->_person->getTitle() . ' '
            . $this->_person->getFirstName() . ' '
            . $this->_person->getLastName();
    }
}

En este ejemplo, pasa su instancia de Person al constructor de TextPerson. Al usar la sobrecarga de métodos, puede seguir llamando a todos los métodos de Person --para establecer el nombre, el apellido o el título-- pero también obtiene ahora una representación en forma de cadena mediante el método __toString().

Este último ejemplo se acerca mucho a cómo funcionan los decoradores de Zend_Form. La diferencia clave es que, en lugar de que un decorador envuelva al elemento, el elemento tiene uno o más decoradores adjuntos, dentro de los cuales se inyecta a sí mismo para poder renderizarse. El decorador puede entonces acceder a los métodos y propiedades del elemento para crear una representación del elemento, o de un subconjunto de este.

8.2.2. Creando su primer decorador

Todos los decoradores de Zend_Form implementan una interfaz común, Zend_Form_Decorator_Interface. Esa interfaz proporciona la capacidad de establecer opciones específicas del decorador, registrar y obtener el elemento, y renderizar. Un decorador base, Zend_Form_Decorator_Abstract, proporciona la mayor parte de la funcionalidad que necesitará, con excepción de la lógica de renderizado.

Consideremos una situación en la que simplemente queremos renderizar un elemento como un campo de texto de formulario estándar con una etiqueta. No nos preocuparemos por el manejo de errores ni por si el elemento debería envolverse dentro de otras etiquetas por ahora --solo lo básico. Un decorador así podría verse de la siguiente manera:

class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract
{
    protected $_format = '<label for="%s">%s</label>'
                       . '<input id="%s" name="%s" type="text" value="%s"/>';

    public function render($content)
    {
        $element = $this->getElement();
        $name    = htmlentities($element->getFullyQualifiedName());
        $label   = htmlentities($element->getLabel());
        $id      = htmlentities($element->getId());
        $value   = htmlentities($element->getValue());

        $markup  = sprintf($this->_format, $name, $label, $id, $name, $value);
        return $markup;
    }
}

Creemos un elemento que use este decorador:

$decorator = new My_Decorator_SimpleInput();
$element   = new Zend_Form_Element('foo', array(
    'label'      => 'Foo',
    'belongsTo'  => 'bar',
    'value'      => 'test',
    'decorators' => array($decorator),
));

Al renderizar este elemento se obtiene el siguiente marcado:

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

También podría colocar esta clase en algún lugar de su biblioteca, informar a su elemento de esa ruta, y referirse al decorador simplemente como "SimpleInput":

$element = new Zend_Form_Element('foo', array(
    'label'      => 'Foo',
    'belongsTo'  => 'bar',
    'value'      => 'test',
    'prefixPath' => array('decorator' => array(
        'My_Decorator' => 'path/to/decorators/',
    )),
    'decorators' => array('SimpleInput'),
));

Esto le da la ventaja de reutilización en otros proyectos, y también abre la puerta a ofrecer implementaciones alternativas de ese decorador más adelante.

En la siguiente sección, veremos cómo combinar decoradores para crear una salida compuesta.