TigerZF
🌐Español

36.9. Uso avanzado de Zend_Form

Zend_Form cuenta con una gran cantidad de funcionalidad, gran parte de ella orientada a desarrolladores experimentados. Este capítulo tiene como objetivo documentar parte de esta funcionalidad con ejemplos y casos de uso.

36.9.1. Notación de array

A muchos desarrolladores web experimentados les gusta agrupar elementos de formulario relacionados utilizando notación de array en los nombres de los elementos. Por ejemplo, si tiene dos direcciones que desea capturar, una de envío y otra de facturación, podría tener elementos idénticos; agrupándolos en un array, puede asegurarse de que se capturen por separado. Tome el siguiente formulario como ejemplo:

<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>

En este ejemplo, la dirección de facturación y la de envío contienen algunos campos idénticos, lo que significa que uno sobrescribiría al otro. Podemos resolver esta situación utilizando notación de array:

<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>

En el ejemplo anterior, ahora obtenemos direcciones separadas. En el formulario enviado, ahora tendremos tres elementos: el elemento 'save' para el envío, y luego dos arrays, 'shipping' y 'billing', cada uno con claves para sus distintos elementos.

Zend_Form intenta automatizar este proceso mediante sus subformularios. Por defecto, los subformularios se renderizan utilizando la notación de array mostrada en el listado HTML anterior, con sus correspondientes ids. El nombre del array está basado en el nombre del subformulario, con las claves basadas en los elementos contenidos en el subformulario. Los subformularios pueden anidarse a una profundidad arbitraria, y esto creará arrays anidados que reflejen dicha estructura. Además, las distintas rutinas de validación de Zend_Form respetan la estructura del array, garantizando que su formulario valide correctamente, sin importar la profundidad con la que anide sus subformularios. No es necesario hacer nada para beneficiarse de esto; este comportamiento está habilitado por defecto.

Además, existen mecanismos que le permiten activar la notación de array de forma condicional, así como especificar el array concreto al que pertenece un elemento o colección:

  • Zend_Form::setIsArray($flag): Al establecer el indicador en TRUE, puede indicar que un formulario completo debe tratarse como un array. Por defecto, se utilizará el nombre del formulario como nombre del array, a menos que se haya llamado a setElementsBelongTo(). Si el formulario no tiene un nombre especificado, o si no se ha establecido setElementsBelongTo(), este indicador se ignorará (ya que no hay ningún nombre de array al que puedan pertenecer los elementos).

    Puede determinar si un formulario está siendo tratado como un array utilizando el accesor isArray().

  • Zend_Form::setElementsBelongTo($array): Mediante este método, puede especificar el nombre de un array al que pertenecen todos los elementos del formulario. Puede determinar el nombre utilizando el accesor getElementsBelongTo().

Además, a nivel de elemento, puede especificar que elementos individuales pertenezcan a arrays concretos utilizando el método Zend_Form_Element::setBelongsTo(). Para descubrir cuál es este valor -- ya sea establecido explícitamente o implícitamente a través del formulario -- puede utilizar el accesor getBelongsTo().

36.9.2. Formularios multi-página

Actualmente, los formularios multi-página no están oficialmente soportados en Zend_Form; sin embargo, la mayor parte del soporte necesario para implementarlos está disponible y puede utilizarse con un poco de herramientas adicionales.

La clave para crear un formulario multi-página es utilizar subformularios, pero mostrar solo uno de esos subformularios por página. Esto le permite enviar un único subformulario a la vez y validarlo, pero no procesar el formulario hasta que todos los subformularios estén completos.

Ejemplo 36.13. Ejemplo de formulario de registro

Usemos un formulario de registro como ejemplo. Para nuestros propósitos, queremos capturar el nombre de usuario y la contraseña deseados en la primera página, luego los metadatos del usuario -- nombre de pila, apellido y ubicación -- y finalmente permitirles decidir a qué listas de correo, si es que a alguna, desean suscribirse.

Primero, creemos nuestro propio formulario, y definamos varios subformularios dentro de él:

class My_Form_Registration extends Zend_Form
{
    public function init()
    {
        // Create user sub form: username and 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))
                )
            )),
        ));

        // Create demographics sub form: given name, family name, and
        // 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))
                )
            )),
        ));

        // Create mailing lists sub form
        $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)))
                )
            )),
        ));

        // Attach sub forms to main form
        $this->addSubForms(array(
            'user'  => $user,
            'demog' => $demog,
            'lists' => $lists
        ));
    }
}

Observe que no hay botones de envío, y que no hemos hecho nada con los decoradores de los subformularios -- lo que significa que por defecto se mostrarán como fieldsets. Necesitaremos poder sobrescribir esto al mostrar cada subformulario individual, y añadir botones de envío para poder procesarlos realmente -- lo cual también requerirá propiedades de acción y método. Añadamos algo de andamiaje a nuestra clase para proporcionar esa información:

class My_Form_Registration extends Zend_Form
{
    // ...

    /**
     * Prepare a sub form for display
     *
     * @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;
    }

    /**
     * Add a submit button to an individual sub form
     *
     * @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;
    }

    /**
     * Add action and method to sub form
     *
     * @param  Zend_Form_SubForm $subForm
     * @return My_Form_Registration
     */
    public function addSubFormActions(Zend_Form_SubForm $subForm)
    {
        $subForm->setAction('/registration/process')
                ->setMethod('post');
        return $this;
    }
}

