| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742 |
- <?xml version="1.0" encoding="UTF-8"?>
- <!-- Reviewed: no -->
- <sect1 id="zend.form.advanced">
- <title>Advanced Zend_Form Usage</title>
- <para>
- <classname>Zend_Form</classname> has a wealth of functionality, much of it aimed
- at experienced developers. This chapter aims to document some of this
- functionality with examples and use cases.
- </para>
- <sect2 id="zend.form.advanced.arrayNotation">
- <title>Array Notation</title>
- <para>
- Many experienced web developers like to group related form elements
- using array notation in the element names. For example, if you have
- two addresses you wish to capture, a shipping and a billing address,
- you may have identical elements; by grouping them in an array, you
- can ensure they are captured separately. Take the following form,
- for example:
- </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>
- In this example, the billing and shipping address contain some
- identical fields, which means one would overwrite the other. We can
- solve this solution using array notation:
- </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>
- In the above sample, we now get separate addresses. In the submitted
- form, we'll now have three elements, the 'save' element for the
- submit, and then two arrays, 'shipping' and 'billing', each with
- keys for their various elements.
- </para>
- <para>
- <classname>Zend_Form</classname> attempts to automate this process with its
- <link linkend="zend.form.forms.subforms">sub forms</link>. By
- default, sub forms render using the array notation as shown in the
- previous <acronym>HTML</acronym> form listing, complete with ids. The array name is
- based on the sub form name, with the keys based on the elements
- contained in the sub form. Sub forms may be nested arbitrarily deep,
- and this will create nested arrays to reflect the structure.
- Additionally, the various validation routines in
- <classname>Zend_Form</classname> honor the array structure, ensuring that your
- form validates correctly, no matter how arbitrarily deep you nest
- your sub forms. You need do nothing to benefit from this; this
- behaviour is enabled by default.
- </para>
- <para>
- Additionally, there are facilities that allow you to turn on array
- notation conditionally, as well as specify the specific array to
- which an element or collection belongs:
- </para>
- <itemizedlist>
- <listitem>
- <para>
- <methodname>Zend_Form::setIsArray($flag)</methodname>: By setting the
- flag <constant>TRUE</constant>, you can indicate that an entire form should be
- treated as an array. By default, the form's name will be
- used as the name of the array, unless
- <methodname>setElementsBelongTo()</methodname> has been called. If the
- form has no specified name, or if
- <methodname>setElementsBelongTo()</methodname> has not been set, this
- flag will be ignored (as there is no array name to which
- the elements may belong).
- </para>
- <para>
- You may determine if a form is being treated as an array
- using the <methodname>isArray()</methodname> accessor.
- </para>
- </listitem>
- <listitem>
- <para>
- <methodname>Zend_Form::setElementsBelongTo($array)</methodname>:
- Using this method, you can specify the name of an array to
- which all elements of the form belong. You can determine the
- name using the <methodname>getElementsBelongTo()</methodname> accessor.
- </para>
- </listitem>
- </itemizedlist>
- <para>
- Additionally, on the element level, you can specify individual
- elements may belong to particular arrays using
- <methodname>Zend_Form_Element::setBelongsTo()</methodname> method.
- To discover what this value is -- whether set explicitly or
- implicitly via the form -- you may use the
- <methodname>getBelongsTo()</methodname> accessor.
- </para>
- </sect2>
- <sect2 id="zend.form.advanced.multiPage">
- <title>Multi-Page Forms</title>
- <para>
- Currently, Multi-Page forms are not officially supported in
- <classname>Zend_Form</classname>; however, most support for implementing them
- is available and can be utilized with a little extra tooling.
- </para>
- <para>
- The key to creating a multi-page form is to utilize sub forms, but
- to display only one such sub form per page. This allows you to
- submit a single sub form at a time and validate it, but not process
- the form until all sub forms are complete.
- </para>
- <example id="zend.form.advanced.multiPage.registration">
- <title>Registration Form Example</title>
- <para>
- Let's use a registration form as an example. For our purposes,
- we want to capture the desired username and password on the
- first page, then the user's metadata -- given name, family name,
- and location -- and finally allow them to decide what mailing
- lists, if any, they wish to subscribe to.
- </para>
- <para>
- First, let's create our own form, and define several sub forms
- within it:
- </para>
- <programlisting language="php"><![CDATA[
- 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
- ));
- }
- }
- ]]></programlisting>
- <para>
- Note that there are no submit buttons, and that we have done
- nothing with the sub form decorators -- which means that by
- default they will be displayed as fieldsets. We will need to be
- able to override these as we display each individual sub form,
- and add in submit buttons so we can actually process them --
- which will also require action and method properties. Let's add
- some scaffolding to our class to provide that information:
- </para>
- <programlisting language="php"><![CDATA[
- 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;
- }
- }
- ]]></programlisting>
- <para>
- Next, we need to add some scaffolding in our action controller,
- and have several considerations. First, we need to make sure we
- persist form data between requests, so that we can determine
- when to quit. Second, we need some logic to determine what form
- segments have already been submitted, and what sub form to
- display based on that information. We'll use
- <classname>Zend_Session_Namespace</classname> to persist data, which will
- also help us answer the question of which form to submit.
- </para>
- <para>
- Let's create our controller, and add a method for retrieving a
- form instance:
- </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>
- Now, let's add some functionality for determining which form to
- display. Basically, until the entire form is considered valid,
- we need to continue displaying form segments. Additionally, we
- likely want to make sure they're in a particular order: user,
- demog, and then lists. We can determine what data has been
- submitted by checking our session namespace for particular keys
- representing each subform.
- </para>
- <programlisting language="php"><![CDATA[
- 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;
- }
- }
- ]]></programlisting>
- <para>
- The above methods allow us to use notations such as "<command>$subForm =
- $this->getCurrentSubForm();</command>" to retrieve the current
- sub form for validation, or "<command>$next =
- $this->getNextSubForm();</command>" to get the next one to
- display.
- </para>
- <para>
- Now, let's figure out how to process and display the various sub
- forms. We can use <methodname>getCurrentSubForm()</methodname> to determine
- if any sub forms have been submitted (<constant>FALSE</constant> return values
- indicate none have been displayed or submitted), and
- <methodname>getNextSubForm()</methodname> to retrieve a form to display. We
- can then use the form's <methodname>prepareSubForm()</methodname> method to
- ensure the form is ready for display.
- </para>
- <para>
- When we have a form submission, we can validate the sub form,
- and then check to see if the entire form is now valid. To do
- these tasks, we'll need additional methods that ensure that
- submitted data is added to the session, and that when validating
- the form entire, we validate against all segments from the
- session:
- </para>
- <programlisting language="php"><![CDATA[
- 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);
- }
- }
- ]]></programlisting>
- <para>
- Now that we have the legwork out of the way, let's build the
- actions for this controller. We'll need a landing page for the
- form, and then a 'process' action for processing the form.
- </para>
- <programlisting language="php"><![CDATA[
- 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');
- }
- }
- ]]></programlisting>
- <para>
- As you'll notice, the actual code for processing the form is
- relatively simple. We check to see if we have a current sub form
- submission, and if not, we go back to the landing page. If we do
- have a sub form, we attempt to validate it, redisplaying it if
- it fails. If the sub form is valid, we then check to see if the
- form is valid, which would indicate we're done; if not, we
- display the next form segment. Finally, we display a
- verification page with the contents of the session.
- </para>
- <para>
- The view scripts are very simple:
- </para>
- <programlisting language="php"><![CDATA[
- <?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 ?>
- ]]></programlisting>
- <para>
- Upcoming releases of Zend Framework will include components to
- make multi page forms simpler by abstracting the session and
- ordering logic. In the meantime, the above example should serve
- as a reasonable guideline on how to accomplish this task for
- your site.
- </para>
- </example>
- </sect2>
- </sect1>
- <!--
- vim:se ts=4 sw=4 et:
- -->
|