View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFDEV:Zend Proposal Zone Template With Tracker}

{zone-data:jira-key}LPFMCT{zone-data}
{zone-data:jira-pid}10017{zone-data}
{zone-data:jira-recently}
{jiraissues:url=http://framework.zend.com/issues/secure/IssueNavigator.jspa?view=rss&created%3Aprevious=-1w&pid=10017&sorter/field=created&sorter/order=DESC&tempMax=25&reset=true&decorator=none|columns=Type;Key;Summary;Assignee;Priority;Status;Created}
{zone-data}
{zone-data:jira-summary}
{jiraportlet:url=http://framework.zend.com/issues/secure/RunPortlet.jspa?portletKey=com.atlassian.jira.plugin.system.portlets:project&projectid=10017&projectinfo=full}
{zone-data}
{zone-data:review-end}August 18, 2006{zone-data}


{zone-data:component-name}
Zend_Controller_Action_Helper_Multiform
{zone-data}

{zone-data:proposer-list}
[Simon Mundy|mailto:studio@peptolab.com]
{zone-data}

{zone-data:revision}
0.1 - 9 June 2006: Initial version
0.2 - 1 August 2007: Reworked version using new Helper component architecture
{zone-data}

{zone-data:overview}
Zend_Controller_Action_Helper_Multiform provides a method for handling modal form submission between controller actions. It allows developers to define a sequence of actions for which forms can be created and validated and have their submitted data stored in a container. Multiform interacts heavily with the Controller, Request object and other Helpers to provide automatic handling of redirects, storage and retrieval.
{zone-data}

{zone-data:references}
* [PEAR's HTML_QuickForm_Controller|http://pear.php.net/package/HTML_QuickForm_Controller/]
{zone-data}

{zone-data:requirements}
* Ability to create 1 or more form 'pages' from which an end-user can work through either in a linear approach.
* Provide a method for storing data per action.
* Provide a method to pass control back to the parent controller.
* Provide a way of setting global form default values and per-page default values.
* Provide a way of retrieving global form values and per-page submit values.
* Allow methods to clear a multi-page 'session' if a user navigates away from the controller.
* Provide a method to export the current form object as either an array, pure HTML or as a view helper
* Release dependency on a Form object (e.g. HTML_QuickForm) and instead provide a single interface from which to accept forms for population, validation and data retrieval
{zone-data}

{zone-data:dependencies}
* Zend_Exception
{zone-data}

{zone-data:operation}
A Multiform instance is referenced from each action within a controller. Ideally, the developer will retrieve an instance from the Helper Broker in the preDispatch phase of the controller and pass a sequence of actions for it to follow, as well as setting default values.

Each Action within a Multiform can be queried to see if it been submitted, whether it is valid and to return any submitted values if applicable. The processing of each Action happens simply by calling the 'update' method with an array of form data and a flag to indicate whether the form data has been validated. The validation flag is necessary, as sometimes the developer may wish to store form values even though the end user wants to go 'back' or 'jump' to a specific page.

Navigation from one Action to the next is achieved by passing control values inside the form data (e.g. a submit button with a specific name). The defaults are for an underscored control key - _next, _back, _submit, _cancel - however these can be changed by the developer to any scheme they desire.

To ensure data is stored between actions all controller-submitted data will be stored in a namespaced session. An existing Zend_Session can be passed to the Controller for convenience, otherwise a default instance is used. The session data remains active until a developer explicitly clears it or the session expires.
{zone-data}

{zone-data:class-list}
* Zend_Controller_Action_Helper_Multiform
* Zend_Controller_Action_Helper_Multiform_Exception
{zone-data}

{zone-data:use-cases}
||UC-01||
In this example, we require 3 forms for a user to fill out - a Personal details form, a Product order/payment details form and a Review page to confirm approval. The 3 forms themselves are an exercise for the developer to complete - what we are interested in is getting the data from those forms and storing them.

The first form has two 'submit' buttons named '_next' and '_cancel' - the former allowing access to the next page when the form is submitted and valid, the latter allowing a user to cleanly cancel the session.

The second form has '_next' and '_back' named submit buttons to navigate forward and backward, allowing revision of the form if desired.

The third form has '_submit' and '_back' named submit buttons. The 'submitAction' controller action will be called only if all three forms have been submitted and validated.

Note also that a user cannot manually navigate to any action without previous actions having been successfully navigated. If, for example, the user navigated to '/payment/review' without having submitted their details or product, Multiform will perform a redirection back through the sequence to determine the correct action to begin with.
{code}
<?php

class PaymentController extends Zend_Controller_Action_Abstract
{
protected $_multiform;

public function init()
{
$this->_multiform = $this->_helper->getHelper('Multiform')
->setActions(array('details', 'product', 'review'));
}

public function indexAction()
{
// Use the default page to auto-clear an old multiform session if exists
$this->_multiform->clear();
$this->_helper->redirector('details');
}

public function detailsAction()
{
// Create a user form object using existing defaults (e.g. HTML_Quickform from PEAR)
$form = new My_Form_PaymentDetails();
$form->build($this->_multiform->getValues('details'));

// If form submitted, save and redirect
if ($form->isSubmitted()) {
$this->_multiform->update($form->exportValues(), $form->validate());
}

$this->view->form = $form;
}

public function productAction()
{
// Create a user form object using existing defaults (e.g. HTML_Quickform from PEAR)
$form = new My_Form_PaymentProduct();
$form->build($this->_multiform->getValues('product'));

// If form submitted, save and redirect
if ($form->isSubmitted()) {
$this->_multiform->update($form->exportValues(), $form->validate());
}

$this->view->form = $form;
}

public function reviewAction()
{
// Create a user form object using existing defaults (e.g. HTML_Quickform from PEAR)
$form = new My_Form_PaymentReview();
$form->build($this->_multiform->getValues('review'));

// If form submitted, save and redirect
if ($form->isSubmitted()) {
$this->_multiform->update($form->exportValues(), $form->validate());
}

$this->view->form = $form;
}

public function submitAction()
{
// Do stuff with your data
$values = $this->_multiform->getValues();

// End session
$this->_multiform->clear();
}

public function cancelAction()
{
// End session and redirect elsewhere
$this->_multiform->clear();
$this->_helper->redirector('index', 'index');
}
}
{code}
{zone-data}

{zone-data:skeletons}
{code}
class Zend_Controller_Action_Helper_Multiform_Exception extends Zend_Exception {}

class Zend_Controller_Action_Helper_Multiform extends Zend_Controller_Action_Helper_Abstract
{
/**
* $_session - Zend_Session storage object
*
* @var Zend_Session
*/
static protected $_session = null;

/**
* Default keys for controlling the multiform
*/
const ACTION_PREFIX = '_';
const ACTION_KEY_NEXT = 'next';
const ACTION_KEY_BACK = 'back';
const ACTION_KEY_SUBMIT = 'submit';
const ACTION_KEY_CANCEL = 'cancel';


/**
* Sequence of actions
*/
protected $_actions = array();

/**
* Current action
*/
protected $_current;

/**
* Sequence of actions
*/

/**
* If set, determines next action that should be processed
*/
protected $_validAction;

/**
* Controller keys
*/
protected $_ctrlAction = array(self::ACTION_KEY_NEXT => 'next',
self::ACTION_KEY_BACK => 'back',
self::ACTION_KEY_SUBMIT => 'submit',
self::ACTION_KEY_CANCEL => 'cancel');

/**
* Construct and set default session object
*/
public function __construct()
{
if (!self::$_session instanceof Zend_Session_Namespace) {
self::$_session = new Zend_Session_Namespace($this->getName());
}
}

/**
* Ensure Multiform has been allocated actions before controller processing begins
*/
public function preDispatch()
{
if (!is_array($this->_actions)) {
throw new exception('multiform has not been assigned any actions');
}

$action = $this->getRequest()->getActionName();

if (!$this->isControllerAction($action)) {
if (!$this->isValidAction($action)) {
$this->redirect($this->getCurrentValidAction());
}
}

$this->_current = $action;
}

/**
* Set sequence of actions
*/
public function setActions(Array $actions)
{
$this->_actions = $actions;

if (is_null(self::$_session->valid) || !array_key_exists($actions[0], self::$_session->valid)) {
$this->clear();
}

return $this;
}

/**
* Set values for an action
*/
public function setValues($action, Array $values, $valid = false)
{
if (!in_array($action, $this->_actions)) {
throw new exception($action . ' is not a valid action');
}

self::$_session->valid[$action] = (boolean) $valid;
self::$_session->value[$action] = $values;

return $this;
}

/**
* Retrieve action values
*/
public function getValues($action = null)
{
if ($action === null) {
return self::$_session->value;
}

if (isset(self::$_session->value[$action])) {
return self::$_session->value[$action];
}

return null;
}

/**
* Retrieve current valid action
*/
public function getCurrentValidAction()
{
return $this->_validAction;
}

/**
* Determine if an action has been validated
*/
public function isValidAction($current)
{
foreach ($this->_actions as $action) {
$this->_validAction = $action;

if ($current == $action) {
break;
}

if (!$this->isCompleteAction($action)) {
$action = false;
break;
}
}

return $action;
}

/**
* Determine if an action has been submitted
*/
public function isCompleteAction($action)
{
if (isset(self::$_session->valid[$action])) {
return self::$_session->valid[$action];
}

return false;
}

/**
* Determine if an action is under control by the Multiform
*/
public function isControllerAction($action)
{
return in_array($action, $this->_ctrlAction);
}

/**
* Reset all session data
*/
public function clear()
{
$this->_default = array();

self::$_session->valid = array();
self::$_session->value = array();
self::$_session->registry = array();

foreach ($this->_actions as $id) {
if (!isset(self::$_session->valid[$id])) {
self::$_session->valid[$id] = false;
self::$_session->value[$id] = array();
}
}
}

/**
* Perform all processing
*/
public function update(Array $data = array(), $valid = true)
{
$action = false;

// Retrieve form values and extract non-control keys/values
$submit = $this->_filterArrayKey($data, self::ACTION_PREFIX);

// Remove any control keys
foreach(array_keys($submit) as $key) {
unset($data[self::ACTION_PREFIX . $key]);
}

// Save data
$this->setValues($this->_current, $data, $valid);

foreach ($submit as $key => $value) {
if ($value != '') {
$action = preg_replace('/_./', '', $key);
break;
}
}

// Ensure we have a valid action
if ($action === false) {
return false;
}

// Process actions
switch ($action) {
case $this->_ctrlAction[self::ACTION_KEY_BACK]:
$pos = array_search($this->_current, $this->_actions);
if ($pos <= 0) {
$action = $this->_actions[0];
} else {
$action = $this->_actions[$pos - 1];
}
break;

case $this->_ctrlAction[self::ACTION_KEY_NEXT]:
if (!$valid) {
return false;
}
$pos = array_search($this->_current, $this->_actions);
if ($pos == count($this->_actions) - 1) {
$action = $this->_actions[count($this->_actions) - 1];
} else {
$action = $this->_actions[$pos + 1];
}
break;

case $this->_ctrlAction[self::ACTION_KEY_SUBMIT]:
if (!$valid) {
return false;
}
break;

case $this->_ctrlAction[self::ACTION_KEY_CANCEL]:
break;

default:
if (!in_array($action, $this->_actions)) {
$this->redirect($this->getValidAction());
}
break;
}

$this->redirect($action);
}

/**
* Use the redirector helper to navigate the controller
*/
public function redirect($action, $controller = null, $module = null)
{
return $this->getActionController()->getHelper('Redirector')->gotoAndExit($action,
$controller,
$module);
}

/**
* Filter an array for prefixed values
*/
protected function _filterArrayKey(Array $array = array(), $prefix)
{
$result = array();
foreach($array as $key => $value) {
if (substr($key, 0, strlen($prefix)) == $prefix) {
$result[substr($key, strlen($prefix))] = $value;
}
}

return $result;
}
}
{code}
{zone-data}

{zone-template-instance}]]></ac:plain-text-body></ac:macro>