2008-04-18 00:03:51 +02:00
|
|
|
<?php
|
|
|
|
|
2017-09-07 01:32:55 +02:00
|
|
|
namespace SilverStripe\MultiForm\Models;
|
|
|
|
|
|
|
|
use SilverStripe\Control\Controller;
|
|
|
|
use SilverStripe\Control\HTTPResponse;
|
|
|
|
use SilverStripe\Control\Session;
|
|
|
|
use SilverStripe\Core\Convert;
|
|
|
|
use SilverStripe\Core\Injector\Injector;
|
|
|
|
use SilverStripe\Forms\FieldList;
|
|
|
|
use SilverStripe\Forms\Form;
|
|
|
|
use SilverStripe\Forms\FormAction;
|
|
|
|
use SilverStripe\Forms\HiddenField;
|
|
|
|
use SilverStripe\ORM\ArrayList;
|
|
|
|
use SilverStripe\ORM\DataList;
|
|
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
use SilverStripe\View\SSViewer;
|
|
|
|
|
2008-04-18 00:03:51 +02:00
|
|
|
/**
|
2008-04-22 13:03:03 +02:00
|
|
|
* MultiForm manages the loading of single form steps, and acts as a state
|
|
|
|
* machine that connects to a {@link MultiFormSession} object as a persistence
|
|
|
|
* layer.
|
2015-11-02 01:38:50 +01:00
|
|
|
*
|
2008-04-18 00:03:51 +02:00
|
|
|
* CAUTION: If you're using controller permission control,
|
|
|
|
* you have to allow the following methods:
|
2013-05-25 09:04:59 +02:00
|
|
|
*
|
2008-04-18 00:03:51 +02:00
|
|
|
* <code>
|
2013-05-25 09:04:59 +02:00
|
|
|
* private static $allowed_actions = array('next','prev');
|
2015-11-02 01:38:50 +01:00
|
|
|
* </code>
|
|
|
|
*
|
2008-04-18 00:03:51 +02:00
|
|
|
*/
|
2017-09-06 22:47:35 +02:00
|
|
|
abstract class MultiForm extends Form
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* A session object stored in the database, to identify and store
|
|
|
|
* data for this MultiForm instance.
|
|
|
|
*
|
|
|
|
* @var MultiFormSession
|
|
|
|
*/
|
|
|
|
protected $session;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current encrypted MultiFormSession identification.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $currentSessionHash;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defines which subclass of {@link MultiFormStep} should be the first
|
|
|
|
* step in the multi-step process.
|
|
|
|
*
|
|
|
|
* @var string Classname of a {@link MultiFormStep} subclass
|
|
|
|
*/
|
2017-09-08 04:24:28 +02:00
|
|
|
private static $start_step;
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the casting for these fields.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2017-09-07 01:32:55 +02:00
|
|
|
private static $casting = [
|
2017-09-06 22:47:35 +02:00
|
|
|
'CompletedStepCount' => 'Int',
|
|
|
|
'TotalStepCount' => 'Int',
|
|
|
|
'CompletedPercent' => 'Float'
|
2017-09-07 01:32:55 +02:00
|
|
|
];
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $get_var = 'MultiFormSessionID';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* These fields are ignored when saving the raw form data into session.
|
|
|
|
* This ensures only field data is saved, and nothing else that's useless
|
|
|
|
* or potentially dangerous.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2017-09-08 04:24:28 +02:00
|
|
|
private static $ignored_fields = [
|
2017-09-06 22:47:35 +02:00
|
|
|
'url',
|
|
|
|
'executeForm',
|
|
|
|
'SecurityID'
|
2017-09-07 01:32:55 +02:00
|
|
|
];
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Any of the actions defined in this variable are exempt from
|
|
|
|
* being validated.
|
|
|
|
*
|
|
|
|
* This is most useful for the "Back" (action_prev) action, as
|
|
|
|
* you typically don't validate the form when the user is going
|
|
|
|
* back a step.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2017-09-08 04:24:28 +02:00
|
|
|
private static $actions_exempt_from_validation = [
|
2017-09-06 22:47:35 +02:00
|
|
|
'action_prev'
|
2017-09-07 01:32:55 +02:00
|
|
|
];
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $displayLink;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flag which is being used in getAllStepsRecursive() to allow adding the completed flag on the steps
|
|
|
|
*
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $currentStepHasBeenFound = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the MultiForm instance.
|
|
|
|
*
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param Controller $controller Controller instance this form is created on
|
2017-09-06 22:47:35 +02:00
|
|
|
* @param string $name The form name, typically the same as the method name
|
|
|
|
*/
|
|
|
|
public function __construct($controller, $name)
|
|
|
|
{
|
|
|
|
// First set the controller and name manually so they are available for
|
|
|
|
// field construction.
|
|
|
|
$this->controller = $controller;
|
|
|
|
$this->name = $name;
|
|
|
|
|
|
|
|
// Set up the session for this MultiForm instance
|
|
|
|
$this->setSession();
|
|
|
|
|
|
|
|
// Get the current step available (Note: either returns an existing
|
|
|
|
// step or creates a new one if none available)
|
|
|
|
$currentStep = $this->getCurrentStep();
|
|
|
|
|
|
|
|
// Set the step returned above as the current step
|
|
|
|
$this->setCurrentStep($currentStep);
|
|
|
|
|
|
|
|
// Set the form of the step to this form instance
|
|
|
|
$currentStep->setForm($this);
|
|
|
|
|
|
|
|
// Set up the fields for the current step
|
|
|
|
$fields = $currentStep->getFields();
|
|
|
|
|
|
|
|
// Set up the actions for the current step
|
|
|
|
$actions = $this->actionsFor($currentStep);
|
|
|
|
|
|
|
|
// Set up validation (if necessary)
|
|
|
|
$validator = null;
|
|
|
|
$applyValidation = true;
|
|
|
|
|
2017-09-08 04:24:28 +02:00
|
|
|
$actionNames = $this->config()->get('actions_exempt_from_validation');
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
if ($actionNames) {
|
|
|
|
foreach ($actionNames as $exemptAction) {
|
2017-09-08 03:57:33 +02:00
|
|
|
if (!empty($this->getRequest()->requestVar($exemptAction))) {
|
2017-09-06 22:47:35 +02:00
|
|
|
$applyValidation = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply validation if the current step requires validation (is not exempt)
|
|
|
|
if ($applyValidation) {
|
|
|
|
if ($currentStep->getValidator()) {
|
2017-09-08 03:57:33 +02:00
|
|
|
$this->setValidator($currentStep->getValidator());
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Give the fields, actions, and validation for the current step back to the parent Form class
|
2017-09-08 03:57:33 +02:00
|
|
|
parent::__construct($controller, $name, $fields, $actions);
|
2017-09-06 22:47:35 +02:00
|
|
|
|
2017-09-08 03:57:33 +02:00
|
|
|
$getVar = $this->getGetVar();
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
// Set a hidden field in our form with an encrypted hash to identify this session.
|
2017-09-07 01:32:55 +02:00
|
|
|
$this->fields->push(HiddenField::create($getVar, false, $this->session->Hash));
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
// If there is saved data for the current step, we load it into the form it here
|
|
|
|
//(CAUTION: loadData() MUST unserialize first!)
|
|
|
|
if ($data = $currentStep->loadData()) {
|
|
|
|
$this->loadDataFrom($data);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disable security token - we tie a form to a session ID instead
|
|
|
|
$this->disableSecurityToken();
|
|
|
|
|
2017-09-08 04:24:28 +02:00
|
|
|
$this->config()->merge('ignored_fields', $getVar);
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Accessor method to $this->controller.
|
|
|
|
*
|
|
|
|
* @return Controller this MultiForm was instanciated on.
|
|
|
|
*/
|
|
|
|
public function getController()
|
|
|
|
{
|
|
|
|
return $this->controller;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the get_var to the template engine
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getGetVar()
|
|
|
|
{
|
2017-09-08 03:57:33 +02:00
|
|
|
return $this->config()->get('get_var');
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current step.
|
|
|
|
*
|
|
|
|
* If StepID has been set in the URL, we attempt to get that record
|
|
|
|
* by the ID. Otherwise, we check if there's a current step ID in
|
|
|
|
* our session record. Failing those cases, we assume that the form has
|
|
|
|
* just been started, and so we create the first step and return it.
|
|
|
|
*
|
|
|
|
* @return MultiFormStep subclass
|
|
|
|
*/
|
|
|
|
public function getCurrentStep()
|
|
|
|
{
|
2017-09-08 04:24:28 +02:00
|
|
|
$startStepClass = $this->config()->get('start_step');
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
// Check if there was a start step defined on the subclass of MultiForm
|
|
|
|
if (!isset($startStepClass)) {
|
|
|
|
user_error(
|
|
|
|
'MultiForm::init(): Please define a $start_step on ' . $this->class,
|
|
|
|
E_USER_ERROR
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine whether we use the current step, or create one if it doesn't exist
|
|
|
|
$currentStep = null;
|
2017-09-07 01:32:55 +02:00
|
|
|
$StepID = $this->controller->getRequest()->getVar('StepID');
|
2017-09-06 22:47:35 +02:00
|
|
|
if (isset($StepID)) {
|
|
|
|
$currentStep = DataObject::get_one(
|
2017-09-08 03:57:33 +02:00
|
|
|
MultiFormStep::class,
|
2017-09-07 01:32:55 +02:00
|
|
|
[
|
2017-09-06 22:47:35 +02:00
|
|
|
'SessionID' => $this->session->ID,
|
|
|
|
'ID' => $StepID
|
2017-09-07 01:32:55 +02:00
|
|
|
]
|
2017-09-06 22:47:35 +02:00
|
|
|
);
|
|
|
|
} elseif ($this->session->CurrentStepID) {
|
|
|
|
$currentStep = $this->session->CurrentStep();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Always fall back to creating a new step (in case the session or request data is invalid)
|
|
|
|
if (!$currentStep || !$currentStep->ID) {
|
2017-09-07 01:32:55 +02:00
|
|
|
$currentStep = Injector::inst()->create($startStepClass);
|
2017-09-06 22:47:35 +02:00
|
|
|
$currentStep->SessionID = $this->session->ID;
|
|
|
|
$currentStep->write();
|
|
|
|
$this->session->CurrentStepID = $currentStep->ID;
|
|
|
|
$this->session->write();
|
|
|
|
$this->session->flushCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($currentStep) {
|
|
|
|
$currentStep->setForm($this);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $currentStep;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the step passed in as the current step.
|
|
|
|
*
|
|
|
|
* @param MultiFormStep $step A subclass of MultiFormStep
|
|
|
|
* @return boolean The return value of write()
|
|
|
|
*/
|
|
|
|
protected function setCurrentStep($step)
|
|
|
|
{
|
|
|
|
$this->session->CurrentStepID = $step->ID;
|
|
|
|
$step->setForm($this);
|
|
|
|
|
|
|
|
return $this->session->write();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Accessor method to $this->session.
|
|
|
|
*
|
|
|
|
* @return MultiFormSession
|
|
|
|
*/
|
2017-09-08 03:57:33 +02:00
|
|
|
public function getMultiFormSession()
|
2017-09-06 22:47:35 +02:00
|
|
|
{
|
2017-09-08 03:57:33 +02:00
|
|
|
if (!$this->session) {
|
|
|
|
$this->setSession();
|
|
|
|
}
|
2017-09-06 22:47:35 +02:00
|
|
|
return $this->session;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up the session.
|
|
|
|
*
|
|
|
|
* If MultiFormSessionID isn't set, we assume that this is a new
|
|
|
|
* multiform that requires a new session record to be created.
|
|
|
|
*
|
|
|
|
* @TODO Fix the fact you can continually refresh and create new records
|
|
|
|
* if MultiFormSessionID isn't set.
|
|
|
|
*
|
|
|
|
* @TODO Not sure if we should bake the session stuff directly into MultiForm.
|
|
|
|
* Perhaps it would be best dealt with on a separate class?
|
|
|
|
*/
|
|
|
|
protected function setSession()
|
|
|
|
{
|
|
|
|
$this->session = $this->getCurrentSession();
|
|
|
|
|
|
|
|
// If there was no session found, create a new one instead
|
|
|
|
if (!$this->session) {
|
2017-09-08 03:57:33 +02:00
|
|
|
$this->session = MultiFormSession::create();
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create encrypted identification to the session instance if it doesn't exist
|
|
|
|
if (!$this->session->Hash) {
|
|
|
|
$this->session->Hash = sha1($this->session->ID . '-' . microtime());
|
|
|
|
$this->session->write();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the currently used encrypted hash to identify
|
|
|
|
* the MultiFormSession.
|
|
|
|
*
|
|
|
|
* @param string $hash Encrypted identification to session
|
|
|
|
*/
|
|
|
|
public function setCurrentSessionHash($hash)
|
|
|
|
{
|
|
|
|
$this->currentSessionHash = $hash;
|
|
|
|
$this->setSession();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the currently used {@link MultiFormSession}
|
|
|
|
* @return MultiFormSession|boolean FALSE
|
|
|
|
*/
|
|
|
|
public function getCurrentSession()
|
|
|
|
{
|
|
|
|
if (!$this->currentSessionHash) {
|
2017-09-08 03:57:33 +02:00
|
|
|
$this->currentSessionHash = $this->controller->getRequest()->getVar($this->getGetVar());
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
if (!$this->currentSessionHash) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-07 01:32:55 +02:00
|
|
|
$this->session = MultiFormSession::get()->filter([
|
2017-09-06 22:47:35 +02:00
|
|
|
"Hash" => $this->currentSessionHash,
|
|
|
|
"IsComplete" => 0
|
2017-09-07 01:32:55 +02:00
|
|
|
])->first();
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
return $this->session;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all steps saved in the database for the currently active session,
|
|
|
|
* in the order they were saved, oldest to newest (automatically ordered by ID).
|
|
|
|
* If you want a full chain of steps regardless if they've already been saved
|
|
|
|
* to the database, use {@link getAllStepsLinear()}.
|
|
|
|
*
|
|
|
|
* @param string $filter SQL WHERE statement
|
2017-09-07 01:32:55 +02:00
|
|
|
* @return DataList|boolean A set of MultiFormStep subclasses
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function getSavedSteps($filter = null)
|
|
|
|
{
|
|
|
|
$filter .= ($filter) ? ' AND ' : '';
|
|
|
|
$filter .= sprintf("\"SessionID\" = '%s'", $this->session->ID);
|
2017-09-08 03:57:33 +02:00
|
|
|
return DataObject::get(MultiFormStep::class, $filter);
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a step which was previously saved to the database in the current session.
|
|
|
|
* Caution: This might cause unexpected behaviour if you have multiple steps
|
|
|
|
* in your chain with the same classname.
|
|
|
|
*
|
|
|
|
* @param string $className Classname of a {@link MultiFormStep} subclass
|
2017-09-07 01:32:55 +02:00
|
|
|
* @return DataObject
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function getSavedStepByClass($className)
|
|
|
|
{
|
|
|
|
return DataObject::get_one(
|
2017-09-08 03:57:33 +02:00
|
|
|
MultiFormStep::class,
|
2017-09-06 22:47:35 +02:00
|
|
|
sprintf(
|
|
|
|
"\"SessionID\" = '%s' AND \"ClassName\" = '%s'",
|
|
|
|
$this->session->ID,
|
|
|
|
Convert::raw2sql($className)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build a FieldList of the FormAction fields for the given step.
|
|
|
|
*
|
|
|
|
* If the current step is the final step, we push in a submit button, which
|
|
|
|
* calls the action {@link finish()} to finalise the submission. Otherwise,
|
|
|
|
* we push in a next button which calls the action {@link next()} to determine
|
|
|
|
* where to go next in our step process, and save any form data collected.
|
|
|
|
*
|
|
|
|
* If there's a previous step (a step that has the current step as it's next
|
|
|
|
* step class), then we allow a previous button, which calls the previous action
|
|
|
|
* to determine which step to go back to.
|
|
|
|
*
|
|
|
|
* If there are any extra actions defined in MultiFormStep->getExtraActions()
|
|
|
|
* then that set of actions is appended to the end of the actions FieldSet we
|
|
|
|
* have created in this method.
|
|
|
|
*
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param MultiFormStep $step Subclass of MultiFormStep
|
2017-09-06 22:47:35 +02:00
|
|
|
* @return FieldList of FormAction objects
|
|
|
|
*/
|
|
|
|
public function actionsFor($step)
|
|
|
|
{
|
|
|
|
// Create default multi step actions (next, prev), and merge with extra actions, if any
|
2017-09-07 01:32:55 +02:00
|
|
|
$actions = FieldList::create();
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
// If the form is at final step, create a submit button to perform final actions
|
|
|
|
// The last step doesn't have a next button, so add that action to any step that isn't the final one
|
|
|
|
if ($step->isFinalStep()) {
|
2017-09-07 01:32:55 +02:00
|
|
|
$actions->push(FormAction::create('finish', $step->getSubmitText()));
|
2017-09-06 22:47:35 +02:00
|
|
|
} else {
|
2017-09-07 01:32:55 +02:00
|
|
|
$actions->push(FormAction::create('next', $step->getNextText()));
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// If there is a previous step defined, add the back button
|
|
|
|
if ($step->getPreviousStep() && $step->canGoBack()) {
|
|
|
|
// If there is a next step, insert the action before the next action
|
|
|
|
if ($step->getNextStep()) {
|
2017-09-07 01:32:55 +02:00
|
|
|
$actions->insertBefore($prev = FormAction::create('prev', $step->getPrevText()), 'action_next');
|
2017-09-06 22:47:35 +02:00
|
|
|
// Assume that this is the last step, insert the action before the finish action
|
|
|
|
} else {
|
2017-09-07 01:32:55 +02:00
|
|
|
$actions->insertBefore($prev = FormAction::create('prev', $step->getPrevText()), 'action_finish');
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
//remove browser validation from prev action
|
|
|
|
$prev->setAttribute("formnovalidate", "formnovalidate");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge any extra action fields defined on the step
|
|
|
|
$actions->merge($step->getExtraActions());
|
|
|
|
|
|
|
|
return $actions;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a rendered version of this form, with a specific template.
|
|
|
|
* Looks through the step ancestory templates (MultiFormStep, current step
|
|
|
|
* subclass template) to see if one is available to render the form with. If
|
|
|
|
* any of those don't exist, look for a default Form template to render
|
|
|
|
* with instead.
|
|
|
|
*
|
|
|
|
* @return SSViewer object to render the template with
|
|
|
|
*/
|
|
|
|
public function forTemplate()
|
|
|
|
{
|
2017-09-07 01:32:55 +02:00
|
|
|
$return = $this->renderWith([
|
2017-09-06 22:47:35 +02:00
|
|
|
$this->getCurrentStep()->class,
|
|
|
|
'MultiFormStep',
|
|
|
|
$this->class,
|
|
|
|
'MultiForm',
|
|
|
|
'Form'
|
2017-09-07 01:32:55 +02:00
|
|
|
]);
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
$this->clearMessage();
|
|
|
|
|
|
|
|
return $return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This method saves the data on the final step, after submitting.
|
|
|
|
* It should always be overloaded with parent::finish($data, $form)
|
|
|
|
* so you can create your own functionality which handles saving
|
|
|
|
* of all the data collected through each step of the form.
|
|
|
|
*
|
|
|
|
* @param array $data The request data returned from the form
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param Form $form The form that the action was called on
|
|
|
|
* @return bool
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function finish($data, $form)
|
|
|
|
{
|
|
|
|
// Save the form data for the current step
|
|
|
|
$this->save($data);
|
|
|
|
|
|
|
|
if (!$this->getCurrentStep()->isFinalStep()) {
|
|
|
|
$this->controller->redirectBack();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$this->getCurrentStep()->validateStep($data, $form)) {
|
2017-09-08 03:57:33 +02:00
|
|
|
$this->getRequest()->getSession()->set("FormInfo.{$form->FormName()}.data", $form->getData());
|
2017-09-06 22:47:35 +02:00
|
|
|
$this->controller->redirectBack();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine what to do when the next action is called.
|
|
|
|
*
|
|
|
|
* Saves the current step session data to the database, creates the
|
|
|
|
* new step based on getNextStep() of the current step (or fetches
|
|
|
|
* an existing one), resets the current step to the next step,
|
|
|
|
* then redirects to the newly set step.
|
|
|
|
*
|
|
|
|
* @param array $data The request data returned from the form
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param Form $form The form that the action was called on
|
|
|
|
* @return bool
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function next($data, $form)
|
|
|
|
{
|
|
|
|
// Save the form data for the current step
|
|
|
|
$this->save($form->getData());
|
|
|
|
|
|
|
|
// Get the next step class
|
|
|
|
$nextStepClass = $this->getCurrentStep()->getNextStep();
|
|
|
|
|
|
|
|
if (!$nextStepClass) {
|
|
|
|
$this->controller->redirectBack();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform custom step validation (use MultiFormStep->getValidator() for
|
|
|
|
// built-in functionality). The data needs to be manually saved on error
|
|
|
|
// so the form is re-populated.
|
|
|
|
if (!$this->getCurrentStep()->validateStep($data, $form)) {
|
2017-09-08 03:57:33 +02:00
|
|
|
$this->getRequest()->getSession()->set("FormInfo.{$form->FormName()}.data", $form->getData());
|
2017-09-06 22:47:35 +02:00
|
|
|
$this->controller->redirectBack();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// validation succeeded so we reset it to remove errors and messages
|
2017-09-07 01:32:55 +02:00
|
|
|
$this->clearFormState();
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
// Determine whether we can use a step already in the DB, or have to create a new one
|
|
|
|
if (!$nextStep = DataObject::get_one($nextStepClass, "\"SessionID\" = {$this->session->ID}")) {
|
2017-09-07 01:32:55 +02:00
|
|
|
$nextStep = Injector::inst()->create($nextStepClass);
|
2017-09-06 22:47:35 +02:00
|
|
|
$nextStep->SessionID = $this->session->ID;
|
|
|
|
$nextStep->write();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the next step found as the current step
|
|
|
|
$this->setCurrentStep($nextStep);
|
|
|
|
|
|
|
|
// Redirect to the next step
|
|
|
|
$this->controller->redirect($nextStep->Link());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine what to do when the previous action is called.
|
|
|
|
*
|
|
|
|
* Retrieves the previous step class, finds the record for that
|
|
|
|
* class in the DB, and sets the current step to that step found.
|
|
|
|
* Finally, it redirects to that step.
|
|
|
|
*
|
|
|
|
* @param array $data The request data returned from the form
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param Form $form The form that the action was called on
|
|
|
|
* @return bool|HTTPResponse
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function prev($data, $form)
|
|
|
|
{
|
|
|
|
// Save the form data for the current step
|
|
|
|
$this->save($form->getData());
|
|
|
|
|
|
|
|
// Get the previous step class
|
|
|
|
$prevStepClass = $this->getCurrentStep()->getPreviousStep();
|
|
|
|
|
|
|
|
if (!$prevStepClass && !$this->getCurrentStep()->canGoBack()) {
|
|
|
|
$this->controller->redirectBack();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the previous step of the class instance returned from $currentStep->getPreviousStep()
|
|
|
|
$prevStep = DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->session->ID}");
|
|
|
|
|
|
|
|
// Set the current step as the previous step
|
|
|
|
$this->setCurrentStep($prevStep);
|
|
|
|
|
|
|
|
// Redirect to the previous step
|
2017-09-07 01:32:55 +02:00
|
|
|
return $this->controller->redirect($prevStep->Link());
|
2017-09-06 22:47:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the raw data given back from the form into session.
|
|
|
|
*
|
|
|
|
* Take the submitted form data for the current step, removing
|
|
|
|
* any key => value pairs that shouldn't be saved, then saves
|
|
|
|
* the data into the session.
|
|
|
|
*
|
|
|
|
* @param array $data An array of data to save
|
|
|
|
*/
|
|
|
|
protected function save($data)
|
|
|
|
{
|
|
|
|
$currentStep = $this->getCurrentStep();
|
|
|
|
if (is_array($data)) {
|
|
|
|
foreach ($data as $field => $value) {
|
2017-09-08 04:24:28 +02:00
|
|
|
if (in_array($field, $this->config()->get('ignored_fields'))) {
|
2017-09-06 22:47:35 +02:00
|
|
|
unset($data[$field]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$currentStep->saveData($data);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ############ Misc ############
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add the MultiFormSessionID variable to the URL on form submission.
|
|
|
|
* This is a means to persist the session, by adding it's identification
|
|
|
|
* to the URL, which ties it back to this MultiForm instance.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function FormAction()
|
|
|
|
{
|
|
|
|
$action = parent::FormAction();
|
|
|
|
$action .= (strpos($action, '?')) ? '&' : '?';
|
2017-09-08 03:57:33 +02:00
|
|
|
$action .= "{$this->getGetVar()}={$this->session->Hash}";
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
return $action;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the link to the page where the form is displayed. The user is
|
|
|
|
* redirected to this link with a session param after each step is
|
|
|
|
* submitted.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getDisplayLink()
|
|
|
|
{
|
|
|
|
return $this->displayLink ? $this->displayLink : Controller::curr()->Link();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the link to the page on which the form is displayed.
|
|
|
|
*
|
|
|
|
* The link defaults to the controllers current link. However if the form
|
|
|
|
* is displayed inside an action the display link must be explicitly set.
|
|
|
|
*
|
|
|
|
* @param string $link
|
|
|
|
*/
|
|
|
|
public function setDisplayLink($link)
|
|
|
|
{
|
|
|
|
$this->displayLink = $link;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the steps to show in a linear fashion, starting from the
|
|
|
|
* first step. We run {@link getAllStepsRecursive} passing the steps found
|
|
|
|
* by reference to get a listing of the steps.
|
|
|
|
*
|
2017-09-07 01:32:55 +02:00
|
|
|
* @return ArrayList of MultiFormStep instances
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
public function getAllStepsLinear()
|
|
|
|
{
|
2017-09-07 01:32:55 +02:00
|
|
|
$stepsFound = ArrayList::create();
|
2017-09-06 22:47:35 +02:00
|
|
|
|
2017-09-08 04:24:28 +02:00
|
|
|
$firstStep = DataObject::get_one($this->config()->get('start_step'), "\"SessionID\" = {$this->session->ID}");
|
2017-09-06 22:47:35 +02:00
|
|
|
$firstStep->LinkingMode = ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
|
|
|
|
$firstStep->setForm($this);
|
|
|
|
$stepsFound->push($firstStep);
|
|
|
|
|
|
|
|
// mark the further steps as non-completed if the first step is the current
|
|
|
|
if ($firstStep->ID == $this->getCurrentStep()->ID) {
|
|
|
|
$this->currentStepHasBeenFound = true;
|
|
|
|
} else {
|
|
|
|
$firstStep->addExtraClass('completed');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->getAllStepsRecursive($firstStep, $stepsFound);
|
|
|
|
|
|
|
|
return $stepsFound;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively run through steps using the getNextStep() method on each step
|
|
|
|
* to determine what the next step is, gathering each step along the way.
|
|
|
|
* We stop on the last step, and return the results.
|
|
|
|
* If a step in the chain was already saved to the database in the current
|
|
|
|
* session, its used - otherwise a singleton of this step is used.
|
|
|
|
* Caution: Doesn't consider branching for steps which aren't in the database yet.
|
|
|
|
*
|
2017-09-07 01:32:55 +02:00
|
|
|
* @param MultiFormStep $step Subclass of MultiFormStep to find the next step of
|
2017-09-06 22:47:35 +02:00
|
|
|
* @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on
|
2017-09-07 01:32:55 +02:00
|
|
|
* @return DataList of MultiFormStep instances
|
2017-09-06 22:47:35 +02:00
|
|
|
*/
|
|
|
|
protected function getAllStepsRecursive($step, &$stepsFound)
|
|
|
|
{
|
|
|
|
// Find the next step to the current step, the final step has no next step
|
|
|
|
if (!$step->isFinalStep()) {
|
|
|
|
if ($step->getNextStep()) {
|
|
|
|
// Is this step in the DB? If it is, we use that
|
|
|
|
$nextStep = $step->getNextStepFromDatabase();
|
|
|
|
if (!$nextStep) {
|
|
|
|
// If it's not in the DB, we use a singleton instance of it instead -
|
|
|
|
// - this step hasn't been accessed yet
|
|
|
|
$nextStep = singleton($step->getNextStep());
|
|
|
|
}
|
|
|
|
|
|
|
|
// once the current steps has been found we won't add the completed class anymore.
|
|
|
|
if ($nextStep->ID == $this->getCurrentStep()->ID) {
|
|
|
|
$this->currentStepHasBeenFound = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$nextStep->LinkingMode = ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
|
|
|
|
|
|
|
|
// add the completed class
|
|
|
|
if (!$this->currentStepHasBeenFound) {
|
|
|
|
$nextStep->addExtraClass('completed');
|
|
|
|
}
|
|
|
|
|
|
|
|
$nextStep->setForm($this);
|
|
|
|
|
|
|
|
// Add the array data, and do a callback
|
|
|
|
$stepsFound->push($nextStep);
|
|
|
|
$this->getAllStepsRecursive($nextStep, $stepsFound);
|
|
|
|
}
|
|
|
|
// Once we've reached the final step, we just return what we've collected
|
|
|
|
} else {
|
|
|
|
return $stepsFound;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of steps already completed (excluding currently started step).
|
|
|
|
* The way we determine a step is complete is to check if it has the Data
|
|
|
|
* field filled out with a serialized value, then we know that the user has
|
|
|
|
* clicked next on the given step, to proceed.
|
|
|
|
*
|
|
|
|
* @TODO Not sure if it's entirely appropriate to check if Data is set as a
|
|
|
|
* way to determine a step is "completed".
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getCompletedStepCount()
|
|
|
|
{
|
2017-09-08 03:57:33 +02:00
|
|
|
$steps = DataObject::get(MultiFormStep::class, "\"SessionID\" = {$this->session->ID} && \"Data\" IS NOT NULL");
|
2017-09-06 22:47:35 +02:00
|
|
|
|
|
|
|
return $steps ? $steps->Count() : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Total number of steps in the shortest path (only counting straight path without any branching)
|
|
|
|
* The way we determine this is to check if each step has a next_step string variable set. If it's
|
|
|
|
* anything else (like an array, for defining multiple branches) then it gets counted as a single step.
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getTotalStepCount()
|
|
|
|
{
|
|
|
|
return $this->getAllStepsLinear() ? $this->getAllStepsLinear()->Count() : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Percentage of steps completed (excluding currently started step)
|
|
|
|
*
|
|
|
|
* @return float
|
|
|
|
*/
|
|
|
|
public function getCompletedPercent()
|
|
|
|
{
|
|
|
|
return (float) $this->getCompletedStepCount() * 100 / $this->getTotalStepCount();
|
|
|
|
}
|
2008-10-01 20:36:52 +02:00
|
|
|
}
|