1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- EN-Revision: 20115 -->
4 <sect1 id="zend.form.advanced">
5 <title>Uso avanzado de Zend_Form</title>
8 <classname>Zend_Form</classname> tiene una funcional riqueza, muchas de
9 ellas dirigidas a expertos desarroladores. Este capítulo esta dirigido a
10 documentar algunas de las funcionalidades con ejemplos y casos de uso. </para>
12 <sect2 id="zend.form.advanced.arrayNotation">
13 <title>Notación de array</title>
15 <para>A muchos desarroladores web experimentados les gusta agrupar
16 elementos relacionados de formulario usando notación de array en los
17 nombres del elemento. Por ejemplo, si se tienen dos direcciones que
18 se desean capturar, un envío y una dirección de facturación, se
19 pueden tener elementos idénticos; agrupándolos en un array se puede
20 asegurar que son capturados por separado. Nótese el siguiente
21 formulario por ejemplo:</para>
23 <programlisting language="html"><![CDATA[
26 <legend>Shipping Address</legend>
28 <dt><label for="recipient">Ship to:</label></dt>
29 <dd><input name="recipient" type="text" value="" /></dd>
31 <dt><label for="address">Address:</label></dt>
32 <dd><input name="address" type="text" value="" /></dd>
34 <dt><label for="municipality">City:</label></dt>
35 <dd><input name="municipality" type="text" value="" /></dd>
37 <dt><label for="province">State:</label></dt>
38 <dd><input name="province" type="text" value="" /></dd>
40 <dt><label for="postal">Postal Code:</label></dt>
41 <dd><input name="postal" type="text" value="" /></dd>
46 <legend>Billing Address</legend>
48 <dt><label for="payer">Bill To:</label></dt>
49 <dd><input name="payer" type="text" value="" /></dd>
51 <dt><label for="address">Address:</label></dt>
52 <dd><input name="address" type="text" value="" /></dd>
54 <dt><label for="municipality">City:</label></dt>
55 <dd><input name="municipality" type="text" value="" /></dd>
57 <dt><label for="province">State:</label></dt>
58 <dd><input name="province" type="text" value="" /></dd>
60 <dt><label for="postal">Postal Code:</label></dt>
61 <dd><input name="postal" type="text" value="" /></dd>
66 <dt><label for="terms">I agree to the Terms of Service</label></dt>
67 <dd><input name="terms" type="checkbox" value="" /></dd>
70 <dd><input name="save" type="submit" value="Save" /></dd>
75 <para>En este ejemplo, la facturación y la dirección de envío contienen
76 algunos campos idénticos, eso significa que uno puede sobrescribir
77 al otro. Nosotros podemos resolver esta solución usando una notación
80 <programlisting language="html"><![CDATA[
83 <legend>Shipping Address</legend>
85 <dt><label for="shipping-recipient">Ship to:</label></dt>
86 <dd><input name="shipping[recipient]" id="shipping-recipient"
87 type="text" value="" /></dd>
89 <dt><label for="shipping-address">Address:</label></dt>
90 <dd><input name="shipping[address]" id="shipping-address"
91 type="text" value="" /></dd>
93 <dt><label for="shipping-municipality">City:</label></dt>
94 <dd><input name="shipping[municipality]" id="shipping-municipality"
95 type="text" value="" /></dd>
97 <dt><label for="shipping-province">State:</label></dt>
98 <dd><input name="shipping[province]" id="shipping-province"
99 type="text" value="" /></dd>
101 <dt><label for="shipping-postal">Postal Code:</label></dt>
102 <dd><input name="shipping[postal]" id="shipping-postal"
103 type="text" value="" /></dd>
108 <legend>Billing Address</legend>
110 <dt><label for="billing-payer">Bill To:</label></dt>
111 <dd><input name="billing[payer]" id="billing-payer"
112 type="text" value="" /></dd>
114 <dt><label for="billing-address">Address:</label></dt>
115 <dd><input name="billing[address]" id="billing-address"
116 type="text" value="" /></dd>
118 <dt><label for="billing-municipality">City:</label></dt>
119 <dd><input name="billing[municipality]" id="billing-municipality"
120 type="text" value="" /></dd>
122 <dt><label for="billing-province">State:</label></dt>
123 <dd><input name="billing[province]" id="billing-province"
124 type="text" value="" /></dd>
126 <dt><label for="billing-postal">Postal Code:</label></dt>
127 <dd><input name="billing[postal]" id="billing-postal"
128 type="text" value="" /></dd>
133 <dt><label for="terms">I agree to the Terms of Service</label></dt>
134 <dd><input name="terms" type="checkbox" value="" /></dd>
137 <dd><input name="save" type="submit" value="Save" /></dd>
142 <para>En el ejemplo anterior, obtenemos direcciones separadas. En el
143 formulario sometido, ahora tenemos tres elementos, 'guardar'
144 elemento para someterlo, y dos arrays, 'envio' y 'cuenta', cada uno
145 con llaves para los variados elementos.</para>
148 <classname>Zend_Form</classname> intenta automatizar este proceso
149 con los <link linkend="zend.form.forms.subforms"
150 >subformularios</link> . Por defecto, los subformularios son
151 generados usando la notación de array como se muestra en el anterior
152 formulario <acronym>HTML</acronym> listado completo con
153 identificadores. El nombre del array esta basado en el nombre del
154 subformulario, con las llaves basados en los elementos contenidos en
155 el subformulario. Los subformularios pueder ser anidados
156 arbitrariamente, y esto puede crear arrays anidados que reflejan la
157 estructura. Adicionalmente, las validaciones rutinarias en
158 <classname>Zend_Form</classname> respetan la estructura del
159 array, asegurando que sus formularios sean validados correctamente,
160 no importa cuan arbitrariamente anidados esten los subformularios.
161 No se necesita hacer nada para beneficiarse; éste comportamiento
162 esta activo por defecto. </para>
164 <para>Adicionalmente, existen facilidades que le permiten activar
165 condicionalmente la notación de un array, así como también
166 especificar el específico array al cual un elemento o coleccion
172 <methodname>Zend_Form::setIsArray($flag)</methodname> :
173 Definiendo la bandera a <constant>TRUE</constant>, se puede indicar que un
174 formulario entero debería ser tratado como un array. Por
175 defecto, el nombre del formulario será usado como el nombre
176 del array, a no ser que
177 <methodname>setElementsBelongTo()</methodname> haya sido
178 llamado. Si el formulario no tiene un nombre específico, o
179 si <methodname>setElementsBelongTo()</methodname> no ha sido
180 definido, esta bandera será ignorada (como cuando no hay
181 nombre del array al cual los elementos puedan pertenecer). </para>
183 <para> Se deberá determinar si un formulario está siendo tratado
184 como un array usando el accesor
185 <methodname>isArray()</methodname> . </para>
190 <methodname>Zend_Form::setElementsBelongTo($array)</methodname>
191 : Usando este método, se puede especificar el nombre de un
192 array al cual todos los elementos del formulario pertenecen.
193 Se puede determinar el nombre usando el accesor
194 <methodname>getElementsBelongTo()</methodname> . </para>
198 <para> Adicionalmente, a nivel del elemento, se pueden especificar
199 elementos individuales que puedan pertenecer a arrays particulares
201 <methodname>Zend_Form_Element::setBelongsTo()</methodname> .
202 Para descubrir el valor que tiene -- sea o no sea definido
203 explícitamente o implícitamente a través del formulario -- se puede
204 usar el accesor <methodname>getBelongsTo()</methodname> . </para>
207 <sect2 id="zend.form.advanced.multiPage">
208 <title>Formularios Multi-Página</title>
210 <para> Actualmente, los formularios multi-página no están oficialmente
211 soportados en <classname>Zend_Form</classname> ; sin embargo, la
212 mayoría del soporte para implementarlos está disponible y puede ser
213 utilizado con algunos retoques. </para>
215 <para>La clave para crear fomrularios multi-página es utilizar
216 subformularios, pero solo para mostrar un solo subformulario por
217 página. Esto le permite someter un solo subformulario a la vez y
218 validarlo, pero no procesar el formulario hasta que todos los
219 subformularios esten completos.</para>
221 <example id="zend.form.advanced.multiPage.registration">
222 <title>Ejemplo de formulario de registro</title>
223 <para>Vamos a usar un formulario de registro como ejemplo. Para
224 nuestros propósitos, queremos capturar el nombre del usuario y
225 la contraseña en la primera página, después la información del
226 usuario -- nombre, apellido, y ubicación -- y finalmente
227 permitirles decidir qué lista de correo, si desean
230 <para>Primero, vamos a crear nuestro propio formulario, y definir
231 varios subformularios dentro del mismo:</para>
233 <programlisting language="php"><![CDATA[
234 class My_Form_Registration extends Zend_Form
236 public function init()
238 // Crea un subformulario usuario: username y password
239 $user = new Zend_Form_SubForm();
240 $user->addElements(array(
241 new Zend_Form_Element_Text('username', array(
243 'label' => 'Username:',
244 'filters' => array('StringTrim', 'StringToLower'),
245 'validators' => array(
249 array('/^[a-z][a-z0-9]{2,}$/'))
253 new Zend_Form_Element_Password('password', array(
255 'label' => 'Password:',
256 'filters' => array('StringTrim'),
257 'validators' => array(
259 array('StringLength', false, array(6))
264 // Crea un subformulario de datos demográficos : given name, family name, y
266 $demog = new Zend_Form_SubForm();
267 $demog->addElements(array(
268 new Zend_Form_Element_Text('givenName', array(
270 'label' => 'Given (First) Name:',
271 'filters' => array('StringTrim'),
272 'validators' => array(
275 array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
279 new Zend_Form_Element_Text('familyName', array(
281 'label' => 'Family (Last) Name:',
282 'filters' => array('StringTrim'),
283 'validators' => array(
286 array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
290 new Zend_Form_Element_Text('location', array(
292 'label' => 'Your Location:',
293 'filters' => array('StringTrim'),
294 'validators' => array(
295 array('StringLength', false, array(2))
300 // Crea un sub fomulario de correos
301 $listOptions = array(
302 'none' => 'No lists, please',
303 'fw-general' => 'Zend Framework General List',
304 'fw-mvc' => 'Zend Framework MVC List',
305 'fw-auth' => 'Zend Framwork Authentication and ACL List',
306 'fw-services' => 'Zend Framework Web Services List',
308 $lists = new Zend_Form_SubForm();
309 $lists->addElements(array(
310 new Zend_Form_Element_MultiCheckbox('subscriptions', array(
312 'Which lists would you like to subscribe to?',
313 'multiOptions' => $listOptions,
315 'filters' => array('StringTrim'),
316 'validators' => array(
319 array(array_keys($listOptions)))
324 // Adjuntando los subformlarios al formulario principal
325 $this->addSubForms(array(
334 <para>Note que no hay botones de enviar, y que ni hemos hecho nada
335 con los decoradores de subformularios -- lo que significa que
336 por defecto serán desplegados como campos. Necesitaremos hacer
337 algo con ellos mientras desplegamos cada subformulario
338 individualmente, y añadir botones de manera que podamos
339 procesarlos realmente -- el cual requerira las propiedades
340 acción y método. Vamos a añadir algunos andamios a nuestras
341 clases para proveer esa información:</para>
343 <programlisting language="php"><![CDATA[
344 class My_Form_Registration extends Zend_Form
349 * Prepara un subformulario para mostrar
351 * @param string|Zend_Form_SubForm $spec
352 * @return Zend_Form_SubForm
354 public function prepareSubForm($spec)
356 if (is_string($spec)) {
357 $subForm = $this->{$spec};
358 } elseif ($spec instanceof Zend_Form_SubForm) {
361 throw new Exception('Invalid argument passed to ' .
362 __FUNCTION__ . '()');
364 $this->setSubFormDecorators($subForm)
365 ->addSubmitButton($subForm)
366 ->addSubFormActions($subForm);
371 * Add form decorators to an individual sub form
373 * @param Zend_Form_SubForm $subForm
374 * @return My_Form_Registration
376 public function setSubFormDecorators(Zend_Form_SubForm $subForm)
378 $subForm->setDecorators(array(
380 array('HtmlTag', array('tag' => 'dl',
381 'class' => 'zend_form')),
388 * Añade un Boton de envio(submit) a cada subformulario
390 * @param Zend_Form_SubForm $subForm
391 * @return My_Form_Registration
393 public function addSubmitButton(Zend_Form_SubForm $subForm)
395 $subForm->addElement(new Zend_Form_Element_Submit(
398 'label' => 'Save and continue',
407 * Añade el method y el action a cada subformulario
409 * @param Zend_Form_SubForm $subForm
410 * @return My_Form_Registration
412 public function addSubFormActions(Zend_Form_SubForm $subForm)
414 $subForm->setAction('/registration/process')
421 <para> Siguiente, necesitamos añadir andamios a nuestro action
422 controller, y tener varias consideraciones. Primero, necesitamos
423 asegurar que persiste la información del formulario entre los
424 requerimientos, de esa manera determinar cuándo terminar.
425 Segundo, necesitamos alguna lógica para determinar qué segmentos
426 del formulario han sido sometidos, y qué subformulario mostrar
427 de acuerdo a la información. Usaremos
428 <classname>Zend_Session_Namespace</classname> para persistir
429 la información, el cual nos ayudará a responder la pregunta de
430 qué formulario someter. </para>
432 <para>Vamos a crear nuestro controlador, y añadir un método para
433 recuperar un formulario instanciado:</para>
435 <programlisting language="php"><![CDATA[
436 class RegistrationController extends Zend_Controller_Action
440 public function getForm()
442 if (null === $this->_form) {
443 $this->_form = new My_Form_Registration();
450 <para>Ahora, vamos a añadir algunas funcionalidades para determinar
451 qué formulario mostrar. Básicamente, hasta que el formulario
452 entero sea considerado válido, necesitamos continuar mostrando
453 segmentos de formulario. Adicionalmente, queremos asegurar que
454 están en un orden particular: usuario, demog, y después las
455 listas. Podemos determinar qué información ha sido sometida
456 verificando nuestro session namespace para claves particulares
457 representando cada subformulario.</para>
459 <programlisting language="php"><![CDATA[
460 class RegistrationController extends Zend_Controller_Action
464 protected $_namespace = 'RegistrationController';
468 * Obtiene el namespace de la sesión que estamos usando
470 * @return Zend_Session_Namespace
472 public function getSessionNamespace()
474 if (null === $this->_session) {
476 new Zend_Session_Namespace($this->_namespace);
479 return $this->_session;
483 * Obtiene la lista de Formularios que ya están almacenados en la sesión
487 public function getStoredForms()
490 foreach ($this->getSessionNamespace() as $key => $value) {
498 * Obtiene la lista de todos los subformularios disponibles
502 public function getPotentialForms()
504 return array_keys($this->getForm()->getSubForms());
508 * ¿Qué subformulario se envio?
510 * @return false|Zend_Form_SubForm
512 public function getCurrentSubForm()
514 $request = $this->getRequest();
515 if (!$request->isPost()) {
519 foreach ($this->getPotentialForms() as $name) {
520 if ($data = $request->getPost($name, false)) {
521 if (is_array($data)) {
522 return $this->getForm()->getSubForm($name);
532 * Obtiene el siguiente subformulario para mostrarlo
534 * @return Zend_Form_SubForm|false
536 public function getNextSubForm()
538 $storedForms = $this->getStoredForms();
539 $potentialForms = $this->getPotentialForms();
541 foreach ($potentialForms as $name) {
542 if (!in_array($name, $storedForms)) {
543 return $this->getForm()->getSubForm($name);
552 <para> El método de arriba nos permite usar notaciones tal como "
554 $this->getCurrentSubForm();</command> " recuperar el
555 actual subformulario para la validación, o " <command>$next =
556 $this->getNextSubForm();</command> " obtener el
557 siguiente para mostrar. </para>
559 <para> Ahora, vamos a encontrar la manera para procesar y mostrar
560 varios subformularios. Podemos usar
561 <methodname>getCurrentSubForm()</methodname> para determinar
562 si algún subformulario ha sido sometido (los valores de retorno
563 <constant>FALSE</constant> indican que ninguno ha sido desplegado o sometido), y
564 <methodname>getNextSubForm()</methodname> recupera el
565 formulario que mostrar. Podemos entonces usar el método del
566 formulario <methodname>prepareSubForm()</methodname> para
567 asegurar que el formulario está listo para mostrar. </para>
569 <para>Cuando tenemos un formulario sometido, podemos validar el
570 subformulario, y luego verificar si el formulario entero es
571 válido ahora. Para hacer esas tareas, necesitamos métodos
572 adicionales que aseguren que la información sometida es añadida
573 a la sesión, y que cuando validamos el formulario entero,
574 nosotros validamos contra todos los segmentos de la
577 <programlisting language="php"><![CDATA[
578 class RegistrationController extends Zend_Controller_Action
583 * ¿Es válido el subformulario?
585 * @param Zend_Form_SubForm $subForm
589 public function subFormIsValid(Zend_Form_SubForm $subForm,
592 $name = $subForm->getName();
593 if ($subForm->isValid($data)) {
594 $this->getSessionNamespace()->$name = $subForm->getValues();
602 * ¿Es válido todo el formulario?
606 public function formIsValid()
609 foreach ($this->getSessionNamespace() as $key => $info) {
613 return $this->getForm()->isValid($data);
618 <para>Ahora que tenemos el trabajo preparado, vamos a construir las
619 acciones para este controlador. Necesitaremos una página de
620 destino para el formulario, y luego una acción 'process' para
621 procesar el formulario.</para>
623 <programlisting language="php"><![CDATA[
624 class RegistrationController extends Zend_Controller_Action
628 public function indexAction()
630 // volver a mostrar la página actual, o mostrar el "siguiente"
631 // (primer) subformulario
632 if (!$form = $this->getCurrentSubForm()) {
633 $form = $this->getNextSubForm();
635 $this->view->form = $this->getForm()->prepareSubForm($form);
638 public function processAction()
640 if (!$form = $this->getCurrentSubForm()) {
641 return $this->_forward('index');
644 if (!$this->subFormIsValid($form,
645 $this->getRequest()->getPost())) {
646 $this->view->form = $this->getForm()->prepareSubForm($form);
647 return $this->render('index');
650 if (!$this->formIsValid()) {
651 $form = $this->getNextSubForm();
652 $this->view->form = $this->getForm()->prepareSubForm($form);
653 return $this->render('index');
656 // Formulario Válido!
657 // Render information in a verification page
658 $this->view->info = $this->getSessionNamespace();
659 $this->render('verification');
664 <para>Como se ha notado, el código actual para procesar el
665 formulario es relativamente simple. Verificamos si tenemos un
666 subformulario actual sometido y si no, retornamos a la página de
667 destino. Si tenemos un subformulario, intentaremos validarlo,
668 volviéndolo a mostrar si tiene fallos. Si el subformulario es
669 válido, entonces verificaremos si el formulario es válido, lo
670 que debería indicar que hemos terminado; si no, mostraremos el
671 siguiente segmento del formulario. Finalmente, mostraremos una
672 página de verificación con el contenido de la sesión.</para>
674 <para>Los scripts de vista son muy simples:</para>
676 <programlisting language="php"><![CDATA[
677 <?php // registration/index.phtml ?>
679 <?php echo $this->form ?>
681 <?php // registration/verification.phtml ?>
682 <h2>Gracias por Registrarse!</h2>
684 Aquí está la información que nos ha proporcionado:
688 // Tienen que construir esto con los items que estan almacenados en los namespaces
690 foreach ($this->info as $info):
691 foreach ($info as $form => $data): ?>
692 <h4><?php echo ucfirst($form) ?>:</h4>
694 <?php foreach ($data as $key => $value): ?>
695 <dt><?php echo ucfirst($key) ?></dt>
696 <?php if (is_array($value)):
697 foreach ($value as $label => $val): ?>
698 <dd><?php echo $val ?></dd>
701 <dd><?php echo $this->escape($value) ?></dd>
709 <para>Próximas novedades de Zend Framework incluirán componentes
710 para hacer formularios multi páginas mas simples, abstrayendo la
711 sesión y la lógica de orden. Mientras tanto, el ejemplo de
712 arriba debería servir como guia razonable para alcanzar esta
713 tarea en su web.</para>