TigerZF
🌐Español

8.3. Superposición de decoradores

Si has seguido atentamente la sección anterior, quizás hayas notado que el método render() de un decorador recibe un único argumento, $content. Se espera que sea una cadena de texto. render() tomará entonces esta cadena y decidirá si la reemplaza, la añade al final o la antepone. Esto te permite tener una cadena de decoradores, lo que a su vez te permite crear decoradores que renderizan únicamente un subconjunto de los metadatos del elemento, y luego superponer estos decoradores para construir el marcado completo del elemento.

Veamos cómo funciona esto en la práctica.

Para la mayoría de los tipos de elementos de formulario, se utilizan los siguientes decoradores:

  • ViewHelper (renderiza la entrada del formulario usando uno de los helpers de vista de formulario estándar).

  • Errors (renderiza los errores de validación mediante una lista sin orden).

  • Description (renderiza cualquier descripción asociada al elemento; a menudo utilizada para tooltips).

  • HtmlTag (envuelve todo lo anterior en una etiqueta <dd>.

  • Label (renderiza la etiqueta que precede a lo anterior, envuelta en una etiqueta <dt>.

Notarás que cada uno de estos decoradores hace solo una cosa, y opera sobre una pieza específica de metadatos almacenada en el elemento de formulario: el decorador Errors extrae los errores de validación y los renderiza; el decorador Label extrae solo la etiqueta y la renderiza. Esto permite que los decoradores individuales sean muy concisos, repetibles y, lo que es más importante, comprobables (testeables).

Aquí es también donde entra en juego el argumento $content: el método render() de cada decorador está diseñado para aceptar contenido, y luego reemplazarlo (normalmente envolviéndolo), anteponerlo o añadirlo al final.

Así pues, es mejor pensar en el proceso de decoración como la construcción de una cebolla desde el interior hacia afuera.

Para simplificar el proceso, veremos de nuevo el ejemplo de la sección anterior. Recordemos:

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

Ahora eliminemos la funcionalidad de la etiqueta y construyamos un decorador separado para ella.

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

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

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

class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
{
    protected $_format = '<label for="%s">%s</label>';

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

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

Ahora bien, esto puede parecer correcto, pero aquí está el problema: tal como está escrito actualmente, el último decorador en ejecutarse gana, y sobrescribe todo lo demás. Terminarás con solo la entrada, o solo la etiqueta, dependiendo de cuál registres al final.

Para superar esto, simplemente concatena el $content recibido con el marcado de alguna manera:

return $content . $markup;

El problema con el enfoque anterior surge cuando quieres elegir programáticamente si el contenido original debe preceder o añadirse a continuación del nuevo marcado. Afortunadamente, ya existe un mecanismo estándar para esto; Zend_Form_Decorator_Abstract tiene un concepto de posicionamiento y define algunas constantes para gestionarlo. Además, permite especificar un separador para colocar entre ambos. Hagamos uso de ellos:

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

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

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

        $placement = $this->getPlacement();
        $separator = $this->getSeparator();
        switch ($placement) {
            case self::PREPEND:
                return $markup . $separator . $content;
            case self::APPEND:
            default:
                return $content . $separator . $markup;
        }
    }
}

class My_Decorator_SimpleLabel extends Zend_Form_Decorator_Abstract
{
    protected $_format = '<label for="%s">%s</label>';

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

        $markup = sprint($this->_format, $id, $label);

        $placement = $this->getPlacement();
        $separator = $this->getSeparator();
        switch ($placement) {
            case self::APPEND:
                return $markup . $separator . $content;
            case self::PREPEND:
            default:
                return $content . $separator . $markup;
        }
    }
}

Nota que en el ejemplo anterior estoy invirtiendo el caso por defecto en cada uno; la suposición será que las etiquetas anteponen contenido, y la entrada lo añade al final.

Ahora, creemos un elemento de formulario que utilice estos decoradores:

$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',
        'SimpleLabel',
    ),
));

¿Cómo funcionará esto? Cuando llamamos a render(), el elemento recorrerá los diversos decoradores adjuntos, llamando a render() en cada uno. Pasará una cadena vacía al primero, y luego el contenido creado se pasará al siguiente, y así sucesivamente:

  • El contenido inicial es una cadena vacía: ''.

  • '' se pasa al decorador SimpleInput, que entonces genera una entrada de formulario que añade a la cadena vacía: <input id="bar-foo" name="bar[foo]" type="text" value="test"/>.

  • La entrada se pasa entonces como contenido al decorador SimpleLabel, que genera una etiqueta y la antepone al contenido original; el separador por defecto es un carácter PHP_EOL, lo que nos da esto: <label for="bar-foo">\n<input id="bar-foo" name="bar[foo]" type="text" value="test"/>.

¡Pero espera un momento! ¿Qué pasa si quisieras que la etiqueta apareciera después de la entrada por alguna razón? ¿Recuerdas ese indicador de "placement"? Puedes pasarlo como opción al decorador. La forma más sencilla de hacerlo es pasar un array de opciones junto con el decorador durante la creación del elemento:

$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'
        array('SimpleLabel', array('placement' => 'append')),
    ),
));

Nota que al pasar opciones, debes envolver el decorador dentro de un array; esto indica al constructor que hay opciones disponibles. El nombre del decorador es el primer elemento del array, y las opciones se pasan en un array como segundo elemento del array.

Lo anterior da como resultado el marcado <input id="bar-foo" name="bar[foo]" type="text" value="test"/>\n<label for="bar-foo">.

Usando esta técnica, puedes tener decoradores que apunten a metadatos específicos del elemento o del formulario y creen solo el marcado relevante para esos metadatos; utilizando múltiples decoradores, puedes luego construir el marcado completo del elemento. Nuestra cebolla es el resultado.

Hay pros y contras en este enfoque. Primero, los contras:

  • Más complejo de implementar. Debes prestar mucha atención a los decoradores que utilizas y qué posicionamiento aplicas para construir el marcado en la secuencia correcta.

  • Más intensivo en recursos. Más decoradores significa más objetos; multiplica esto por el número de elementos que tengas en un formulario, y puede que termines con un uso de recursos considerable. El caché puede ayudar aquí.

Sin embargo, las ventajas son convincentes:

  • Decoradores reutilizables. Puedes crear decoradores verdaderamente reutilizables con esta técnica, ya que no tienes que preocuparte por el marcado completo, sino solo por el marcado de una o pocas piezas de metadatos del elemento o del formulario.

  • Máxima flexibilidad. En teoría, puedes generar cualquier combinación de marcado que desees a partir de un pequeño número de decoradores.

Si bien los ejemplos anteriores son el uso previsto de los decoradores dentro de Zend_Form, a menudo es difícil entender cómo interactúan los decoradores entre sí para construir el marcado final. Por esta razón, se añadió cierta flexibilidad en la serie 1.7 para hacer posible renderizar decoradores individuales, lo que da cierta simplicidad al estilo Rails para renderizar formularios. Veremos eso en la siguiente sección.