[MANUAL] English:
[zend.git] / documentation / manual / en / module_specs / Zend_Form-Advanced.xml
blob7f3082ff9bef6407cf2abee044028f8b4f7f08ce
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- Reviewed: no -->
3 <sect1 id="zend.form.advanced">
4     <title>Advanced Zend_Form Usage</title>
6     <para>
7         <classname>Zend_Form</classname> has a wealth of functionality, much of it aimed
8         at experienced developers. This chapter aims to document some of this
9         functionality with examples and use cases.
10     </para>
12     <sect2 id="zend.form.advanced.arrayNotation">
13         <title>Array Notation</title>
15         <para>
16             Many experienced web developers like to group related form elements
17             using array notation in the element names. For example, if you have
18             two addresses you wish to capture, a shipping and a billing address,
19             you may have identical elements; by grouping them in an array, you
20             can ensure they are captured separately. Take the following form,
21             for example:
22         </para>
24         <programlisting language="html"><![CDATA[
25 <form>
26     <fieldset>
27         <legend>Shipping Address</legend>
28         <dl>
29             <dt><label for="recipient">Ship to:</label></dt>
30             <dd><input name="recipient" type="text" value="" /></dd>
32             <dt><label for="address">Address:</label></dt>
33             <dd><input name="address" type="text" value="" /></dd>
35             <dt><label for="municipality">City:</label></dt>
36             <dd><input name="municipality" type="text" value="" /></dd>
38             <dt><label for="province">State:</label></dt>
39             <dd><input name="province" type="text" value="" /></dd>
41             <dt><label for="postal">Postal Code:</label></dt>
42             <dd><input name="postal" type="text" value="" /></dd>
43         </dl>
44     </fieldset>
46     <fieldset>
47         <legend>Billing Address</legend>
48         <dl>
49             <dt><label for="payer">Bill To:</label></dt>
50             <dd><input name="payer" type="text" value="" /></dd>
52             <dt><label for="address">Address:</label></dt>
53             <dd><input name="address" type="text" value="" /></dd>
55             <dt><label for="municipality">City:</label></dt>
56             <dd><input name="municipality" type="text" value="" /></dd>
58             <dt><label for="province">State:</label></dt>
59             <dd><input name="province" type="text" value="" /></dd>
61             <dt><label for="postal">Postal Code:</label></dt>
62             <dd><input name="postal" type="text" value="" /></dd>
63         </dl>
64     </fieldset>
66     <dl>
67         <dt><label for="terms">I agree to the Terms of Service</label></dt>
68         <dd><input name="terms" type="checkbox" value="" /></dd>
70         <dt></dt>
71         <dd><input name="save" type="submit" value="Save" /></dd>
72     </dl>
73 </form>
74 ]]></programlisting>
76         <para>
77             In this example, the billing and shipping address contain some
78             identical fields, which means one would overwrite the other. We can
79             solve this solution using array notation:
80         </para>
82         <programlisting language="html"><![CDATA[
83 <form>
84     <fieldset>
85         <legend>Shipping Address</legend>
86         <dl>
87             <dt><label for="shipping-recipient">Ship to:</label></dt>
88             <dd><input name="shipping[recipient]" id="shipping-recipient"
89                 type="text" value="" /></dd>
91             <dt><label for="shipping-address">Address:</label></dt>
92             <dd><input name="shipping[address]" id="shipping-address"
93                 type="text" value="" /></dd>
95             <dt><label for="shipping-municipality">City:</label></dt>
96             <dd><input name="shipping[municipality]" id="shipping-municipality"
97                 type="text" value="" /></dd>
99             <dt><label for="shipping-province">State:</label></dt>
100             <dd><input name="shipping[province]" id="shipping-province"
101                 type="text" value="" /></dd>
103             <dt><label for="shipping-postal">Postal Code:</label></dt>
104             <dd><input name="shipping[postal]" id="shipping-postal"
105                 type="text" value="" /></dd>
106         </dl>
107     </fieldset>
109     <fieldset>
110         <legend>Billing Address</legend>
111         <dl>
112             <dt><label for="billing-payer">Bill To:</label></dt>
113             <dd><input name="billing[payer]" id="billing-payer"
114                 type="text" value="" /></dd>
116             <dt><label for="billing-address">Address:</label></dt>
117             <dd><input name="billing[address]" id="billing-address"
118                 type="text" value="" /></dd>
120             <dt><label for="billing-municipality">City:</label></dt>
121             <dd><input name="billing[municipality]" id="billing-municipality"
122                 type="text" value="" /></dd>
124             <dt><label for="billing-province">State:</label></dt>
125             <dd><input name="billing[province]" id="billing-province"
126                 type="text" value="" /></dd>
128             <dt><label for="billing-postal">Postal Code:</label></dt>
129             <dd><input name="billing[postal]" id="billing-postal"
130                 type="text" value="" /></dd>
131         </dl>
132     </fieldset>
134     <dl>
135         <dt><label for="terms">I agree to the Terms of Service</label></dt>
136         <dd><input name="terms" type="checkbox" value="" /></dd>
138         <dt></dt>
139         <dd><input name="save" type="submit" value="Save" /></dd>
140     </dl>
141 </form>
142 ]]></programlisting>
144         <para>
145             In the above sample, we now get separate addresses. In the submitted
146             form, we'll now have three elements, the 'save' element for the
147             submit, and then two arrays, 'shipping' and 'billing', each with
148             keys for their various elements.
149         </para>
151         <para>
152             <classname>Zend_Form</classname> attempts to automate this process with its
153             <link linkend="zend.form.forms.subforms">sub forms</link>. By
154             default, sub forms render using the array notation as shown in the
155             previous <acronym>HTML</acronym> form listing, complete with ids. The array name is
156             based on the sub form name, with the keys based on the elements
157             contained in the sub form. Sub forms may be nested arbitrarily deep,
158             and this will create nested arrays to reflect the structure.
159             Additionally, the various validation routines in
160             <classname>Zend_Form</classname> honor the array structure, ensuring that your
161             form validates correctly, no matter how arbitrarily deep you nest
162             your sub forms. You need do nothing to benefit from this; this
163             behaviour is enabled by default.
164         </para>
166         <para>
167             Additionally, there are facilities that allow you to turn on array
168             notation conditionally, as well as specify the specific array to
169             which an element or collection belongs:
170         </para>
172         <itemizedlist>
173             <listitem>
174                 <para>
175                     <methodname>Zend_Form::setIsArray($flag)</methodname>: By setting the
176                     flag <constant>TRUE</constant>, you can indicate that an entire form should be
177                     treated as an array. By default, the form's name will be
178                     used as the name of the array, unless
179                     <methodname>setElementsBelongTo()</methodname> has been called. If the
180                     form has no specified name, or if
181                     <methodname>setElementsBelongTo()</methodname> has not been set, this
182                     flag will be ignored (as there is no array name to which
183                     the elements may belong).
184                 </para>
186                 <para>
187                     You may determine if a form is being treated as an array
188                     using the <methodname>isArray()</methodname> accessor.
189                 </para>
190             </listitem>
192             <listitem>
193                 <para>
194                     <methodname>Zend_Form::setElementsBelongTo($array)</methodname>:
195                     Using this method, you can specify the name of an array to
196                     which all elements of the form belong. You can determine the
197                     name using the <methodname>getElementsBelongTo()</methodname> accessor.
198                 </para>
199             </listitem>
200         </itemizedlist>
202         <para>
203             Additionally, on the element level, you can specify individual
204             elements may belong to particular arrays using
205             <methodname>Zend_Form_Element::setBelongsTo()</methodname> method.
206             To discover what this value is -- whether set explicitly or
207             implicitly via the form -- you may use the
208             <methodname>getBelongsTo()</methodname> accessor.
209         </para>
210     </sect2>
212     <sect2 id="zend.form.advanced.multiPage">
213         <title>Multi-Page Forms</title>
215         <para>
216             Currently, Multi-Page forms are not officially supported in
217             <classname>Zend_Form</classname>; however, most support for implementing them
218             is available and can be utilized with a little extra tooling.
219         </para>
221         <para>
222             The key to creating a multi-page form is to utilize sub forms, but
223             to display only one such sub form per page. This allows you to
224             submit a single sub form at a time and validate it, but not process
225             the form until all sub forms are complete.
226         </para>
228         <example id="zend.form.advanced.multiPage.registration">
229             <title>Registration Form Example</title>
231             <para>
232                 Let's use a registration form as an example. For our purposes,
233                 we want to capture the desired username and password on the
234                 first page, then the user's metadata -- given name, family name,
235                 and location -- and finally allow them to decide what mailing
236                 lists, if any, they wish to subscribe to.
237             </para>
239             <para>
240                 First, let's create our own form, and define several sub forms
241                 within it:
242             </para>
244             <programlisting language="php"><![CDATA[
245 class My_Form_Registration extends Zend_Form
247     public function init()
248     {
249         // Create user sub form: username and password
250         $user = new Zend_Form_SubForm();
251         $user->addElements(array(
252             new Zend_Form_Element_Text('username', array(
253                 'required'   => true,
254                 'label'      => 'Username:',
255                 'filters'    => array('StringTrim', 'StringToLower'),
256                 'validators' => array(
257                     'Alnum',
258                     array('Regex',
259                           false,
260                           array('/^[a-z][a-z0-9]{2,}$/'))
261                 )
262             )),
264             new Zend_Form_Element_Password('password', array(
265                 'required'   => true,
266                 'label'      => 'Password:',
267                 'filters'    => array('StringTrim'),
268                 'validators' => array(
269                     'NotEmpty',
270                     array('StringLength', false, array(6))
271                 )
272             )),
273         ));
275         // Create demographics sub form: given name, family name, and
276         // location
277         $demog = new Zend_Form_SubForm();
278         $demog->addElements(array(
279             new Zend_Form_Element_Text('givenName', array(
280                 'required'   => true,
281                 'label'      => 'Given (First) Name:',
282                 'filters'    => array('StringTrim'),
283                 'validators' => array(
284                     array('Regex',
285                           false,
286                           array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
287                 )
288             )),
290             new Zend_Form_Element_Text('familyName', array(
291                 'required'   => true,
292                 'label'      => 'Family (Last) Name:',
293                 'filters'    => array('StringTrim'),
294                 'validators' => array(
295                     array('Regex',
296                           false,
297                           array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
298                 )
299             )),
301             new Zend_Form_Element_Text('location', array(
302                 'required'   => true,
303                 'label'      => 'Your Location:',
304                 'filters'    => array('StringTrim'),
305                 'validators' => array(
306                     array('StringLength', false, array(2))
307                 )
308             )),
309         ));
311         // Create mailing lists sub form
312         $listOptions = array(
313             'none'        => 'No lists, please',
314             'fw-general'  => 'Zend Framework General List',
315             'fw-mvc'      => 'Zend Framework MVC List',
316             'fw-auth'     => 'Zend Framwork Authentication and ACL List',
317             'fw-services' => 'Zend Framework Web Services List',
318         );
319         $lists = new Zend_Form_SubForm();
320         $lists->addElements(array(
321             new Zend_Form_Element_MultiCheckbox('subscriptions', array(
322                 'label'        =>
323                     'Which lists would you like to subscribe to?',
324                 'multiOptions' => $listOptions,
325                 'required'     => true,
326                 'filters'      => array('StringTrim'),
327                 'validators'   => array(
328                     array('InArray',
329                           false,
330                           array(array_keys($listOptions)))
331                 )
332             )),
333         ));
335         // Attach sub forms to main form
336         $this->addSubForms(array(
337             'user'  => $user,
338             'demog' => $demog,
339             'lists' => $lists
340         ));
341     }
343 ]]></programlisting>
345             <para>
346                 Note that there are no submit buttons, and that we have done
347                 nothing with the sub form decorators -- which means that by
348                 default they will be displayed as fieldsets. We will need to be
349                 able to override these as we display each individual sub form,
350                 and add in submit buttons so we can actually process them --
351                 which will also require action and method properties. Let's add
352                 some scaffolding to our class to provide that information:
353             </para>
355             <programlisting language="php"><![CDATA[
356 class My_Form_Registration extends Zend_Form
358     // ...
360     /**
361      * Prepare a sub form for display
362      *
363      * @param  string|Zend_Form_SubForm $spec
364      * @return Zend_Form_SubForm
365      */
366     public function prepareSubForm($spec)
367     {
368         if (is_string($spec)) {
369             $subForm = $this->{$spec};
370         } elseif ($spec instanceof Zend_Form_SubForm) {
371             $subForm = $spec;
372         } else {
373             throw new Exception('Invalid argument passed to ' .
374                                 __FUNCTION__ . '()');
375         }
376         $this->setSubFormDecorators($subForm)
377              ->addSubmitButton($subForm)
378              ->addSubFormActions($subForm);
379         return $subForm;
380     }
382     /**
383      * Add form decorators to an individual sub form
384      *
385      * @param  Zend_Form_SubForm $subForm
386      * @return My_Form_Registration
387      */
388     public function setSubFormDecorators(Zend_Form_SubForm $subForm)
389     {
390         $subForm->setDecorators(array(
391             'FormElements',
392             array('HtmlTag', array('tag' => 'dl',
393                                    'class' => 'zend_form')),
394             'Form',
395         ));
396         return $this;
397     }
399     /**
400      * Add a submit button to an individual sub form
401      *
402      * @param  Zend_Form_SubForm $subForm
403      * @return My_Form_Registration
404      */
405     public function addSubmitButton(Zend_Form_SubForm $subForm)
406     {
407         $subForm->addElement(new Zend_Form_Element_Submit(
408             'save',
409             array(
410                 'label'    => 'Save and continue',
411                 'required' => false,
412                 'ignore'   => true,
413             )
414         ));
415         return $this;
416     }
418     /**
419      * Add action and method to sub form
420      *
421      * @param  Zend_Form_SubForm $subForm
422      * @return My_Form_Registration
423      */
424     public function addSubFormActions(Zend_Form_SubForm $subForm)
425     {
426         $subForm->setAction('/registration/process')
427                 ->setMethod('post');
428         return $this;
429     }
431 ]]></programlisting>
433             <para>
434                 Next, we need to add some scaffolding in our action controller,
435                 and have several considerations. First, we need to make sure we
436                 persist form data between requests, so that we can determine
437                 when to quit. Second, we need some logic to determine what form
438                 segments have already been submitted, and what sub form to
439                 display based on that information. We'll use
440                 <classname>Zend_Session_Namespace</classname> to persist data, which will
441                 also help us answer the question of which form to submit.
442             </para>
444             <para>
445                 Let's create our controller, and add a method for retrieving a
446                 form instance:
447             </para>
449             <programlisting language="php"><![CDATA[
450 class RegistrationController extends Zend_Controller_Action
452     protected $_form;
454     public function getForm()
455     {
456         if (null === $this->_form) {
457             $this->_form = new My_Form_Registration();
458         }
459         return $this->_form;
460     }
462 ]]></programlisting>
464             <para>
465                 Now, let's add some functionality for determining which form to
466                 display. Basically, until the entire form is considered valid,
467                 we need to continue displaying form segments. Additionally, we
468                 likely want to make sure they're in a particular order: user,
469                 demog, and then lists. We can determine what data has been
470                 submitted by checking our session namespace for particular keys
471                 representing each subform.
472             </para>
474             <programlisting language="php"><![CDATA[
475 class RegistrationController extends Zend_Controller_Action
477     // ...
479     protected $_namespace = 'RegistrationController';
480     protected $_session;
482     /**
483      * Get the session namespace we're using
484      *
485      * @return Zend_Session_Namespace
486      */
487     public function getSessionNamespace()
488     {
489         if (null === $this->_session) {
490             $this->_session =
491                 new Zend_Session_Namespace($this->_namespace);
492         }
494         return $this->_session;
495     }
497     /**
498      * Get a list of forms already stored in the session
499      *
500      * @return array
501      */
502     public function getStoredForms()
503     {
504         $stored = array();
505         foreach ($this->getSessionNamespace() as $key => $value) {
506             $stored[] = $key;
507         }
509         return $stored;
510     }
512     /**
513      * Get list of all subforms available
514      *
515      * @return array
516      */
517     public function getPotentialForms()
518     {
519         return array_keys($this->getForm()->getSubForms());
520     }
522     /**
523      * What sub form was submitted?
524      *
525      * @return false|Zend_Form_SubForm
526      */
527     public function getCurrentSubForm()
528     {
529         $request = $this->getRequest();
530         if (!$request->isPost()) {
531             return false;
532         }
534         foreach ($this->getPotentialForms() as $name) {
535             if ($data = $request->getPost($name, false)) {
536                 if (is_array($data)) {
537                     return $this->getForm()->getSubForm($name);
538                     break;
539                 }
540             }
541         }
543         return false;
544     }
546     /**
547      * Get the next sub form to display
548      *
549      * @return Zend_Form_SubForm|false
550      */
551     public function getNextSubForm()
552     {
553         $storedForms    = $this->getStoredForms();
554         $potentialForms = $this->getPotentialForms();
556         foreach ($potentialForms as $name) {
557             if (!in_array($name, $storedForms)) {
558                 return $this->getForm()->getSubForm($name);
559             }
560         }
562         return false;
563     }
565 ]]></programlisting>
567             <para>
568                 The above methods allow us to use notations such as "<command>$subForm =
569                     $this-&gt;getCurrentSubForm();</command>" to retrieve the current
570                 sub form for validation, or "<command>$next =
571                     $this-&gt;getNextSubForm();</command>" to get the next one to
572                 display.
573             </para>
575             <para>
576                 Now, let's figure out how to process and display the various sub
577                 forms. We can use <methodname>getCurrentSubForm()</methodname> to determine
578                 if any sub forms have been submitted (<constant>FALSE</constant> return values
579                 indicate none have been displayed or submitted), and
580                 <methodname>getNextSubForm()</methodname> to retrieve a form to display. We
581                 can then use the form's <methodname>prepareSubForm()</methodname> method to
582                 ensure the form is ready for display.
583             </para>
585             <para>
586                 When we have a form submission, we can validate the sub form,
587                 and then check to see if the entire form is now valid. To do
588                 these tasks, we'll need additional methods that ensure that
589                 submitted data is added to the session, and that when validating
590                 the form entire, we validate against all segments from the
591                 session:
592             </para>
594             <programlisting language="php"><![CDATA[
595 class RegistrationController extends Zend_Controller_Action
597     // ...
599     /**
600      * Is the sub form valid?
601      *
602      * @param  Zend_Form_SubForm $subForm
603      * @param  array $data
604      * @return bool
605      */
606     public function subFormIsValid(Zend_Form_SubForm $subForm,
607                                    array $data)
608     {
609         $name = $subForm->getName();
610         if ($subForm->isValid($data)) {
611             $this->getSessionNamespace()->$name = $subForm->getValues();
612             return true;
613         }
615         return false;
616     }
618     /**
619      * Is the full form valid?
620      *
621      * @return bool
622      */
623     public function formIsValid()
624     {
625         $data = array();
626         foreach ($this->getSessionNamespace() as $key => $info) {
627             $data[$key] = $info;
628         }
630         return $this->getForm()->isValid($data);
631     }
633 ]]></programlisting>
635             <para>
636                 Now that we have the legwork out of the way, let's build the
637                 actions for this controller. We'll need a landing page for the
638                 form, and then a 'process' action for processing the form.
639             </para>
641             <programlisting language="php"><![CDATA[
642 class RegistrationController extends Zend_Controller_Action
644     // ...
646     public function indexAction()
647     {
648         // Either re-display the current page, or grab the "next"
649         // (first) sub form
650         if (!$form = $this->getCurrentSubForm()) {
651             $form = $this->getNextSubForm();
652         }
653         $this->view->form = $this->getForm()->prepareSubForm($form);
654     }
656     public function processAction()
657     {
658         if (!$form = $this->getCurrentSubForm()) {
659             return $this->_forward('index');
660         }
662         if (!$this->subFormIsValid($form,
663                                    $this->getRequest()->getPost())) {
664             $this->view->form = $this->getForm()->prepareSubForm($form);
665             return $this->render('index');
666         }
668         if (!$this->formIsValid()) {
669             $form = $this->getNextSubForm();
670             $this->view->form = $this->getForm()->prepareSubForm($form);
671             return $this->render('index');
672         }
674         // Valid form!
675         // Render information in a verification page
676         $this->view->info = $this->getSessionNamespace();
677         $this->render('verification');
678     }
680 ]]></programlisting>
682             <para>
683                 As you'll notice, the actual code for processing the form is
684                 relatively simple. We check to see if we have a current sub form
685                 submission, and if not, we go back to the landing page. If we do
686                 have a sub form, we attempt to validate it, redisplaying it if
687                 it fails. If the sub form is valid, we then check to see if the
688                 form is valid, which would indicate we're done; if not, we
689                 display the next form segment. Finally, we display a
690                 verification page with the contents of the session.
691             </para>
693             <para>
694                 The view scripts are very simple:
695             </para>
697             <programlisting language="php"><![CDATA[
698 <?php // registration/index.phtml ?>
699 <h2>Registration</h2>
700 <?php echo $this->form ?>
702 <?php // registration/verification.phtml ?>
703 <h2>Thank you for registering!</h2>
705     Here is the information you provided:
706 </p>
709 // Have to do this construct due to how items are stored in session
710 // namespaces
711 foreach ($this->info as $info):
712     foreach ($info as $form => $data): ?>
713 <h4><?php echo ucfirst($form) ?>:</h4>
714 <dl>
715     <?php foreach ($data as $key => $value): ?>
716     <dt><?php echo ucfirst($key) ?></dt>
717     <?php if (is_array($value)):
718         foreach ($value as $label => $val): ?>
719     <dd><?php echo $val ?></dd>
720         <?php endforeach;
721        else: ?>
722     <dd><?php echo $this->escape($value) ?></dd>
723     <?php endif;
724     endforeach; ?>
725 </dl>
726 <?php endforeach;
727 endforeach ?>
728 ]]></programlisting>
730             <para>
731                 Upcoming releases of Zend Framework will include components to
732                 make multi page forms simpler by abstracting the session and
733                 ordering logic. In the meantime, the above example should serve
734                 as a reasonable guideline on how to accomplish this task for
735                 your site.
736             </para>
737         </example>
738     </sect2>
739 </sect1>
740 <!--
741 vim:se ts=4 sw=4 et: