TigerZF
🌐Español

8.5. Creación y renderizado de elementos compuestos

En la sección anterior, tuvimos un ejemplo que mostraba un "elemento de fecha de nacimiento":

<div class="element">
    <?php echo $form->dateOfBirth->renderLabel() ?>
    <?php echo $this->formText('dateOfBirth[day]', '', array(
        'size' => 2, 'maxlength' => 2)) ?>
    /
    <?php echo $this->formText('dateOfBirth[month]', '', array(
        'size' => 2, 'maxlength' => 2)) ?>
    /
    <?php echo $this->formText('dateOfBirth[year]', '', array(
        'size' => 4, 'maxlength' => 4)) ?>
</div>

¿Cómo podría representarse este elemento como un Zend_Form_Element? ¿Cómo podría escribirse un decorador para renderizarlo?

8.5.1. El elemento

Las preguntas sobre cómo funcionaría el elemento incluyen:

  • ¿Cómo se establecería y recuperaría el valor?

  • ¿Cómo se validaría el valor?

  • Independientemente de eso, ¿cómo se permitirían entradas de formulario discretas para los tres segmentos (día, mes, año)?

Las dos primeras preguntas se centran en el propio elemento de formulario: ¿cómo funcionarían setValue() y getValue()? De hecho, hay otra pregunta implícita en la pregunta sobre el decorador: ¿cómo se recuperarían los segmentos discretos de la fecha desde el elemento y/o cómo se establecerían?

La solución es sobrescribir el método setValue() de nuestro elemento para proporcionar cierta lógica personalizada. En este caso particular, nuestro elemento debería tener tres comportamientos discretos:

  • Si se proporciona una marca de tiempo entera, debería usarse para determinar y almacenar el día, el mes y el año.

  • Si se proporciona una cadena de texto, debería convertirse a una marca de tiempo, y luego ese valor se usaría para determinar y almacenar el día, el mes y el año.

  • Si se proporciona un array que contiene claves para día, mes y año, esos valores deberían almacenarse.

Internamente, el día, el mes y el año se almacenarán de forma discreta. Cuando se recupera el valor del elemento, se hará en un formato de cadena normalizado. También sobrescribiremos getValue() para ensamblar los segmentos discretos de fecha en una cadena final.

Así es como se vería la clase:

class My_Form_Element_Date extends Zend_Form_Element_Xhtml
{
    protected $_dateFormat = '%year%-%month%-%day%';
    protected $_day;
    protected $_month;
    protected $_year;

    public function setDay($value)
    {
        $this->_day = (int) $value;
        return $this;
    }

    public function getDay()
    {
        return $this->_day;
    }

    public function setMonth($value)
    {
        $this->_month = (int) $value;
        return $this;
    }

    public function getMonth()
    {
        return $this->_month;
    }

    public function setYear($value)
    {
        $this->_year = (int) $value;
        return $this;
    }

    public function getYear()
    {
        return $this->_year;
    }

    public function setValue($value)
    {
        if (is_int($value)) {
            $this->setDay(date('d', $value))
                 ->setMonth(date('m', $value))
                 ->setYear(date('Y', $value));
        } elseif (is_string($value)) {
            $date = strtotime($value);
            $this->setDay(date('d', $date))
                 ->setMonth(date('m', $date))
                 ->setYear(date('Y', $date));
        } elseif (is_array($value)
                  && (isset($value['day'])
                      && isset($value['month'])
                      && isset($value['year'])
                  )
        ) {
            $this->setDay($value['day'])
                 ->setMonth($value['month'])
                 ->setYear($value['year']);
        } else {
            throw new Exception('Invalid date value provided');
        }

        return $this;
    }

    public function getValue()
    {
        return str_replace(
            array('%year%', '%month%', '%day%'),
            array($this->getYear(), $this->getMonth(), $this->getDay()),
            $this->_dateFormat
        );
    }
}

Esta clase ofrece una buena flexibilidad -- podemos establecer valores por defecto desde nuestra base de datos, y estar seguros de que el valor se almacenará y representará correctamente. Además, podemos permitir que el valor se establezca a partir de un array pasado a través de la entrada del formulario. Por último, tenemos accesores discretos para cada segmento de fecha, que ahora podemos usar en un decorador para crear un elemento compuesto.

8.5.2. El decorador

Retomando el ejemplo de la sección anterior, supongamos que queremos que los usuarios introduzcan el año, el mes y el día por separado. Afortunadamente, PHP nos permite usar la notación de array al crear elementos, por lo que sigue siendo posible capturar estas tres entidades en un único valor -- y ahora hemos creado un elemento Zend_Form que puede manejar ese tipo de valor de array.