A continuación, necesitamos añadir algo de andamiaje en nuestro controlador de acción, y tener en cuenta varias consideraciones. Primero, debemos asegurarnos de persistir los datos del formulario entre peticiones, para poder determinar cuándo finalizar. Segundo, necesitamos algo de lógica para determinar qué segmentos del formulario ya se han enviado, y qué subformulario mostrar en función de esa información. Utilizaremos Zend_Session_Namespace para persistir los datos, lo que también nos ayudará a responder a la pregunta de qué formulario enviar.

Creemos nuestro controlador, y añadamos un método para obtener una instancia del formulario:

class RegistrationController extends Zend_Controller_Action
{
    protected $_form;

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

Ahora, añadamos algo de funcionalidad para determinar qué formulario mostrar. Básicamente, hasta que el formulario completo se considere válido, necesitamos seguir mostrando segmentos del formulario. Además, probablemente queramos asegurarnos de que estén en un orden particular: user, demog, y luego lists. Podemos determinar qué datos se han enviado comprobando nuestro espacio de nombres de sesión en busca de determinadas claves que representan cada subformulario.

class RegistrationController extends Zend_Controller_Action
{
    // ...

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

    /**
     * Get the session namespace we're using
     *
     * @return Zend_Session_Namespace
     */
    public function getSessionNamespace()
    {
        if (null === $this->_session) {
            $this->_session =
                new Zend_Session_Namespace($this->_namespace);
        }

        return $this->_session;
    }

    /**
     * Get a list of forms already stored in the session
     *
     * @return array
     */
    public function getStoredForms()
    {
        $stored = array();
        foreach ($this->getSessionNamespace() as $key => $value) {
            $stored[] = $key;
        }

        return $stored;
    }

    /**
     * Get list of all subforms available
     *
     * @return array
     */
    public function getPotentialForms()
    {
        return array_keys($this->getForm()->getSubForms());
    }

    /**
     * What sub form was submitted?
     *
     * @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;
    }

    /**
     * Get the next sub form to display
     *
     * @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;
    }
}

Los métodos anteriores nos permiten utilizar notaciones como "$subForm = $this->getCurrentSubForm();" para recuperar el subformulario actual para su validación, o "$next = $this->getNextSubForm();" para obtener el siguiente a mostrar.

Ahora, averigüemos cómo procesar y mostrar los distintos subformularios. Podemos utilizar getCurrentSubForm() para determinar si se ha enviado algún subformulario (los valores de retorno FALSE indican que no se ha mostrado ni enviado ninguno), y getNextSubForm() para recuperar un formulario a mostrar. Luego podemos utilizar el método prepareSubForm() del formulario para asegurarnos de que el formulario esté listo para mostrarse.

Cuando tenemos un envío de formulario, podemos validar el subformulario, y luego comprobar si el formulario completo ya es válido. Para realizar estas tareas, necesitaremos métodos adicionales que se aseguren de que los datos enviados se añadan a la sesión, y de que, al validar el formulario completo, validemos contra todos los segmentos almacenados en la sesión:

class RegistrationController extends Zend_Controller_Action
{
    // ...

    /**
     * Is the sub form valid?
     *
     * @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;
    }

    /**
     * Is the full form valid?
     *
     * @return bool
     */
    public function formIsValid()
    {
        $data = array();
        foreach ($this->getSessionNamespace() as $key => $info) {
            $data[$key] = $info;
        }

        return $this->getForm()->isValid($data);
    }
}

Ahora que ya tenemos el trabajo preliminar hecho, construyamos las acciones de este controlador. Necesitaremos una página de aterrizaje para el formulario, y luego una acción 'process' para procesarlo.

class RegistrationController extends Zend_Controller_Action
{
    // ...

    public function indexAction()
    {
        // Either re-display the current page, or grab the "next"
        // (first) sub form
        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');
        }

        // Valid form!
        // Render information in a verification page
        $this->view->info = $this->getSessionNamespace();
        $this->render('verification');
    }
}

Como notará, el código real para procesar el formulario es relativamente simple. Comprobamos si tenemos un envío de subformulario actual, y si no, volvemos a la página de aterrizaje. Si tenemos un subformulario, intentamos validarlo, volviendo a mostrarlo si falla. Si el subformulario es válido, comprobamos entonces si el formulario es válido, lo que indicaría que hemos terminado; si no, se muestra el siguiente segmento del formulario. Finalmente, mostramos una página de verificación con el contenido de la sesión.

Los scripts de vista son muy simples:

<?php // registration/index.phtml ?>
<h2>Registration</h2>
<?php echo $this->form ?>

<?php // registration/verification.phtml ?>
<h2>Thank you for registering!</h2>
<p>
    Here is the information you provided:
</p>

<?
// Have to do this construct due to how items are stored in session
// namespaces
foreach ($this->info as $info):
    foreach ($info as $form => $data): ?>
<h4><?php echo ucfirst($form) ?>:</h4>
<dl>
    <?php foreach ($data as $key => $value): ?>
    <dt><?php echo ucfirst($key) ?></dt>
    <?php if (is_array($value)):
        foreach ($value as $label => $val): ?>
    <dd><?php echo $val ?></dd>
        <?php endforeach;
       else: ?>
    <dd><?php echo $this->escape($value) ?></dd>
    <?php endif;
    endforeach; ?>
</dl>
<?php endforeach;
endforeach ?>

Las próximas versiones de Zend Framework incluirán componentes para simplificar los formularios multi-página abstrayendo la lógica de sesión y ordenación. Mientras tanto, el ejemplo anterior debería servir como una guía razonable de cómo llevar a cabo esta tarea en su sitio.