<?xml version="1.0" encoding="UTF-8"?>
<!-- EN-Revision: 15103 -->
<!-- Reviewed: no -->
<sect1 id="zend.form.advanced">
    <title>Uso avanzado de Zend_Form</title>

    <para>
        <classname>Zend_Form</classname> tiene una funcional riqueza, muchas de ellas dirigidas
        a expertos desarroladores. Este capítulo esta dirigido a documentar algunas de las
        funcionalidades con ejemplos y casos de uso.
    </para>

    <sect2 id="zend.form.advanced.arrayNotation">
        <title>Notación de array</title>

        <para>
            A muchos desarroladores web experimentados les gusta agrupar elementos relacionados de formulario
            usando notación de array en los nombres del elemento. Por ejemplo, si se tienen
            dos direcciones que se desean capturar, un envío y una dirección de facturación,
            se pueden tener elementos idénticos; agrupándolos en un array se puede
            asegurar que son capturados por separado. Nótese el siguiente formulario
            por ejemplo:
        </para>

        <programlisting language="html"><![CDATA[
<form>
    <fieldset>
        <legend>Shipping Address</legend>
        <dl>
            <dt><label for="recipient">Ship to:</label></dt>
            <dd><input name="recipient" type="text" value="" /></dd>

            <dt><label for="address">Address:</label></dt>
            <dd><input name="address" type="text" value="" /></dd>

            <dt><label for="municipality">City:</label></dt>
            <dd><input name="municipality" type="text" value="" /></dd>

            <dt><label for="province">State:</label></dt>
            <dd><input name="province" type="text" value="" /></dd>

            <dt><label for="postal">Postal Code:</label></dt>
            <dd><input name="postal" type="text" value="" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Billing Address</legend>
        <dl>
            <dt><label for="payer">Bill To:</label></dt>
            <dd><input name="payer" type="text" value="" /></dd>

            <dt><label for="address">Address:</label></dt>
            <dd><input name="address" type="text" value="" /></dd>

            <dt><label for="municipality">City:</label></dt>
            <dd><input name="municipality" type="text" value="" /></dd>

            <dt><label for="province">State:</label></dt>
            <dd><input name="province" type="text" value="" /></dd>

            <dt><label for="postal">Postal Code:</label></dt>
            <dd><input name="postal" type="text" value="" /></dd>
        </dl>
    </fieldset>

    <dl>
        <dt><label for="terms">I agree to the Terms of Service</label></dt>
        <dd><input name="terms" type="checkbox" value="" /></dd>

        <dt></dt>
        <dd><input name="save" type="submit" value="Save" /></dd>
    </dl>
</form>
]]></programlisting>

        <para>
            En este ejemplo, la facturación y la dirección de envío contienen algunos
            campos idénticos, eso significa que uno puede sobrescribir al otro. Nosotros podemos
            resolver esta solución usando una notación de array:
        </para>

        <programlisting language="html"><![CDATA[
<form>
    <fieldset>
        <legend>Shipping Address</legend>
        <dl>
            <dt><label for="shipping-recipient">Ship to:</label></dt>
            <dd><input name="shipping[recipient]" id="shipping-recipient"
                type="text" value="" /></dd>

            <dt><label for="shipping-address">Address:</label></dt>
            <dd><input name="shipping[address]" id="shipping-address"
                type="text" value="" /></dd>

            <dt><label for="shipping-municipality">City:</label></dt>
            <dd><input name="shipping[municipality]" id="shipping-municipality"
                type="text" value="" /></dd>

            <dt><label for="shipping-province">State:</label></dt>
            <dd><input name="shipping[province]" id="shipping-province"
                type="text" value="" /></dd>

            <dt><label for="shipping-postal">Postal Code:</label></dt>
            <dd><input name="shipping[postal]" id="shipping-postal"
                type="text" value="" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Billing Address</legend>
        <dl>
            <dt><label for="billing-payer">Bill To:</label></dt>
            <dd><input name="billing[payer]" id="billing-payer"
                type="text" value="" /></dd>

            <dt><label for="billing-address">Address:</label></dt>
            <dd><input name="billing[address]" id="billing-address"
                type="text" value="" /></dd>

            <dt><label for="billing-municipality">City:</label></dt>
            <dd><input name="billing[municipality]" id="billing-municipality"
                type="text" value="" /></dd>

            <dt><label for="billing-province">State:</label></dt>
            <dd><input name="billing[province]" id="billing-province"
                type="text" value="" /></dd>

            <dt><label for="billing-postal">Postal Code:</label></dt>
            <dd><input name="billing[postal]" id="billing-postal"
                type="text" value="" /></dd>
        </dl>
    </fieldset>

    <dl>
        <dt><label for="terms">I agree to the Terms of Service</label></dt>
        <dd><input name="terms" type="checkbox" value="" /></dd>

        <dt></dt>
        <dd><input name="save" type="submit" value="Save" /></dd>
    </dl>
</form>
]]></programlisting>

        <para>
            En el ejemplo anterior, obtenemos direcciones separadas. En el formulario
            sometido, ahora tenemos tres elementos, 'guardar' elemento para someterlo,
            y dos arrays, 'envio' y 'cuenta', cada uno con
            llaves para los variados elementos.
        </para>

        <para>
            <classname>Zend_Form</classname> intenta automatizar este proceso con los
            <link linkend="zend.form.forms.subforms">subformularios</link>. Por
            defecto, los subformularios son generados usando la notación de array como se muestra
            en el anterior formulario HTML listado completo con identificadores. El nombre del array
            esta basado en el nombre del subformulario, con las llaves basados en los elementos
            contenidos  en el subformulario. Los subformularios pueder ser anidados arbitrariamente,
            y esto puede crear arrays anidados que reflejan la estructura.
            Adicionalmente, las validaciones rutinarias en
            <classname>Zend_Form</classname> respetan la estructura del array, asegurando que sus
            formularios sean validados correctamente, no importa cuan arbitrariamente anidados esten los
            subformularios. No se necesita hacer nada para beneficiarse; éste
            comportamiento esta activo por defecto.
        </para>

        <para>
            Adicionalmente, existen facilidades que le permiten activar condicionalmente la notación
            de un array, así como también especificar el específico array al
            cual un elemento o coleccion pertenece:
        </para>

        <itemizedlist>
            <listitem>
                <para>
                    <methodname>Zend_Form::setIsArray($flag)</methodname>: Definiendo la
                    bandera a verdadero, se puede indicar que un formulario entero debería ser
                    tratado como un array. Por defecto, el nombre del formulario será
                    usado como el nombre del array, a no ser que
                    <methodname>setElementsBelongTo()</methodname> haya sido llamado. Si el
                    formulario no tiene un nombre específico, o si
                    <methodname>setElementsBelongTo()</methodname> no ha sido definido, esta bandera
                    será ignorada (como cuando no hay nombre del array al cual
                    los elementos puedan pertenecer).
                </para>

                <para>
                    Se deberá determinar si un formulario está siendo tratado como un array
                    usando el accesor <methodname>isArray()</methodname>.
                </para>
            </listitem>

            <listitem><para>
                    <methodname>Zend_Form::setElementsBelongTo($array)</methodname>:
                    Usando este método, se puede especificar el nombre de un array al
                    cual todos los elementos del formulario pertenecen. Se puede determinar el
                    nombre usando el accesor <methodname>getElementsBelongTo()</methodname>.
            </para></listitem>
        </itemizedlist>

        <para>
            Adicionalmente, a nivel del elemento, se pueden especificar
            elementos individuales que puedan pertenecer a arrays particulares usando
            el método <classname>Zend_Form_Element::setBelongsTo()</classname>.
            Para descubrir el valor que tiene -- sea o no sea definido explícitamente o
            implícitamente a través del formulario -- se puede usar el
            accesor <methodname>getBelongsTo()</methodname>.
        </para>
    </sect2>

    <sect2 id="zend.form.advanced.multiPage">
        <title>Formularios Multi-Página</title>

        <para>
            Actualmente, los formularios multi-página no están oficialmente soportados en
            <classname>Zend_Form</classname>; sin embargo, la mayoría del soporte para implementarlos
            está disponible y puede ser utilizado con algunos retoques.
        </para>

        <para>
            La clave para crear fomrularios multi-página es utilizar subformularios, pero
            solo para mostrar un solo subformulario por página. Esto le permite
            someter un solo subformulario a la vez y validarlo, pero no procesar
            el formulario hasta que todos los subformularios esten completos.
        </para>

        <example id="zend.form.advanced.multiPage.registration">
            <title>Ejemplo de formulario de registro</title>
            <para>
                Vamos a usar un formulario de registro como ejemplo. Para nuestros propósitos,
                queremos capturar el nombre del usuario y la contraseña en la
                primera página, después la información del usuario -- nombre, apellido,
                y ubicación -- y finalmente permitirles decidir qué lista de correo,
                si desean suscribirse.
            </para>

            <para>
                Primero, vamos a crear nuestro propio formulario, y definir varios subformularios
                dentro del mismo:
            </para>

             <programlisting language="php"><![CDATA[
class My_Form_Registration extends Zend_Form
{
    public function init()
    {
        // Crea un subformulario usuario: username y password
        $user = new Zend_Form_SubForm();
        $user->addElements(array(
            new Zend_Form_Element_Text('username', array(
                'required'   => true,
                'label'      => 'Username:',
                'filters'    => array('StringTrim', 'StringToLower'),
                'validators' => array(
                    'Alnum',
                    array('Regex',
                          false,
                          array('/^[a-z][a-z0-9]{2,}$/'))
                )
            )),

            new Zend_Form_Element_Password('password', array(
                'required'   => true,
                'label'      => 'Password:',
                'filters'    => array('StringTrim'),
                'validators' => array(
                    'NotEmpty',
                    array('StringLength', false, array(6))
                )
            )),
        ));

        // Crea un subformulario de datos demográficos : given name, family name, y
        // location
        $demog = new Zend_Form_SubForm();
        $demog->addElements(array(
            new Zend_Form_Element_Text('givenName', array(
                'required'   => true,
                'label'      => 'Given (First) Name:',
                'filters'    => array('StringTrim'),
                'validators' => array(
                    array('Regex',
                          false,
                          array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
                )
            )),

            new Zend_Form_Element_Text('familyName', array(
                'required'   => true,
                'label'      => 'Family (Last) Name:',
                'filters'    => array('StringTrim'),
                'validators' => array(
                    array('Regex',
                          false,
                          array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
                )
            )),

            new Zend_Form_Element_Text('location', array(
                'required'   => true,
                'label'      => 'Your Location:',
                'filters'    => array('StringTrim'),
                'validators' => array(
                    array('StringLength', false, array(2))
                )
            )),
        ));

        // Crea un sub fomulario de correos
        $listOptions = array(
            'none'        => 'No lists, please',
            'fw-general'  => 'Zend Framework General List',
            'fw-mvc'      => 'Zend Framework MVC List',
            'fw-auth'     => 'Zend Framwork Authentication and ACL List',
            'fw-services' => 'Zend Framework Web Services List',
        );
        $lists = new Zend_Form_SubForm();
        $lists->addElements(array(
            new Zend_Form_Element_MultiCheckbox('subscriptions', array(
                'label'        =>
                    'Which lists would you like to subscribe to?',
                'multiOptions' => $listOptions,
                'required'     => true,
                'filters'      => array('StringTrim'),
                'validators'   => array(
                    array('InArray',
                          false,
                          array(array_keys($listOptions)))
                )
            )),
        ));

        // Adjuntando los subformlarios al formulario principal
        $this->addSubForms(array(
            'user'  => $user,
            'demog' => $demog,
            'lists' => $lists
        ));
    }
}
]]></programlisting>

            <para>
                Note que no hay botones de enviar, y que ni hemos hecho
                nada con los decoradores de subformularios -- lo que significa que por
                defecto serán desplegados como campos. Necesitaremos hacer
                algo con ellos mientras desplegamos cada subformulario individualmente,
                y añadir botones de manera que podamos procesarlos realmente --
                el cual requerira las propiedades acción y método. Vamos a añadir
                algunos andamios a nuestras clases para proveer esa información:
            </para>

             <programlisting language="php"><![CDATA[
class My_Form_Registration extends Zend_Form
{
    // ...

    /**
     * Prepara un subformulario para mostrar
     *
     * @param  string|Zend_Form_SubForm $spec
     * @return Zend_Form_SubForm
     */
    public function prepareSubForm($spec)
    {
        if (is_string($spec)) {
            $subForm = $this->{$spec};
        } elseif ($spec instanceof Zend_Form_SubForm) {
            $subForm = $spec;
        } else {
            throw new Exception('Invalid argument passed to ' .
                                __FUNCTION__ . '()');
        }
        $this->setSubFormDecorators($subForm)
             ->addSubmitButton($subForm)
             ->addSubFormActions($subForm);
        return $subForm;
    }

    /**
     * Add form decorators to an individual sub form
     *
     * @param  Zend_Form_SubForm $subForm
     * @return My_Form_Registration
     */
    public function setSubFormDecorators(Zend_Form_SubForm $subForm)
    {
        $subForm->setDecorators(array(
            'FormElements',
            array('HtmlTag', array('tag' => 'dl',
                                   'class' => 'zend_form')),
            'Form',
        ));
        return $this;
    }

    /**
     * Añade un Boton de envio(submit) a cada subformulario
     *
     * @param  Zend_Form_SubForm $subForm
     * @return My_Form_Registration
     */
    public function addSubmitButton(Zend_Form_SubForm $subForm)
    {
        $subForm->addElement(new Zend_Form_Element_Submit(
            'save',
            array(
                'label'    => 'Save and continue',
                'required' => false,
                'ignore'   => true,
            )
        ));
        return $this;
    }

    /**
     * Añade el method y el action a cada subformulario
     *
     * @param  Zend_Form_SubForm $subForm
     * @return My_Form_Registration
     */
    public function addSubFormActions(Zend_Form_SubForm $subForm)
    {
        $subForm->setAction('/registration/process')
                ->setMethod('post');
        return $this;
    }
}
]]></programlisting>

            <para>
                Siguiente, necesitamos añadir andamios a nuestro action controller,
                y tener varias consideraciones. Primero, necesitamos asegurar que
                persiste la información del formulario entre los requerimientos, de esa manera determinar
                cuándo terminar. Segundo, necesitamos alguna lógica para determinar qué segmentos
                del formulario han sido sometidos, y qué subformulario
                mostrar de acuerdo a la información. Usaremos
                <classname>Zend_Session_Namespace</classname> para persistir la información, el cual
                nos ayudará a responder la pregunta de qué formulario someter.
            </para>

            <para>
                Vamos a crear nuestro controlador, y añadir un método para recuperar un
                formulario instanciado:
            </para>

             <programlisting language="php"><![CDATA[
class RegistrationController extends Zend_Controller_Action
{
    protected $_form;

    public function getForm()
    {
        if (null === $this->_form) {
            $this->_form = new My_Form_Registration();
        }
        return $this->_form;
    }
}
]]></programlisting>

            <para>
                Ahora, vamos a añadir algunas funcionalidades para determinar qué formulario
                mostrar. Básicamente, hasta que el formulario entero sea considerado válido,
                necesitamos continuar mostrando segmentos de formulario. Adicionalmente,
                queremos asegurar que están en un orden particular: usuario,
                demog, y después las listas. Podemos determinar qué información ha sido
                sometida verificando nuestro session namespace para claves particulares
                representando cada subformulario.
            </para>

             <programlisting language="php"><![CDATA[
class RegistrationController extends Zend_Controller_Action
{
    // ...

    protected $_namespace = 'RegistrationController';
    protected $_session;

    /**
     * Obtiene el namespace de la sesión que estamos usando
     *
     * @return Zend_Session_Namespace
     */
    public function getSessionNamespace()
    {
        if (null === $this->_session) {
            $this->_session =
                new Zend_Session_Namespace($this->_namespace);
        }

        return $this->_session;
    }

    /**
     * Obtiene la lista de Formularios que ya están almacenados en la sesión
     *
     * @return array
     */
    public function getStoredForms()
    {
        $stored = array();
        foreach ($this->getSessionNamespace() as $key => $value) {
            $stored[] = $key;
        }

        return $stored;
    }

    /**
     * Obtiene la lista de todos los subformularios disponibles
     *
     * @return array
     */
    public function getPotentialForms()
    {
        return array_keys($this->getForm()->getSubForms());
    }

    /**
     * ¿Qué subformulario se envio?
     *
     * @return false|Zend_Form_SubForm
     */
    public function getCurrentSubForm()
    {
        $request = $this->getRequest();
        if (!$request->isPost()) {
            return false;
        }

        foreach ($this->getPotentialForms() as $name) {
            if ($data = $request->getPost($name, false)) {
                if (is_array($data)) {
                    return $this->getForm()->getSubForm($name);
                    break;
                }
            }
        }

        return false;
    }

    /**
     * Obtiene el siguiente subformulario para mostrarlo
     *
     * @return Zend_Form_SubForm|false
     */
    public function getNextSubForm()
    {
        $storedForms    = $this->getStoredForms();
        $potentialForms = $this->getPotentialForms();

        foreach ($potentialForms as $name) {
            if (!in_array($name, $storedForms)) {
                return $this->getForm()->getSubForm($name);
            }
        }

        return false;
    }
}
]]></programlisting>

            <para>
                El método de arriba nos permite usar notaciones tal como "<methodname>$subForm =
                    $this-&gt;getCurrentSubForm();</methodname>" recuperar el actual
                subformulario para la validación, o "<methodname>$next =
                    $this-&gt;getNextSubForm();</methodname>" obtener el siguiente para
                mostrar.
            </para>

            <para>
                Ahora, vamos a encontrar la manera para procesar y mostrar varios
                subformularios. Podemos usar <methodname>getCurrentSubForm()</methodname> para determinar
                si algún subformulario ha sido sometido (los valores de retorno falso indican que
                ninguno ha sido desplegado o sometido), y
                <methodname>getNextSubForm()</methodname> recupera el formulario que mostrar. Podemos
                entonces usar el método del formulario <methodname>prepareSubForm()</methodname> para asegurar
                que el formulario está listo para mostrar.
            </para>

            <para>
                Cuando tenemos un formulario sometido, podemos validar el subformulario,
                y luego verificar si el formulario entero es válido ahora. Para hacer
                esas tareas, necesitamos métodos adicionales que aseguren que la
                información sometida es añadida a la sesión, y que cuando validamos
                el formulario entero, nosotros validamos contra todos los segmentos de
                la sesión:
            </para>

             <programlisting language="php"><![CDATA[
class RegistrationController extends Zend_Controller_Action
{
    // ...

    /**
     * ¿Es válido el subformulario?
     *
     * @param  Zend_Form_SubForm $subForm
     * @param  array $data
     * @return bool
     */
    public function subFormIsValid(Zend_Form_SubForm $subForm,
                                   array $data)
    {
        $name = $subForm->getName();
        if ($subForm->isValid($data)) {
            $this->getSessionNamespace()->$name = $subForm->getValues();
            return true;
        }

        return false;
    }

    /**
     * ¿Es válido todo el formulario?
     *
     * @return bool
     */
    public function formIsValid()
    {
        $data = array();
        foreach ($this->getSessionNamespace() as $key => $info) {
            $data[$key] = $info;
        }

        return $this->getForm()->isValid($data);
    }
}
]]></programlisting>

            <para>
                Ahora que tenemos el trabajo preparado, vamos a construir las
                acciones para este controlador. Necesitaremos una página de destino
                para el formulario, y luego una acción 'process' para procesar el formulario.
            </para>

             <programlisting language="php"><![CDATA[
class RegistrationController extends Zend_Controller_Action
{
    // ...

    public function indexAction()
    {
        // volver a mostrar la página actual, o mostrar el "siguiente"
        // (primer) subformulario
        if (!$form = $this->getCurrentSubForm()) {
            $form = $this->getNextSubForm();
        }
        $this->view->form = $this->getForm()->prepareSubForm($form);
    }

    public function processAction()
    {
        if (!$form = $this->getCurrentSubForm()) {
            return $this->_forward('index');
        }

        if (!$this->subFormIsValid($form,
                                   $this->getRequest()->getPost())) {
            $this->view->form = $this->getForm()->prepareSubForm($form);
            return $this->render('index');
        }

        if (!$this->formIsValid()) {
            $form = $this->getNextSubForm();
            $this->view->form = $this->getForm()->prepareSubForm($form);
            return $this->render('index');
        }

        // Formulario Válido!
        // Render information in a verification page
        $this->view->info = $this->getSessionNamespace();
        $this->render('verification');
    }
}
]]></programlisting>

            <para>
                Como se ha notado, el código actual para procesar el formulario es
                relativamente simple. Verificamos si tenemos un subformulario actual
                sometido y si no, retornamos a la página de destino. Si tenemos
                un subformulario, intentaremos validarlo, volviéndolo a mostrar si tiene
                fallos. Si el subformulario es válido, entonces verificaremos si
                el formulario es válido, lo que debería indicar que hemos terminado; si no,
                mostraremos el siguiente segmento del formulario. Finalmente, mostraremos una
                página de verificación con el contenido de la sesión.
            </para>

            <para>
                Los scripts de vista son muy simples:
            </para>

             <programlisting language="php"><![CDATA[
<? // registration/index.phtml ?>
<h2>registro</h2>
<?php echo  $this->form ?>

<? // registration/verification.phtml ?>
<h2>Gracias por Registrarse!</h2>
<p>
    Aquí está la información que nos ha proporcionado:
</p>

<?
// Tienen que construir esto con los items que estan almacenados en los namespaces
// de la sesión
foreach ($this->info as $info):
    foreach ($info as $form => $data): ?>
<h4><?php echo  ucfirst($form) ?>:</h4>
<dl>
    <? foreach ($data as $key => $value): ?>
    <dt><?php echo  ucfirst($key) ?></dt>
    <? if (is_array($value)):
        foreach ($value as $label => $val): ?>
    <dd><?php echo  $val ?></dd>
        <? endforeach;
       else: ?>
    <dd><?php echo  $this->escape($value) ?></dd>
    <? endif;
    endforeach; ?>
</dl>
<? endforeach;
endforeach
]]></programlisting>

            <para>
                Próximas novedades de Zend Framework incluirán componentes
                para hacer formularios multi páginas mas simples, abstrayendo la sesión y
                la lógica de orden. Mientras tanto, el ejemplo de arriba debería servir
                como guia razonable para alcanzar esta tarea en su web.
            </para>
        </example>
    </sect2>
</sect1>
<!--
vim:se ts=4 sw=4 et:
-->