El decorador es relativamente sencillo: tomará el día, el mes y el año del elemento, y pasará cada uno a un helper de vista discreto para renderizar entradas de formulario individuales; estas se agregarán después para formar el marcado final.

class My_Form_Decorator_Date extends Zend_Form_Decorator_Abstract
{
    public function render($content)
    {
        $element = $this->getElement();
        if (!$element instanceof My_Form_Element_Date) {
            // only want to render Date elements
            return $content;
        }

        $view = $element->getView();
        if (!$view instanceof Zend_View_Interface) {
            // using view helpers, so do nothing if no view present
            return $content;
        }

        $day   = $element->getDay();
        $month = $element->getMonth();
        $year  = $element->getYear();
        $name  = $element->getFullyQualifiedName();

        $params = array(
            'size'      => 2,
            'maxlength' => 2,
        );
        $yearParams = array(
            'size'      => 4,
            'maxlength' => 4,
        );

        $markup = $view->formText($name . '[day]', $day, $params)
                . ' / ' . $view->formText($name . '[month]', $month, $params)
                . ' / ' . $view->formText($name . '[year]', $year, $yearParams);

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

Ahora tenemos que hacer un pequeño ajuste a nuestro elemento de formulario, e indicarle que queremos usar el decorador anterior por defecto. Esto se hace en dos pasos. Primero, necesitamos informar al elemento de la ruta del decorador. Podemos hacerlo en el constructor:

class My_Form_Element_Date extends Zend_Form_Element_Xhtml
{
    // ...

    public function __construct($spec, $options = null)
    {
        $this->addPrefixPath(
            'My_Form_Decorator',
            'My/Form/Decorator',
            'decorator'
        );
        parent::__construct($spec, $options);
    }

    // ...
}

Tenga en cuenta que esto se hace en el constructor y no en init(). Esto es por dos razones. Primero, permite extender el elemento más adelante para añadir lógica en init sin necesidad de preocuparse por llamar a parent::init(). Segundo, permite pasar rutas de plugin adicionales a través de la configuración o dentro de un método init que luego permitirá sobrescribir el decorador Date por defecto con nuestro propio reemplazo.

A continuación, necesitamos sobrescribir el método loadDefaultDecorators() para usar nuestro nuevo decorador Date:

class My_Form_Element_Date extends Zend_Form_Element_Xhtml
{
    // ...

    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }

        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            $this->addDecorator('Date')
                 ->addDecorator('Errors')
                 ->addDecorator('Description', array(
                     'tag'   => 'p',
                     'class' => 'description'
                 ))
                 ->addDecorator('HtmlTag', array(
                     'tag' => 'dd',
                     'id'  => $this->getName() . '-element'
                 ))
                 ->addDecorator('Label', array('tag' => 'dt'));
        }
    }

    // ...
}

¿Cómo se ve el resultado final? Consideremos el siguiente elemento:

$d = new My_Form_Element_Date('dateOfBirth');
$d->setLabel('Date of Birth: ')
  ->setView(new Zend_View());

// These are equivalent:
$d->setValue('20 April 2009');
$d->setValue(array('year' => '2009', 'month' => '04', 'day' => '20'));

Si luego imprime este elemento (echo), obtendrá el siguiente marcado (con algunas modificaciones menores de espacios en blanco para facilitar la lectura):

<dt id="dateOfBirth-label"><label for="dateOfBirth" class="optional">
    Date of Birth:
</label></dt>
<dd id="dateOfBirth-element">
    <input type="text" name="dateOfBirth[day]" id="dateOfBirth-day"
        value="20" size="2" maxlength="2"> /
    <input type="text" name="dateOfBirth[month]" id="dateOfBirth-month"
        value="4" size="2" maxlength="2"> /
    <input type="text" name="dateOfBirth[year]" id="dateOfBirth-year"
        value="2009" size="4" maxlength="4">
</dd>

8.5.3. Conclusión

Ahora tenemos un elemento que puede renderizar múltiples campos de entrada de formulario relacionados, y luego manejar los campos agregados como una única entidad -- el elemento dateOfBirth se pasará como un array al elemento, y el elemento entonces, como señalamos antes, creará los segmentos de fecha adecuados y devolverá un valor que podemos usar para la mayoría de los backends.

Además, podemos usar diferentes decoradores con el elemento. Si quisiéramos usar un dijit DateTextBox de Dojo como decorador -- que acepta y devuelve valores de cadena -- podemos hacerlo, sin modificaciones al elemento en sí.

Al final, se obtiene un API de elemento uniforme que se puede usar para describir un elemento que representa un valor compuesto.