509 lines
16 KiB
PHP
509 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Class implementing the Page Controller pattern for multipage forms
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* LICENSE:
|
|
*
|
|
* Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>,
|
|
* Bertrand Mansion <golgote@mamasam.com>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * The names of the authors may not be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* @category HTML
|
|
* @package HTML_QuickForm2
|
|
* @author Alexey Borzov <avb@php.net>
|
|
* @author Bertrand Mansion <golgote@mamasam.com>
|
|
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
|
* @version SVN: $Id: Controller.php 295963 2010-03-08 14:33:43Z avb $
|
|
* @link http://pear.php.net/package/HTML_QuickForm2
|
|
*/
|
|
|
|
/** The class representing a page of a multipage form */
|
|
// require_once 'HTML/QuickForm2/Controller/Page.php';
|
|
|
|
/** Object wrapping around session variable used to store controller data */
|
|
// require_once 'HTML/QuickForm2/Controller/SessionContainer.php';
|
|
|
|
/** Class presenting the values stored in session by Controller as submitted ones */
|
|
// require_once 'HTML/QuickForm2/DataSource/Session.php';
|
|
|
|
/**
|
|
* Class implementing the Page Controller pattern for multipage forms
|
|
*
|
|
* This class keeps track of pages and (default) action handlers for the form,
|
|
* it manages $_SESSION container for the form values, allows setting
|
|
* DataSources for the form as a whole and getting its value.
|
|
*
|
|
* @category HTML
|
|
* @package HTML_QuickForm2
|
|
* @author Alexey Borzov <avb@php.net>
|
|
* @author Bertrand Mansion <golgote@mamasam.com>
|
|
* @version Release: @package_version@
|
|
*/
|
|
class HTML_QuickForm2_Controller implements IteratorAggregate
|
|
{
|
|
/**
|
|
* Key in $_REQUEST array that contains the ID of the Controller
|
|
*/
|
|
const KEY_ID = '_qfc_id';
|
|
|
|
/**
|
|
* Key in $_SESSION array that contains the Controller data (needs ID substituted via sprintf())
|
|
*/
|
|
const KEY_CONTAINER = '_%s_container';
|
|
|
|
/**
|
|
* Whether the form is a wizard
|
|
* @var boolean
|
|
*/
|
|
protected $wizard = true;
|
|
|
|
/**
|
|
* Whether Controller ID should be sent in GET and POST parameters
|
|
* @var boolean
|
|
*/
|
|
protected $propagate = true;
|
|
|
|
/**
|
|
* Controller ID
|
|
* @var string
|
|
*/
|
|
protected $id = null;
|
|
|
|
/**
|
|
* Contains the pages (instances of HTML_QuickForm2_Controller_Page) of the multipage form
|
|
* @var array
|
|
*/
|
|
protected $pages = array();
|
|
|
|
/**
|
|
* Contains the mapping of action names to handlers (objects implementing HTML_QuickForm2_Controller_Action)
|
|
* @var array
|
|
*/
|
|
protected $handlers = array();
|
|
|
|
/**
|
|
* The action extracted from HTTP request: array('page', 'action')
|
|
* @var array
|
|
*/
|
|
protected $actionName = null;
|
|
|
|
/**
|
|
* A wrapper around session variable used to store form data
|
|
* @var HTML_QuickForm2_Controller_SessionContainer
|
|
*/
|
|
protected $sessionContainer = null;
|
|
|
|
/**
|
|
* Finds a controller name in $_REQUEST
|
|
*
|
|
* @return string|null Returns nulle if either a KEY_ID is not present
|
|
* in $_REQUEST or KEY_CONTAINER is not present in
|
|
* $_SESSION
|
|
*/
|
|
public static function findControllerID()
|
|
{
|
|
if (empty($_REQUEST[self::KEY_ID])
|
|
|| empty($_SESSION[sprintf(self::KEY_CONTAINER, $_REQUEST[self::KEY_ID])])
|
|
) {
|
|
return null;
|
|
} else {
|
|
return $_REQUEST[self::KEY_ID];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class constructor
|
|
*
|
|
* Sets the form ID, whether to send this ID in POST and GET parameters,
|
|
* wizard / non-wizard behaviour.
|
|
*
|
|
* Different forms should be given different IDs, as they are used to store
|
|
* values in session. If $id is empty, the controller will try to find it
|
|
* in $_REQUEST, throwing the exception if this fails.
|
|
*
|
|
* Wizard forms only allow going to the next page if all the previous ones
|
|
* are valid.
|
|
*
|
|
* @param string Form ID
|
|
* @param boolean Whether the form is a wizard
|
|
* @param boolean Whether form's ID should be sent with GET and POST parameters
|
|
* @throws HTML_QuickForm2_NotFoundException if ID is not given and cannot
|
|
* be found in $_REQUEST, or session container is empty
|
|
*/
|
|
public function __construct($id = null, $wizard = true, $propagateId = false)
|
|
{
|
|
if (empty($id)) {
|
|
$propagateId = true;
|
|
$id = self::findControllerID();
|
|
}
|
|
if (empty($id)) {
|
|
throw new HTML_QuickForm2_NotFoundException(
|
|
'Controller ID not available in $_REQUEST or session ' .
|
|
'container is empty, please provide ID to constructor'
|
|
);
|
|
}
|
|
$this->id = $id;
|
|
$this->wizard = (bool)$wizard;
|
|
$this->propagate = (bool)$propagateId;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the form is a wizard
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isWizard()
|
|
{
|
|
return $this->wizard;
|
|
}
|
|
|
|
/**
|
|
* Returns the form ID
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getId()
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
/**
|
|
* Returns whether to send form id with GET and POST parameters
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function propagateId()
|
|
{
|
|
return $this->propagate;
|
|
}
|
|
|
|
/**
|
|
* Returns the session container with the controller data
|
|
*
|
|
* @return HTML_QuickForm2_Controller_SessionContainer
|
|
*/
|
|
public function getSessionContainer()
|
|
{
|
|
if (empty($this->sessionContainer)) {
|
|
$this->sessionContainer = new HTML_QuickForm2_Controller_SessionContainer($this);
|
|
}
|
|
return $this->sessionContainer;
|
|
}
|
|
|
|
/**
|
|
* Removes the session variable containing the controller data
|
|
*/
|
|
public function destroySessionContainer()
|
|
{
|
|
unset($_SESSION[sprintf(self::KEY_CONTAINER, $this->id)]);
|
|
$this->sessionContainer = null;
|
|
}
|
|
|
|
/**
|
|
* Extracts the name of the page and the action to perform with it from HTTP request data
|
|
*
|
|
* @return array first element is page name, second is action name
|
|
*/
|
|
public function getActionName()
|
|
{
|
|
if (is_array($this->actionName)) {
|
|
return $this->actionName;
|
|
}
|
|
if (empty($this->pages)) {
|
|
throw new HTML_QuickForm2_NotFoundException('No pages added to the form');
|
|
}
|
|
$names = array_map('preg_quote', array_keys($this->pages));
|
|
$regex = '/^_qf_(' . implode('|', $names) . ')_(.+?)(_x)?$/';
|
|
foreach (array_keys($_REQUEST) as $key) {
|
|
if (preg_match($regex, $key, $matches)) {
|
|
$this->actionName = array($matches[1], $matches[2]);
|
|
break;
|
|
}
|
|
}
|
|
if (!is_array($this->actionName)) {
|
|
reset($this->pages);
|
|
$this->actionName = array(key($this->pages), 'display');
|
|
}
|
|
return $this->actionName;
|
|
}
|
|
|
|
/**
|
|
* Processes the request
|
|
*
|
|
* This finds the page, the action to perform with it and passes the action
|
|
* to the page's handle() method.
|
|
*
|
|
* @throws HTML_QuickForm2_Exception
|
|
*/
|
|
public function run()
|
|
{
|
|
list($page, $action) = $this->getActionName();
|
|
return $this->pages[$page]->handle($action);
|
|
}
|
|
|
|
/**
|
|
* Adds a handler for a specific action
|
|
*
|
|
* @param string action name
|
|
* @param HTML_QuickForm2_Controller_Action the handler for the action
|
|
*/
|
|
public function addHandler($actionName, HTML_QuickForm2_Controller_Action $action)
|
|
{
|
|
$this->handlers[$actionName] = $action;
|
|
}
|
|
|
|
/**
|
|
* Handles an action
|
|
*
|
|
* This will be called if the page itself does not have a handler for a
|
|
* specific action. The method also loads and uses default handlers for
|
|
* common actions, if specific ones were not added.
|
|
*
|
|
* @param HTML_QuickForm2_Controller_Page form page
|
|
* @param string action name
|
|
* @throws HTML_QuickForm2_NotFoundException if handler for an action is missing
|
|
*/
|
|
public function handle(HTML_QuickForm2_Controller_Page $page, $actionName)
|
|
{
|
|
if (!isset($this->handlers[$actionName])
|
|
&& in_array($actionName, array('next', 'back', 'submit', 'display', 'jump'))
|
|
) {
|
|
$className = 'HTML_QuickForm2_Controller_Action_' . ucfirst($actionName);
|
|
if (!class_exists($className)) {
|
|
HTML_QuickForm2_Loader::loadClass($className);
|
|
}
|
|
$this->addHandler($actionName, new $className());
|
|
}
|
|
if (isset($this->handlers[$actionName])) {
|
|
return $this->handlers[$actionName]->perform($page, $actionName);
|
|
} else {
|
|
throw new HTML_QuickForm2_NotFoundException(
|
|
"Unhandled action '{$actionName}' for page '{$page->getForm()->getId()}'"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new page to the form
|
|
*
|
|
* @param HTML_QuickForm2_Controller_Page
|
|
*/
|
|
public function addPage(HTML_QuickForm2_Controller_Page $page)
|
|
{
|
|
$pageId = $page->getForm()->getId();
|
|
if (!empty($this->pages[$pageId])) {
|
|
throw new HTML_QuickForm2_InvalidArgumentException(
|
|
"Duplicate page ID '{$pageId}'"
|
|
);
|
|
}
|
|
$page->setController($this);
|
|
$this->pages[$pageId] = $page;
|
|
}
|
|
|
|
/**
|
|
* Returns a page
|
|
*
|
|
* @param string Page ID
|
|
* @return HTML_QuickForm2_Controller_Page
|
|
* @throws HTML_QuickForm2_NotFoundException if there is no page with
|
|
* the given ID
|
|
*/
|
|
public function getPage($pageId)
|
|
{
|
|
if (!empty($this->pages[$pageId])) {
|
|
return $this->pages[$pageId];
|
|
} else {
|
|
throw new HTML_QuickForm2_NotFoundException(
|
|
"Unknown page '{$pageId}'"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the page preceding the given one
|
|
*
|
|
* @param HTML_QuickForm2_Controller_Page
|
|
* @return HTML_QuickForm2_Controller_Page|null
|
|
*/
|
|
public function previousPage(HTML_QuickForm2_Controller_Page $reference)
|
|
{
|
|
$previous = null;
|
|
foreach ($this->pages as $page) {
|
|
if ($page === $reference) {
|
|
return $previous;
|
|
}
|
|
$previous = $page;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the page following the given one
|
|
*
|
|
* @param HTML_QuickForm2_Controller_Page
|
|
* @return HTML_QuickForm2_Controller_Page|null
|
|
*/
|
|
public function nextPage(HTML_QuickForm2_Controller_Page $reference)
|
|
{
|
|
$previous = null;
|
|
foreach ($this->pages as $page) {
|
|
if ($previous === $reference) {
|
|
return $page;
|
|
}
|
|
$previous = $page;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the pages of the controller are valid
|
|
*
|
|
* @param HTML_QuickForm2_Controller_Page If given, check only the pages
|
|
* before (not including) that page
|
|
* @return bool
|
|
*/
|
|
public function isValid(HTML_QuickForm2_Controller_Page $reference = null)
|
|
{
|
|
$container = $this->getSessionContainer();
|
|
foreach ($this->pages as $id => $page) {
|
|
if ($reference === $page) {
|
|
return true;
|
|
}
|
|
if (!$container->getValidationStatus($id)) {
|
|
// We should handle the possible situation when the user has never
|
|
// seen a page of a non-modal multipage form
|
|
if (!$this->isWizard()
|
|
&& null === $container->getValidationStatus($id)
|
|
) {
|
|
// Empty Session datasource makes the form look submitted
|
|
$page->getForm()->setDatasources(array_merge(
|
|
$container->getDatasources(),
|
|
array(new HTML_QuickForm2_DataSource_Session(array()))
|
|
));
|
|
// This will store the "submitted" values in session and
|
|
// return validation status
|
|
if ($page->storeValues()) {
|
|
continue;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the first page that failed validation
|
|
*
|
|
* @return HTML_QuickForm2_Controller_Page|null
|
|
*/
|
|
public function getFirstInvalidPage()
|
|
{
|
|
foreach ($this->pages as $id => $page) {
|
|
if (!$this->getSessionContainer()->getValidationStatus($id)) {
|
|
return $page;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Adds a new data source to the Controller
|
|
*
|
|
* Note that Controller data sources are stored in session, so your data source
|
|
* implementation should properly handle its (un)serialization.
|
|
*
|
|
* @param HTML_QuickForm2_DataSource Data source
|
|
*/
|
|
public function addDataSource(HTML_QuickForm2_DataSource $datasource)
|
|
{
|
|
$this->getSessionContainer()->storeDatasources(
|
|
array_merge($this->getSessionContainer()->getDatasources(),
|
|
array($datasource))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the form's values
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getValue()
|
|
{
|
|
$values = array();
|
|
foreach (array_keys($this->pages) as $id) {
|
|
$pageValues = $this->getSessionContainer()->getValues($id);
|
|
// skip elements representing actions
|
|
foreach ($pageValues as $key => $value) {
|
|
if (0 !== strpos($key, '_qf')) {
|
|
if (isset($values[$key]) && is_array($value)) {
|
|
$values[$key] = self::arrayMerge($values[$key], $value);
|
|
} else {
|
|
$values[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Merges two arrays
|
|
*
|
|
* Merges two arrays like the PHP function array_merge_recursive does,
|
|
* the difference being that existing integer keys will not be renumbered.
|
|
*
|
|
* @param array
|
|
* @param array
|
|
* @return array resulting array
|
|
*/
|
|
protected static function arrayMerge($a, $b)
|
|
{
|
|
foreach ($b as $k => $v) {
|
|
if (!is_array($v) || isset($a[$k]) && !is_array($a[$k])) {
|
|
$a[$k] = $v;
|
|
} else {
|
|
$a[$k] = self::arrayMerge(isset($a[$k])? $a[$k]: array(), $v);
|
|
}
|
|
}
|
|
return $a;
|
|
}
|
|
|
|
/**
|
|
* Returns an Iterator for the form's pages
|
|
*
|
|
* @return ArrayIterator
|
|
*/
|
|
public function getIterator()
|
|
{
|
|
return new ArrayIterator($this->pages);
|
|
}
|
|
}
|
|
?>
|