diff --git a/.gitignore b/.gitignore index 4df8f53..cd97ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ *~ -.DS_Store \ No newline at end of file +.DS_Store +/.buildpath +/.project +/.settings/ +/vendor/ +/composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d9998ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,89 @@ +# Thanks cakephp/debug_kit for this file. +language: php + +php: +# - 5.5 + - 5.6 +# - 7.0 +# - 7.1 + +env: +# - CAKE_VERSION=2.8.0 DB=mysql + - CAKE_VERSION=2.9.9 DB=mysql + +install: + - git clone git://github.com/cakephp/cakephp ../cakephp && cd ../cakephp && git checkout $CAKE_VERSION + - cp -R ../cakephp-wizard plugins/Wizard + - chmod -R 777 ../cakephp/app/tmp + - sh -c "composer global require 'phpunit/phpunit=3.7.33'" + - sh -c "ln -s ~/.composer/vendor/phpunit/phpunit/PHPUnit ../cakephp/vendors/PHPUnit" + - sh -c "composer global require 'cakephp/cakephp-codesniffer:1.*'" + - sh -c "~/.composer/vendor/bin/phpcs --config-set installed_paths ~/.composer/vendor/cakephp/cakephp-codesniffer" + +before_script: + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE cakephp_test;'; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi" + - set +H + - echo " array( + 'datasource' => 'Database/Mysql', + 'host' => '0.0.0.0', + 'login' => 'travis' + ), + 'pgsql' => array( + 'datasource' => 'Database/Postgres', + 'host' => '127.0.0.1', + 'login' => 'postgres', + 'database' => 'cakephp_test', + 'schema' => array( + 'default' => 'public', + 'test' => 'public' + ) + ) + ); + public \$default = array( + 'persistent' => false, + 'host' => '', + 'login' => '', + 'password' => '', + 'database' => 'cakephp_test', + 'prefix' => '' + ); + public \$test = array( + 'persistent' => false, + 'host' => '', + 'login' => '', + 'password' => '', + 'database' => 'cakephp_test', + 'prefix' => '' + ); + public function __construct() { + \$db = 'mysql'; + if (!empty(\$_SERVER['DB'])) { + \$db = \$_SERVER['DB']; + } + foreach (array('default', 'test') as \$source) { + \$config = array_merge(\$this->{\$source}, \$this->identities[\$db]); + if (is_array(\$config['database'])) { + \$config['database'] = \$config['database'][\$source]; + } + if (!empty(\$config['schema']) && is_array(\$config['schema'])) { + \$config['schema'] = \$config['schema'][\$source]; + } + \$this->{\$source} = \$config; + } + } + }" > ../cakephp/app/Config/database.php + +script: +# Temporary disable code style until we have enough unit tests to fix code style errors. + - ~/.composer/vendor/bin/phpcs --standard=CakePHP ./plugins/Wizard --ignore=*/vendor/* -p + - ./lib/Cake/Console/cake test Wizard AllWizard --configuration=./plugins/Wizard/phpunit-clover.xml --stderr + +after_success: + - cd ./plugins/Wizard && bash <(curl -s https://codecov.io/bash) + +notifications: + email: false diff --git a/Controller/Component/WizardComponent.php b/Controller/Component/WizardComponent.php new file mode 100644 index 0000000..a232bd3 --- /dev/null +++ b/Controller/Component/WizardComponent.php @@ -0,0 +1,722 @@ + + * $steps = array('contact', 'payment', 'confirm'); + * + * + * The $steps array can also contain nested steps arrays of the same format but + * must be wrapped by a branch group. + * Plot-branched example: + * + * $steps = array( + * 'job_application', + * array( + * 'degree' => array('college', 'degree_type'), + * 'nodegree' => 'experience' + * ), + * 'confirm', + * ); + * + * + * The 'branchnames' (ie 'degree', 'nodegree') are arbitrary but used as selectors + * for the branch() and unbranch() methods. Branches can point to either another + * steps array or a single step. The first branch in a group that hasn't been + * skipped (see branch()) is included by default (if $defaultBranch = true). + * + * @var array + * @access public + */ + public $steps = array(); + +/** + * Controller action that processes your step. + * + * @var string + * @access public + */ + public $action = 'wizard'; + +/** + * Url to be redirected to after the wizard has been completed. + * Controller::afterComplete() is called directly before redirection. + * + * @var mixed + * @access public + */ + public $completeUrl = '/'; + +/** + * Url to be redirected to after 'Cancel' submit button has been pressed by user. + * + * @var mixed + * @access public + */ + public $cancelUrl = '/'; + +/** + * Url to be redirected to after 'Draft' submit button has been pressed by user. + * + * @var mixed + * @access public + */ + public $draftUrl = '/'; + +/** + * If `true` then URL parameters from the first step will be present in the URLs + * of all other steps. + * + * @var bool + */ + public $persistUrlParams = false; + +/** + * If true, the first "non-skipped" branch in a group will be used if a branch has + * not been included specifically. + * + * @var bool + * @access public + */ + public $defaultBranch = true; + +/** + * If true, the user will not be allowed to edit previously completed steps. They will be + * "locked down" to the current step. The opposite of $roaming. + * + * @var bool + * @access public + */ + public $lockdown = false; + +/** + * If true, the user will be allowed navigate to any steps. The opposite of $lockdown. + * + * @var bool + * @access public + */ + public $roaming = false; + +/** + * If true, the component will render views found in views/{wizardAction}/{step}.ctp rather + * than views/{step}.ctp. + * + * @var bool + * @access public + */ + public $nestedViews = false; + +/** + * Holds the root of the session key for data storage. + * + * @var string + */ + public $sessionRootKey = 'Wizard'; + +/** + * Other components used. + * + * @var array + * @access public + */ + public $components = array('Session'); + +/** + * Internal step tracking. + * + * @var string + * @access protected + */ + protected $_currentStep = null; + +/** + * Holds the session key for data storage. + * + * @var string + * @access protected + */ + protected $_sessionKey = null; + +/** + * Other session keys used. + * + * @var string + * @access protected + */ + protected $_configKey = null; + + protected $_branchKey = null; + +/** + * Holds the array based url for redirecting. + * + * @var array + * @access protected + */ + protected $_wizardUrl = array(); + +/** + * Holds the array with steps and branches from the initial Wizard configuration. + * + * @var array + */ + protected $_stepsAndBranches = array(); + +/** + * Initializes WizardComponent for use in the controller + * + * @param \Controller|object $controller A reference to the instantiating controller object + * + * @access public + * @return void + */ + public function initialize(Controller $controller) { + $this->controller = $controller; + $this->__setSessionKeys(); + $this->_stepsAndBranches = $this->steps; + } + +/** + * Sets session keys used by this component. + * + * @return void + */ + private function __setSessionKeys() { + if ($this->controller->Session->check($this->sessionRootKey . '.complete')) { + $this->_sessionKey = $this->sessionRootKey . '.complete'; + } else { + $this->_sessionKey = $this->sessionRootKey . '.' . $this->controller->name; + } + $this->_configKey = $this->sessionRootKey . '.config'; + $this->_branchKey = $this->sessionRootKey . '.branches.' . $this->controller->name; + } + +/** + * Component startup method. + * Called after the Controller::beforeFilter() and before the controller action + * + * @param \Controller|object $controller A reference to the instantiating controller object + * + * @access public + * @return void + */ + public function startup(Controller $controller) { + $this->__setSessionKeys(); + $this->config('action', $this->action); + $this->_configSteps($this->steps); + if (!in_array('Wizard.Wizard', $this->controller->helpers) && !array_key_exists('Wizard.Wizard', $this->controller->helpers)) { + $this->controller->helpers['Wizard.Wizard'] = array( + 'sessionRootKey' => $this->sessionRootKey, + ); + } + } + +/** + * Parses the steps array by stripping off nested arrays not included in the branches + * and writes a simple array with the correct steps to session. + * + * @param array $steps Array to be parsed for nested arrays. + * @return void + */ + protected function _configSteps($steps) { + $this->steps = $this->_parseSteps($steps); + $this->config('steps', $this->steps); + } + +/** + * Parses the steps array by stripping off nested arrays not included in the branches + * and returns a simple array with the correct steps. + * + * @param array $steps Array to be parsed for nested arrays and returned as simple array. + * + * @return array + * @access protected + */ + protected function _parseSteps($steps) { + $parsed = array(); + foreach ($steps as $key => $name) { + if (is_array($name)) { + foreach ($name as $branchName => $step) { + $branchType = $this->_branchType($branchName); + if ($branchType) { + if ($branchType !== 'skip') { + $branch = $branchName; + } + } elseif (empty($branch) && $this->defaultBranch) { + $branch = $branchName; + } + } + if (!empty($branch)) { + if (is_array($name[$branch])) { + $parsed = array_merge($parsed, $this->_parseSteps($name[$branch])); + } else { + $parsed[] = $name[$branch]; + } + } + unset($branch); + } else { + $parsed[] = $name; + } + } + return $parsed; + } + +/** + * Saves configuration details for use in WizardHelper. + * + * @param string $branch branch key. + * + * @return mixed + * @access protected + */ + protected function _branchType($branch) { + if ($this->controller->Session->check("$this->_branchKey.$branch")) { + return $this->controller->Session->read("$this->_branchKey.$branch"); + } + return false; + } + +/** + * Saves configuration details for use in WizardHelper or returns a config value. + * This is method usually handled only by the component. + * + * @param string $name Name of configuration variable. + * @param mixed $value Value to be stored. + * + * @return mixed + * @access public + */ + public function config($name, $value = null) { + if ($value == null) { + return $this->controller->Session->read("$this->_configKey.$name"); + } + $this->controller->Session->write("$this->_configKey.$name", $value); + return $value; + } + +/** + * Main Component method. + * + * @param string $step Name of step associated in $this->steps to be processed. + * + * @throws NotImplementedException + * @return bool|\CakeResponse + * @access public + */ + public function process($step) { + if (isset($this->controller->request->data['Cancel'])) { + if (method_exists($this->controller, 'beforeCancel')) { + $this->controller->beforeCancel($this->_getExpectedStep()); + } + $this->reset(); + return $this->controller->redirect($this->cancelUrl); + } + if (isset($this->controller->request->data['Draft'])) { + if (method_exists($this->controller, 'saveDraft')) { + $draft = array( + '_draft' => array( + 'current' => array( + 'step' => $step, + 'data' => $this->controller->request->data + ) + ) + ); + $this->controller->saveDraft(array_merge_recursive((array)$this->read(), $draft)); + } + $this->reset(); + return $this->controller->redirect($this->draftUrl); + } + if (empty($step)) { + if ($this->controller->Session->check($this->sessionRootKey . '.complete')) { + if (method_exists($this->controller, 'afterComplete')) { + $this->controller->afterComplete(); + } + $this->reset(); + return $this->controller->redirect($this->completeUrl); + } + $this->autoReset = false; + } elseif ($step == 'reset') { + if (!$this->lockdown) { + $this->reset(); + } + } else { + if ($this->_validStep($step)) { + $this->_setCurrentStep($step); + if (!empty($this->controller->request->data) && !isset($this->controller->request->data['Previous'])) { + $processCallback = Inflector::variable('process_' . $this->_currentStep); + if (method_exists($this->controller, $processCallback)) { + $proceed = $this->controller->$processCallback(); + if (!is_bool($proceed)) { + throw new NotImplementedException(sprintf(__('Process Callback Controller::%s should return boolean', $processCallback))); + } + } elseif ($this->autoValidate) { + $proceed = $this->_validateData(); + } else { + throw new NotImplementedException(sprintf(__('Process Callback not found. Please create Controller::%s', $processCallback))); + } + if ($proceed) { + $this->save(); + if (isset($this->controller->request->data['SaveAndBack']) && prev($this->steps)) { + return $this->redirect(current($this->steps)); + } + if (next($this->steps)) { + if ($this->autoAdvance) { + return $this->redirect(); + } + return $this->redirect(current($this->steps)); + } else { + $this->controller->Session->write($this->sessionRootKey . '.complete', $this->read()); + $this->reset(); + return $this->controller->redirect(array('action' => $this->action)); + } + } + } elseif (isset($this->controller->request->data['Previous']) && prev($this->steps)) { + return $this->redirect(current($this->steps)); + } elseif ($this->controller->Session->check("$this->_sessionKey._draft.current")) { + $this->controller->request->data = $this->read('_draft.current.data'); + $this->controller->Session->delete("$this->_sessionKey._draft.current"); + } elseif ($this->controller->Session->check("$this->_sessionKey.$this->_currentStep")) { + $this->controller->request->data = $this->read($this->_currentStep); + } + $prepareCallback = Inflector::variable('prepare_' . $this->_currentStep); + if (method_exists($this->controller, $prepareCallback)) { + $this->controller->$prepareCallback(); + } + $this->config('activeStep', $this->_currentStep); + if ($this->nestedViews) { + $this->controller->viewPath .= '/' . $this->action; + } + if ($this->controller->autoRender) { + return $this->controller->render($this->_currentStep); + } + return true; + } else { + return $this->redirect(); + } + } + if ($step != 'reset' && $this->autoReset) { + $this->reset(); + } + return $this->redirect(); + } + +/** + * Finds the first incomplete step (i.e. step data not saved in Session). + * + * @return string $step or false if complete + * @access protected + */ + protected function _getExpectedStep() { + foreach ($this->steps as $step) { + if (!$this->controller->Session->check("$this->_sessionKey.$step")) { + $this->config('expectedStep', $step); + return $step; + } + } + return false; + } + +/** + * Resets the wizard by deleting the wizard session. + * + * @access public + * @return void + */ + public function reset() { + $this->controller->Session->delete($this->_branchKey); + $this->controller->Session->delete($this->_sessionKey); + } + +/** + * Get the data from the Session that has been stored by the WizardComponent. + * + * @param string $key step key. + * + * @internal param mixed $name The name of the session variable (or a path as sent to Set.extract) + * + * @return mixed The value of the session variable + * @access public + */ + public function read($key = null) { + if ($key == null) { + return $this->controller->Session->read($this->_sessionKey); + } else { + $wizardData = $this->controller->Session->read("$this->_sessionKey.$key"); + if (!empty($wizardData)) { + return $wizardData; + } + return null; + } + } + +/** + * Validates the $step four ways: + * 1. Explicitly only validate step that exists in $this->steps array. + * 2. If $roaming option is true any steps within $this->steps is valid + * 3. If $lockdown option is true only the next/current step is valid. + * 4. If $roaming and $lockdown is false validate the step either before or exactly the expected step. + * + * @param string $step Step to validate. + * + * @return mixed + * @access protected + */ + protected function _validStep($step) { + if (in_array($step, $this->steps)) { + if ($this->roaming) { + return true; + } elseif ($this->lockdown) { + return (array_search($step, $this->steps) == array_search($this->_getExpectedStep(), $this->steps)); + } + return (array_search($step, $this->steps) <= array_search($this->_getExpectedStep(), $this->steps)); + } + return false; + } + +/** + * Moves internal array pointer of $this->steps to $step and sets $this->_currentStep. + * + * @param string $step Step to point to. + * + * @access protected + * @return void + */ + protected function _setCurrentStep($step) { + if (!in_array($step, $this->steps)) { + return; + } + $this->_currentStep = reset($this->steps); + while (current($this->steps) != $step) { + $this->_currentStep = next($this->steps); + } + } + +/** + * Validates controller data with the correct model if the model is included in + * the controller's uses array. This only occurs if $autoValidate = true and there + * is no processCallback in the controller for the current step. + * + * @return bool + * @access protected + */ + protected function _validateData() { + $controller =& $this->controller; + foreach ($controller->request->data as $model => $data) { + if (in_array($model, $controller->uses)) { + $controller->{$model}->set($data); + if (!$controller->{$model}->validates()) { + return false; + } + } + } + return true; + } + +/** + * Saves the data from the current step into the Session. + * + * Please note: This is normally called automatically by the component after + * a successful processCallback, but can be called directly for advanced navigation purposes. + * + * @param string $step step key. + * @param array $data step details. + * @access public + * @return void + */ + public function save($step = null, $data = null) { + if (is_null($step)) { + $step = $this->_currentStep; + } + if (is_null($data)) { + $data = $this->controller->request->data; + } + $this->controller->Session->write("$this->_sessionKey.$step", $data); + $this->_getExpectedStep(); + $this->_setCurrentStep($step); + } + +/** + * Handles Wizard redirection. A null url will redirect to the "expected" step. + * + * @param string $step Stepname to be redirected to. + * @param int $status Optional HTTP status code (eg: 404) + * @param bool $exit If true, exit() will be called after the redirect + * + * @see Controller::redirect() + * @access public + * @return void + */ + public function redirect($step = null, $status = null, $exit = true) { + if ($step == null) { + $step = $this->_getExpectedStep(); + } + if ($this->persistUrlParams) { + $url = Router::reverseToArray($this->controller->request); + $url['action'] = $this->action; + $url[0] = $step; + } else { + $url = array( + 'controller' => Inflector::underscore($this->controller->name), + 'action' => $this->action, + $step, + ); + } + return $this->controller->redirect($url, $status, $exit); + } + +/** + * Selects a branch to be used in the steps array. The first branch in a group + * is included by default. + * + * @param string $name Branch name to be included in steps. + * @param bool $skip Branch will be skipped instead of included if true. + * + * @access public + * @return void + */ + public function branch($name, $skip = false) { + $branches = array(); + if ($this->controller->Session->check($this->_branchKey)) { + $branches = $this->controller->Session->read($this->_branchKey); + } + if ($skip) { + $value = 'skip'; + } else { + $value = 'branch'; + } + $branches[$name] = $value; + $this->controller->Session->write($this->_branchKey, $branches); + $this->_configSteps($this->_stepsAndBranches); + } + +/** + * Loads previous draft session. + * + * @param array $draft Session data of same format passed to Controller::saveDraft() + * + * @see WizardComponent::process() + * @access public + * @return void + */ + public function loadDraft($draft = array()) { + if (!empty($draft['_draft']['current']['step'])) { + $this->restore($draft); + return $this->redirect($draft['_draft']['current']['step']); + } + return $this->redirect(); + } + +/** + * Sets data into controller's wizard session. Particularly useful if the data + * originated from WizardComponent::read() as this will restore a previous session. + * + * @param array $data Data to be written to controller's wizard session. + * + * @access public + * @return void + */ + public function restore($data = array()) { + $this->controller->Session->write($this->_sessionKey, $data); + } + +/** + * Resets the wizard by deleting the wizard session. + * + * @access public + * @return void + */ + public function resetWizard() { + $this->reset(); + } + +/** + * Resets the data from the Session that has been stored by the WizardComponent. + * + * @param string $key step key. + * + * @internal param mixed $name The name of the session variable (or a path as sent to Set.extract) + * + * @access public + * @return void + */ + public function delete($key = null) { + if ($key == null) { + return; + } else { + $this->controller->Session->delete("$this->_sessionKey.$key"); + return; + } + } + +/** + * Removes a branch from the steps array. + * + * @param string $branch Name of branch to be removed from steps array. + * + * @access public + * @return void + */ + public function unbranch($branch) { + $this->controller->Session->delete("$this->_branchKey.$branch"); + $this->_configSteps($this->_stepsAndBranches); + } + +} diff --git a/README.md b/README.md index 43d3dd0..8e4cd36 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,8 @@ The Wizard plugin for CakePHP automates several aspects of multi-page forms incl * Clone/Copy the files in this directory into `app/plugins/wizard` * Include the wizard component in your controller: - * `var $components = array('Wizard.Wizard');` + * `public $components = array('Wizard.Wizard');` ## Documentation Detailed documentation, including usage examples, can be found in the [GitHub wiki](http://github.com/jaredhoyt/cakephp-wizard/wiki). - -## Reporting issues - -If you have any issues with this plugin, please open a ticket on [Lighthouse](http://jaredhoyt.lighthouseapp.com/projects/60073-cakephp-wizard). diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..0b113ba --- /dev/null +++ b/README2.md @@ -0,0 +1,89 @@ +# Step 2: View Preparation and Data Processing + +Next we are going to setup our controller to handle each of the steps in the form wizard. + +*Very important:* Rather than creating a separate controller action for each of the steps in the form, all the steps are tied together through one action (the default is 'wizard'). This means, for our example, our urls will look like example.com/signup/wizard/account etc. This way, everything is handle by the component and customization is handled through controller callbacks. + +Because of this, the wizard action itself can be very basic. It merely needs to pass the step requested to the component's main method - process(): + +### Controller Class: + +
<?php 
+class SignupController extends AppController {
+	public $components = array('Wizard');
+
+	public function beforeFilter() {
+		$this->Wizard->steps = array('account', 'address', 'billing', 'review');
+	}
+
+	public function wizard($step = null) {
+		$this->Wizard->process($step);
+	}
+}
+?>
+ +Something to consider if your wizard is the controller's main feature (as it would be in our example), is to route the default action for the controller to the wizard action. This would allow prettier links such as example.com/signup to be handled by SignupController::wizard(), which would then redirect to /signup/wizard/account (or the first incomplete step in the wizard). + +
Router::connect('/signup', array('controller' => 'signup', 'action' => 'wizard'));
+ +Next, we are going to create controller callbacks to handle each step. Each step has two controller callbacks: prepare and process. + +The prepare callback is *optional* and occurs before the step's view is loaded. This is a good place to set any data or variables that you want available for the view. The name of the callback is prepareStepName. So for our example, our prepare callbacks would be prepareAccount(), prepareAddress(), etc. + +The process callback is *required* and occurs after data has been posted. This is where data validation should be handled. The process callback must return either true or false. If true, the wizard will continue to the next step; if false, the user will remain on the step and any validation errors will be presented. The name of the callback is processStepName. So for our example, our process callbacks would be processAccount(), processAddress(), etc. _You do not have to worry about retaining data as this is handled automatically by the component. Data retrieval will be discussed later in the tutorial._ + + +It's very important to note that every step in the wizard must contain a form with a field. The only way for the wizard to continue to the next step is for the process callback to return true. And the process callback is only called if $this->data is not empty. + +So lets create some basic process callbacks. Real world examples would most likely be more complicated, but this should give you the basic idea (don't forget to add any needed models): + +### Controller Class: + +
<?php 
+class SignupController extends AppController {
+	public $uses = array('Client', 'User', 'Billing');
+	public $components = array('Wizard');
+
+	public function beforeFilter() {
+		$this->Wizard->steps = array('account', 'address', 'billing', 'review');
+	}
+
+	public function wizard($step = null) {
+		$this->Wizard->process($step);
+	}
+/**
+ * [Wizard Process Callbacks]
+ */
+	public function processAccount() {
+		$this->Client->set($this->data);
+		$this->User->set($this->data);
+
+		if($this->Client->validates() && $this->User->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processAddress() {
+		$this->Client->set($this->data);
+
+		if($this->Client->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processBilling() {
+		$this->Billing->set($this->data);
+
+		if($this->Billing->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processReview() {
+		return true;
+	}
+}
+?>
\ No newline at end of file diff --git a/README3.md b/README3.md new file mode 100644 index 0000000..278f748 --- /dev/null +++ b/README3.md @@ -0,0 +1,101 @@ +# Step 3: Data Retrieval and Wizard Completion + +At this point in the tutorial, your wizard should have of four steps - each consisting of a view and process callback (plus any optional prepare callbacks). Also, the wizard should be automatically handling data persistence and navigation between the steps. The next question is how to retrieve the data stored by the component and what happens at the completion of the wizard. + +## Data Retrieval + +Retrieving data from the component is possible at any point in the wizard. While our example will not manipulate or store the data permanently until the completion of the wizard, it's also reasonable that some applications may need to store data before the end of the wizard. For example, a job application may not be completed in one session but rather over a period of time. The progress, then, would need to be kept up with between sessions, rather than manipulated/stored all at once during the wizard completion. + +Wizard data is stored with the following path: sessionKey.stepName.modelName.fieldName. The sessionKey will be explained in the Wizard Completion section below. The component method for retrieving data is read($key = null) which works pretty much like SessionComponent::read() except that the sessionKey is handled automatically by the WizardComponent and doesn't need to be passed into read(). Passing null into read() returns all Wizard data. + +So, for example, if we wanted to do something with the client's email address (which was obtained in the account step) while processing the review step, we would use the following code: + +
public function processReview() {
+	$email = $this->Wizard->read('account.User.email');
+	/* do something with the $email here */
+
+	return true;
+}
+ +An example showing how to retrieve all the current data with read() will be given below. + +## Wizard Completion + +One of my goals when writing this component was to prevent double submission of user data. One of the ways I accomplished this was by using the process callbacks for each step and redirecting to rather than rendering the next step. + +The second way was including an extra redirect and callback during the wizard completion process that creates a sort of "no man's land" for the wizard data. The way this works is, after the process callback for the last step is completed, the wizard data is moved to a new location in the session (Wizard.complete), the wizard redirects to a null step and another callback is called: afterComplete(). + +afterComplete() is an optional callback and is the ideal place to manipulate/store data after the wizard has been completed by the user. The callback does not need to return anything and the component automatically redirects to the $completeUrl (default '/') after the callback is finished. + +It's important to note that immediately after the afterComplete() callback and before the user is redirected to $completeUrl, the wizard is reset completely (all data is flushed from the session). If you need to redirect manually from afterComplete(), be sure to call Wizard->reset() manually. + +So, to complete our tutorial example, we will pull all the data out of the wizard, store it in our database, and redirect the user to a confirmation page. + +### Controller Class: + +
Wizard->steps = array('account', 'address', 'billing', 'review');
+		$this->Wizard->completeUrl = '/signup/confirm';
+	}
+
+	public function confirm() {
+	}
+
+	public function wizard($step = null) {
+		$this->Wizard->process($step);
+	}
+/**
+ * [Wizard Process Callbacks]
+ */
+	public function processAccount() {
+		$this->Client->set($this->data);
+		$this->User->set($this->data);
+
+		if($this->Client->validates() && $this->User->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processAddress() {
+		$this->Client->set($this->data);
+
+		if($this->Client->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processBilling() {
+		$this->Billing->set($this->data);
+
+		if($this->Billing->validates()) {
+			return true;
+		}
+		return false;
+	}
+
+	public function processReview() {
+		return true;
+	}
+/**
+ * [Wizard Completion Callback]
+ */
+	protected function afterComplete() {
+		$wizardData = $this->Wizard->read();
+		extract($wizardData);
+
+		$this->Client->save($account['Client'], false, array('first_name', 'last_name', 'phone'));
+		$this->User->save($account['User'], false, array('email', 'password'));
+		
+		... etc ...
+	}
+}
+?>
+ +Please note the addition to beforeFilter() and the new confirm() method. You would also need to create a view file (confirm.ctp) with something like "Congrats, your sign-up was successful!" etc. It would also be good to create some sort of token during the afterComplete() callback and have it checked for in the confirm() method, but that's outside the scope of this tutorial. \ No newline at end of file diff --git a/README4.md b/README4.md new file mode 100644 index 0000000..a1362b8 --- /dev/null +++ b/README4.md @@ -0,0 +1,29 @@ +# Step 4: Plot-Branching Navigation + +A new addition to the WizardComponent 1.2 is *plot-branching navigation* (pbn). If you ever read a book as a child in which you interacted with the plot - i.e. If the knight slays the dragon, turn to page 64, if the knight runs for safety, turn to page 82. - then you've experienced pbn. In some applications, the steps in a wizard may not be a simple linear path, but might instead require the ability to "change course" based on user input. + +For example, a survey that has varying questions for men or women might ask gender on the first page and would then need to navigate to different pages depending on the answer. While this is a simple example, some wizards can become very complicated when all the different options occur at different points in the wizard and "paths" begin to cross. + +In some instances, it may not be a different path altogether, but merely a step being skipped over. Integrating Paypal Pro, for instance, requires the application allow the user to either enter their billing information on the site, or hop over to Paypal, login to their account and "skip" the billing page on the original site. + +## Advanced $steps Array + +When using pbn, the $steps array becomes a bit more complex. Instead of adding/removing steps on the fly, all the steps are included into the array like they normally would. Then, "branches" are selected or skipped using the component methods. The trick to understanding the WizardComponent's pbn implementation is understanding the $steps array - the rest is pretty simple. + +A simple $steps array is a single-tiered structure with each element corresponding to a step in the wizard. The array is ordered and the steps are handled sequentially. + +An advanced $steps array setup for pbn is a multi-tiered structure consisting of simple $steps arrays separated by branch arrays (or branch groups). The branch arrays are associative arrays with branch names as indexes and simple $steps arrays as elements. + +For example, lets say we had six steps: step1, step2, gender, step3, step4, and step5. The gender step would determine the user's gender and the subsequent steps would vary accordingly. If male, step3 and step4 would be used; if female, step4 and step5 would be used. So lets setup our $steps array: + +
public function beforeFilter() {
+	$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3', 'step4'), 'female' => array('step4', 'step5')));
+}
+ +It's important to understand that there is almost always more than one way to accomplish the same effect with different $steps arrays. For example, I could have instead, setup a 'male' branch that used step3, included step4 for both, and then another branch for 'female' that would include step5. + +
public function beforeFilter() {
+	$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3')), 'step4', array('female' => array('step5')));
+}
+ +Also, although these examples are simple, I should point out that the $steps array is not limited to a three-tiered array. As long as the pattern is followed - array(stepName, array(branchName => array(stepName, etc...))) - the steps array can be as complex as resources allow for. \ No newline at end of file diff --git a/README5.md b/README5.md new file mode 100644 index 0000000..11e2d6a --- /dev/null +++ b/README5.md @@ -0,0 +1,47 @@ +# Step 5: PBN Component Methods + +After the the $steps array is setup, the question becomes, "How does the component navigate through all the branches?" This is done be selecting which branch will be used in a "branch group". By default, the first branch in a group is always used (unless it has been "skipped" - more on that later). You can turn this feature off by setting Wizard->defaultBranch = false. + +So, lets look at our two previous examples: + +
*Example 1:*
+$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3', 'step4'), 'female' => array('step4', 'step5')));
+
+*Example 2:*
+$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3')), 'step4', array('female' => array('step5')));
+ +In example 1, 'male' and 'female' are two branches in the same branch group. Therefore, without any interference, the component would automatically use the 'male' branch and 'female' would be skipped. The steps would occur: step1, step2, gender, step3, step4. If $defaultBranch = false, both would be skipped and the steps would occur: step1, step2, gender. + +In example 2, 'male' and 'female' are in separate branch groups. Therefore, without any interference, both branches would be used since they are the first branch in their respective groups. The steps would occur: step1, step2, gender, step3, step4, step5. If $defaultBranch = false, both would be skipped and the steps would occur: step1, step2, gender, step4. + +## branch() and unbranch() + +In order to specify to the component which branches should be used, you must use the branch() and unbranch() methods. The branch() method includes a branch (specified by its name) in the session and unbranch() removes a branch from the session. branch() also has an extra parameter that allows branches to be easily skipped - more on that below. + +So lets assume "female" was selected on the gender step. During the "processGender" callback, we could specify the "female" branch to be included: + +
public  function processGender() {
+	$this->Client->set($this->data);
+
+	if($this->Client->validates()) {
+		if($this->data['Client']['gender'] == 'female') {
+			 $this->Wizard->branch('female');
+		} else {
+			 $this->Wizard->branch('male');
+		}
+		return true;
+	}
+	return false;
+}
+ +In example 1, the 'female' branch would be used instead of the 'male' branch and the steps would occur: step1, step2, gender, step4, step5. However, in example 2, unless $defaultBranch = false, the 'male' branch would also be used since it is not in the same branch group as 'female'. + +Important: The first branch that has been included in the session will be used. In other words, if you were to do branch('male') and branch('female') for example 1, 'male' would be used since it occurs before 'female'. If 'male' was branched previously and you later wanted 'female' to be used, you would need to use unbranch('male'). + +In addition to including a branch to be used, branch() can also specify branches to be "skipped" by setting the second parameter to 'true'. If, for example, we used Wizard->branch('male', true) in the previous examples, 'male' would be skipped and 'female' would be used. The steps would occur: step1, step2, gender, step4, step5 - the same as using branch('female') with $defaultBranch = true! + +The last thing I want to mention about pbn is that branch names do not necessarily have to be unique. In fact, I'd imagine some complex pbn wizards could be solved with some creative branch naming schemes in which identical branch names would be used only one branch() would have to be called to alter multiple branch groups. For example, using branch('male') with the following $steps array would select the 'male' branches in both the first and second branch groups. + +
$steps = array('step1', array('male' => ..., 'female' => ...), 'step2', array('cyborg' => ..., 'male' => ..., 'alien' => ...)); 
+ +Also, (the other last thing I want to mention), the $steps array that each branch name points to can be treated exactly the same as the main $steps array - i.e. branch groups can be nested and branches are selected with branch() and $defaultBranch. \ No newline at end of file diff --git a/Test/Case/AllWizardTest.php b/Test/Case/AllWizardTest.php new file mode 100644 index 0000000..3813712 --- /dev/null +++ b/Test/Case/AllWizardTest.php @@ -0,0 +1,10 @@ +addTestDirectoryRecursive(dirname(__FILE__) . DS . 'Controller'); + $suite->addTestDirectoryRecursive(dirname(__FILE__) . DS . 'View'); + return $suite; + } +} diff --git a/Test/Case/Controller/Component/WizardComponentTest.php b/Test/Case/Controller/Component/WizardComponentTest.php new file mode 100644 index 0000000..279d831 --- /dev/null +++ b/Test/Case/Controller/Component/WizardComponentTest.php @@ -0,0 +1,710 @@ + array( + 'inList' => array( + 'rule' => array('inList', array('male', 'female')), + ), + ), + ); + +} + +/** + * AuthTestController class + * + * @property WizardComponent $Wizard + * @package Wizard.Test.Case.Controller.Component + */ +class WizardTestController extends Controller { + + public $autoRender = false; + + public $uses = array('WizardUserMock'); + + public $components = array( + 'Session', + 'Wizard.Wizard' => array( + 'autoValidate' => true, + 'completeUrl' => array( + 'action' => 'wizard', + 'step1', + ), + 'steps' => array( + 'step1', + 'step2', + 'gender', // This step is autovalidated. + array( + 'male' => array('step3', 'step4'), + 'female' => array('step4', 'step5'), + 'unknown' => 'step6', + ), + 'confirmation', + ), + ), + ); + + public function wizard($step = null) { + $this->Wizard->process($step); + } + + public function processStep1() { + if (!empty($this->request->data)) { + return true; + } + return false; + } + + public function processStep2() { + if (!empty($this->request->data)) { + return true; + } + return false; + } + + public function processGender() { + if (!empty($this->request->data)) { + if ($this->Wizard->defaultBranch === false) { + if ($this->request->data['WizardUserMock']['gender'] == 'female') { + $this->Wizard->unbranch('male'); + $this->Wizard->branch('female'); + } else { + $this->Wizard->unbranch('female'); + $this->Wizard->branch('male'); + } + } + return true; + } + return false; + } + + public function processStep3() { + if (!empty($this->request->data)) { + return true; + } + return false; + } + + public function processStep4() { + if (!empty($this->request->data)) { + return true; + } + return false; + } + + public function processStep5() { + if (!empty($this->request->data)) { + return true; + } + return false; + } + + public function processConfirmation() { + return true; + } + + public function afterComplete() { + } + + public function redirect($url = null, $status = null, $exit = true) { + // Do not allow redirect() to exit in unit tests. + return parent::redirect($url, $status, false); + } +} +/** + * WizardComponentTest class + * + * @property WizardComponent $Wizard + * @package Wizard.Test.Case.Controller.Component + */ +class WizardComponentTest extends CakeTestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + $CakeRequest = new CakeRequest(null, false); + $CakeResponse = $this->getMock('CakeResponse', array('send')); + $this->Controller = new WizardTestController($CakeRequest, $CakeResponse); + $ComponentCollection = new ComponentCollection(); + $ComponentCollection->init($this->Controller); + $this->Controller->Components->init($this->Controller); + $this->Wizard = $this->Controller->Wizard; + $this->Wizard->initialize($this->Controller); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + CakeSession::destroy(); + unset($this->Controller, $this->Wizard); + } + +/** + * Test WizardComponent::initialize(). + * + * @return void + */ + public function testInitialize() { + $this->assertTrue($this->Wizard->controller instanceof WizardTestController); + } + + public function testConfig() { + $steps = array('account', 'review'); + $result = $this->Wizard->config('steps', $steps); + $this->assertEquals($steps, $result); + + $configSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEquals($steps, $configSteps); + + $result = $this->Wizard->config('steps'); + $this->assertEquals($steps, $result); + } + + public function testBranch() { + $this->Wizard->branch('female'); + $expectedBranches = array( + 'WizardTest' => array( + 'female' => 'branch', + ), + ); + $sessionBranches = $this->Wizard->Session->read('Wizard.branches'); + $this->assertEquals($expectedBranches, $sessionBranches); + } + + public function testBranchSkip() { + $this->Wizard->branch('female', true); + $expectedBranches = array( + 'WizardTest' => array( + 'female' => 'skip', + ), + ); + $sessionBranches = $this->Wizard->Session->read('Wizard.branches'); + $this->assertEquals($expectedBranches, $sessionBranches); + } + + public function testBranchOverwrite() { + $this->Wizard->branch('male'); + $this->Wizard->branch('female'); + $expectedBranches = array( + 'WizardTest' => array( + 'male' => 'branch', + 'female' => 'branch', + ), + ); + $sessionBranches = $this->Wizard->Session->read('Wizard.branches'); + $this->assertEquals($expectedBranches, $sessionBranches); + + $this->Wizard->branch('male', true); + $expectedBranches = array( + 'WizardTest' => array( + 'male' => 'skip', + 'female' => 'branch', + ), + ); + $sessionBranches = $this->Wizard->Session->read('Wizard.branches'); + $this->assertEquals($expectedBranches, $sessionBranches); + } + + public function testStartup() { + $configAction = $this->Wizard->Session->read('Wizard.config.action'); + $this->assertEmpty($configAction); + $configSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEmpty($configSteps); + $this->assertEmpty($this->Wizard->controller->helpers); + + $this->Wizard->startup($this->Controller); + + $expectedAction = 'wizard'; + $resultAction = $this->Wizard->Session->read('Wizard.config.action'); + $this->assertEquals($expectedAction, $resultAction); + $expectedSteps = array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ); + $resultSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEquals($expectedSteps, $resultSteps); + $this->assertEquals($expectedSteps, $this->Wizard->steps); + $expectedHelpers = array( + 'Wizard.Wizard' => array('sessionRootKey' => 'Wizard'), + ); + $this->assertEquals($expectedHelpers, $this->Wizard->controller->helpers); + } + + public function testStartupSkipBranch() { + $configSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEmpty($configSteps); + + $this->Wizard->branch('male', true); + $this->Wizard->branch('female', true); + $this->Wizard->startup($this->Controller); + + $expectedSteps = array( + 'step1', + 'step2', + 'gender', + 'step6', + 'confirmation', + ); + $resultSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEquals($expectedSteps, $resultSteps); + $this->assertEquals($expectedSteps, $this->Wizard->steps); + } + + public function testStartupBranch() { + $configSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEmpty($configSteps); + + $this->Wizard->branch('female'); + $this->Wizard->startup($this->Controller); + + $expectedSteps = array( + 'step1', + 'step2', + 'gender', + 'step4', + 'step5', + 'confirmation', + ); + $resultSteps = $this->Wizard->Session->read('Wizard.config.steps'); + $this->assertEquals($expectedSteps, $resultSteps); + $this->assertEquals($expectedSteps, $this->Wizard->steps); + } + + public function testStartupCustomRootSessionKey() { + $configAction = $this->Wizard->Session->read('WizardInstance001.config.action'); + $this->assertEmpty($configAction); + $configSteps = $this->Wizard->Session->read('WizardInstance001.config.steps'); + $this->assertEmpty($configSteps); + $this->assertEmpty($this->Wizard->controller->helpers); + + $this->Wizard->sessionRootKey = 'WizardInstance001'; + $this->Wizard->startup($this->Controller); + + $expectedAction = 'wizard'; + $resultAction = $this->Wizard->Session->read('WizardInstance001.config.action'); + $this->assertEquals($expectedAction, $resultAction); + $expectedSteps = array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ); + $resultSteps = $this->Wizard->Session->read('WizardInstance001.config.steps'); + $this->assertEquals($expectedSteps, $resultSteps); + $this->assertEquals($expectedSteps, $this->Wizard->steps); + $expectedHelpers = array( + 'Wizard.Wizard' => array('sessionRootKey' => 'WizardInstance001'), + ); + $this->assertEquals($expectedHelpers, $this->Wizard->controller->helpers); + } + + public function testProcessStepOneGet() { + $session = $this->Wizard->Session->read('Wizard'); + $this->assertEmpty($session); + + $this->Wizard->startup($this->Controller); + $result = $this->Wizard->process('step1'); + $this->assertTrue($result); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'step1', + 'activeStep' => 'step1', + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } + + public function testProcessStepOnePost() { + $session = $this->Wizard->Session->read('Wizard'); + $this->assertEmpty($session); + $this->Wizard->startup($this->Controller); + // Emulate GET request to set session variables. + $this->Wizard->process('step1'); + // Emulate POST request. + $postData = array( + 'User' => array( + 'username' => 'admin', + 'password' => 'pass', + ), + ); + $this->Wizard->controller->request->data = $postData; + $CakeResponse = $this->Wizard->process('step1'); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard/step2', $headers['Location']); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'step2', + 'activeStep' => 'step1', + ), + 'WizardTest' => array( + 'step1' => $postData, + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } + +/** + * Tests 'autoAdvance' and 'defaultBranch' settings set to false and manual call to `branch()`. + * + * @return void + */ + public function testProcessGenderPost() { + $this->Wizard->Session->delete('Wizard'); + unset($this->Controller, $this->Wizard); + $CakeRequest = new CakeRequest(null, false); + $CakeResponse = $this->getMock('CakeResponse', array('send')); + $this->Controller = new WizardTestController($CakeRequest, $CakeResponse); + $this->Controller->components['Wizard.Wizard']['autoAdvance'] = false; + $this->Controller->components['Wizard.Wizard']['defaultBranch'] = false; + $ComponentCollection = new ComponentCollection(); + $ComponentCollection->init($this->Controller); + $this->Controller->Components->init($this->Controller); + $this->Wizard = $this->Controller->Wizard; + $this->Wizard->initialize($this->Controller); + + // Set session prerequisites. + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'gender', + 'activeStep' => 'gender', + ), + 'WizardTest' => array( + 'step1' => array(), + 'step2' => array(), + ), + ); + $this->Wizard->Session->write('Wizard', $session); + + $this->Wizard->startup($this->Controller); + $postData = array( + 'WizardUserMock' => array( + 'gender' => 'female', + ), + ); + $this->Wizard->controller->request->data = $postData; + $CakeResponse = $this->Wizard->process('gender'); + + $expectedSession = array( + 'branches' => array( + 'WizardTest' => array( + 'female' => 'branch', + ), + ), + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step4', + 'step5', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'step4', + 'activeStep' => 'gender', + ), + 'WizardTest' => array( + 'step1' => array(), + 'step2' => array(), + 'gender' => $postData, + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard/step4', $headers['Location']); + } + + public function testProcessAutovalidatePost() { + // Set session prerequisites. + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'gender', + 'activeStep' => 'gender', + ), + 'WizardTest' => array( + 'step1' => array(), + 'step2' => array(), + ), + ); + $this->Wizard->Session->write('Wizard', $session); + + $this->Wizard->startup($this->Controller); + $postData = array( + 'WizardUserMock' => array( + 'gender' => 'male', + ), + ); + $this->Wizard->controller->request->data = $postData; + $CakeResponse = $this->Wizard->process('gender'); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard/step3', $headers['Location']); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'step3', + 'activeStep' => 'gender', + ), + 'WizardTest' => array( + 'step1' => array(), + 'step2' => array(), + 'gender' => $postData, + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } + + public function testProcessLastStepPost() { + // Set session prerequisites. + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'confirmation', + 'activeStep' => 'confirmation', + ), + 'WizardTest' => array( + 'step1' => array(), + 'step2' => array(), + 'gender' => array(), + 'step3' => array(), + 'step4' => array(), + ), + ); + $this->Wizard->Session->write('Wizard', $session); + + $this->Wizard->startup($this->Controller); + $postData = array( + 'WizardUserMock' => array( + 'confirm' => '1', + ), + ); + $this->Wizard->controller->request->data = $postData; + $CakeResponse = $this->Wizard->process('confirmation'); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard', $headers['Location']); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'confirmation', + 'activeStep' => 'confirmation', + ), + 'complete' => array( + 'step1' => array(), + 'step2' => array(), + 'gender' => array(), + 'step3' => array(), + 'step4' => array(), + 'confirmation' => $postData, + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } + + public function testProcessAfterComplete() { + // Set session prerequisites. + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'confirmation', + 'activeStep' => 'confirmation', + ), + 'complete' => array( + 'step1' => array(), + 'step2' => array(), + 'gender' => array(), + 'step3' => array(), + 'step4' => array(), + 'confirmation' => array(), + ), + ); + $this->Wizard->Session->write('Wizard', $session); + + $this->Wizard->initialize($this->Controller); + $this->Wizard->startup($this->Controller); + $CakeResponse = $this->Wizard->process(null); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard/step1', $headers['Location']); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'confirmation', + 'activeStep' => 'confirmation', + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } + + public function testRedirectPersistUrlParams() { + $session = $this->Wizard->Session->read('Wizard'); + $this->assertEmpty($session); + + $url = '/wizard_test/wizard/step1/123/key:value?x=7&y=9'; + $CakeRequest = new CakeRequest($url, true); + $CakeRequest->addParams(Router::parse($url)); + $CakeResponse = $this->getMock('CakeResponse', array('send')); + $this->Controller = new WizardTestController($CakeRequest, $CakeResponse); + $this->Controller->components['Wizard.Wizard']['persistUrlParams'] = true; + $ComponentCollection = new ComponentCollection(); + $ComponentCollection->init($this->Controller); + $this->Controller->Components->init($this->Controller); + $this->Wizard = $this->Controller->Wizard; + $this->Wizard->initialize($this->Controller); + + $this->Wizard->startup($this->Controller); + //$this->Wizard->persistUrlParams = true; + // Emulate GET request to set session variables. + $this->Wizard->process('step1'); + // Emulate POST request. + $postData = array( + 'User' => array( + 'username' => 'admin', + 'password' => 'pass', + ), + ); + $this->Wizard->controller->request->data = $postData; + $CakeResponse = $this->Wizard->process('step1'); + + $this->assertInstanceOf('CakeResponse', $CakeResponse); + $headers = $CakeResponse->header(); + $this->assertContains('/wizard_test/wizard/step2/123/key:value?x=7&y=9', $headers['Location']); + + $expectedSession = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'step2', + 'activeStep' => 'step1', + ), + 'WizardTest' => array( + 'step1' => $postData, + ), + ); + $resultSession = $this->Wizard->Session->read('Wizard'); + $this->assertEquals($expectedSession, $resultSession); + } +} diff --git a/Test/Case/View/Helper/WizardHelperTest.php b/Test/Case/View/Helper/WizardHelperTest.php new file mode 100644 index 0000000..9689dd5 --- /dev/null +++ b/Test/Case/View/Helper/WizardHelperTest.php @@ -0,0 +1,194 @@ +Wizard = new WizardHelper($View); + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'gender', + 'activeStep' => 'gender', + ), + ); + CakeSession::write('Wizard', $session); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + unset($this->Wizard); + CakeSession::delete('Wizard'); + parent::tearDown(); + } + + public function testConfigEmpty() { + CakeSession::delete('Wizard'); + $result = $this->Wizard->config('steps'); + $this->assertNull($result); + } + + public function testConfigReadAll() { + $expected = array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'gender', + 'activeStep' => 'gender', + ); + $result = $this->Wizard->config(); + $this->assertEquals($expected, $result); + } + + public function testConfigReadOne() { + $expected = array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ); + $result = $this->Wizard->config('steps'); + $this->assertEquals($expected, $result); + } + + public function testLink() { + $expected = 'gender'; + $result = $this->Wizard->link('gender'); + $this->assertEquals($expected, $result); + } + + public function testLinkStep() { + $expected = 'Gender'; + $result = $this->Wizard->link('Gender', 'gender'); + $this->assertEquals($expected, $result); + } + + public function testStepNumberCurrent() { + $result = $this->Wizard->stepNumber(); + $this->assertEquals(3, $result); + } + + public function testStepNumberConfirmation() { + $result = $this->Wizard->stepNumber('confirmation'); + $this->assertEquals(6, $result); + } + + public function testStepNumberNone() { + $result = $this->Wizard->stepNumber('step5'); + $this->assertFalse($result); + } + + public function testStepTotal() { + $result = $this->Wizard->stepTotal(); + $this->assertEquals(6, $result); + } + + public function testProgressMenu() { + $expected = '
Step1
'; + $expected .= '
Step2
'; + $expected .= '
Gender
'; + $expected .= '
Step3
'; + $expected .= '
Step4
'; + $expected .= '
Confirmation
'; + $result = $this->Wizard->progressMenu(); + $this->assertEquals($expected, $result); + } + + public function testProgressMenuCustomWrapper() { + $expected = '
  • Step1
  • '; + $expected .= '
  • Step2
  • '; + $expected .= '
  • Gender
  • '; + $expected .= '
  • Step3
  • '; + $expected .= '
  • Step4
  • '; + $expected .= '
  • Confirmation
  • '; + $result = $this->Wizard->progressMenu(array(), array('wrap' => 'li')); + $this->assertEquals($expected, $result); + } + + public function testProgressMenuCustomTitles() { + $expected = '
    Credentials
    '; + $expected .= '
    Address
    '; + $expected .= '
    Gender
    '; + $expected .= '
    Shipping Address
    '; + $expected .= '
    Payment
    '; + $expected .= '
    Confirmation
    '; + + $titles = array( + 'step1' => 'Credentials', + 'step2' => 'Address', + 'gender' => 'Gender', + 'step3' => 'Shipping Address', + 'step4' => 'Payment', + 'confirmation' => 'Confirmation', + ); + $result = $this->Wizard->progressMenu($titles); + $this->assertEquals($expected, $result); + } + + public function testProgressMenuPersistUrlParams() { + $url = '/wizard_test/wizard/gender/123?x=7&y=9'; + $CakeRequest = new CakeRequest($url, true); + $CakeRequest->addParams(Router::parse($url)); + $Controller = new Controller($CakeRequest, new CakeResponse()); + $View = new View($Controller); + $this->Wizard = new WizardHelper($View); + $session = array( + 'config' => array( + 'steps' => array( + 'step1', + 'step2', + 'gender', + 'step3', + 'step4', + 'confirmation', + ), + 'action' => 'wizard', + 'expectedStep' => 'gender', + 'activeStep' => 'gender', + 'persistUrlParams' => true, + ), + ); + CakeSession::write('Wizard', $session); + + $expected = '
    Step1
    '; + $expected .= '
    Step2
    '; + $expected .= '
    Gender
    '; + $expected .= '
    Step3
    '; + $expected .= '
    Step4
    '; + $expected .= '
    Confirmation
    '; + $result = $this->Wizard->progressMenu(); + $this->assertEquals($expected, $result); + } +} diff --git a/View/Helper/WizardHelper.php b/View/Helper/WizardHelper.php new file mode 100644 index 0000000..234dab9 --- /dev/null +++ b/View/Helper/WizardHelper.php @@ -0,0 +1,182 @@ +Session->read($this->sessionRootKey . '.config'); + } else { + $wizardData = $this->Session->read($this->sessionRootKey . '.config.' . $key); + if (!empty($wizardData)) { + return $wizardData; + } else { + return null; + } + } + } + +/** + * undocumented function + * + * @param string $title The content to be wrapped by `` tags. + * @param string $step Form step. + * @param array|string $htmlAttributes Array of options and HTML attributes. + * @param bool|string $confirmMessage JavaScript confirmation message. This + * argument is deprecated as of 2.6. Use `confirm` key in $options instead. + * @return string link to a specific step + */ + public function link($title, $step = null, $htmlAttributes = array(), $confirmMessage = false) { + if ($step == null) { + $step = $title; + } + $url = array( + 'action' => $this->config('action'), + $step, + ); + return $this->Html->link($title, $url, $htmlAttributes, $confirmMessage); + } + +/** + * Retrieve the step number of the specified step name, or the active step + * + * @param string $step optional name of step + * @param int|string $shiftIndex optional offset of returned array index. Default 1 + * @return string step number. Returns false if not found + */ + public function stepNumber($step = null, $shiftIndex = 1) { + if ($step == null) { + $step = $this->config('activeStep'); + } + $steps = $this->config('steps'); + if (in_array($step, $steps)) { + return array_search($step, $steps) + $shiftIndex; + } else { + return false; + } + } + +/** + * Counts the total number of steps. + * + * @return int + */ + public function stepTotal() { + $steps = $this->config('steps'); + return count($steps); + } + +/** + * Returns a set of html elements containing links for each step in the wizard. + * + * @param array|string $titles Array of form steps where the keys are + * the steps and the values are the titles to be used for links. If empty then humanized + * step names are used from session. + * @param array|string $attributes pass a value for 'wrap' to change the default tag used + * @param array|string $htmlAttributes Array of options and HTML attributes. + * @param bool|string $confirmMessage JavaScript confirmation message. This + * argument is deprecated as of 2.6. Use `confirm` key in $options instead. + * @return string + */ + public function progressMenu($titles = array(), $attributes = array(), $htmlAttributes = array(), $confirmMessage = false) { + $wizardConfig = $this->config(); + extract($wizardConfig); + $wizardAction = $this->config('action'); + $attributes = array_merge(array('wrap' => 'div'), $attributes); + extract($attributes); + $incomplete = null; + foreach ($steps as $title => $step) { + if (empty($titles[$step])) { + $title = Inflector::humanize($step); + } else { + $title = $titles[$step]; + } + if (!$incomplete) { + if ($step == $expectedStep) { + $incomplete = true; + $class = 'expected'; + } else { + $class = 'complete'; + } + if ($step == $activeStep) { + $class .= ' active'; + } + $url = $this->__getStepUrl($step); + $this->output .= "<$wrap class=\"$class\">"; + $this->output .= $this->Html->link($title, $url, $htmlAttributes, $confirmMessage); + $this->output .= ""; + } else { + $this->output .= "<$wrap class=\"incomplete\">$title"; + } + } + return $this->output; + } + +/** + * Wrapper for Form->create() + * + * @param string $model The model name for which the form is being defined. + * @param array $options An array of html attributes and options. + * + * @return string + */ + public function create($model = null, $options = array()) { + if (!isset($options['url']) || !in_array($this->request->params['pass'][0], $options['url'])) { + $options['url'][] = $this->request->params['pass'][0]; + } + return $this->Form->create($model, $options); + } + +/** + * Constructs the URL for a given step. + * + * @param string $step step action. + * @return array + */ + private function __getStepUrl($step) { + $wizardAction = $this->config('action'); + if ($this->config('persistUrlParams')) { + $url = Router::reverseToArray($this->request); + $url['action'] = $this->action; + $url[0] = $step; + } else { + $url = array( + 'action' => $wizardAction, + $step, + ); + } + return $url; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0f215f2 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "proloser/cakephp-wizard", + "description": "CakePHP Wizard Plugin", + "type": "cakephp-plugin", + "keywords": ["cakephp", "wizard"], + "homepage": "https://github.com/ProLoser/CakePHP-Wizard", + "license": "MIT", + "authors": [ + { + "name": "Dean Sofer", + "homepage": "https://github.com/ProLoser", + "role": "developer" + } + ], + "replace": { + "lucasff/cakephp-wizard": "dev-master", + "ProLoser/CakePHP-Wizard": "dev-master" + }, + "require": { + "php": ">=5.3.0", + "composer/installers": "*" + }, + "require-dev": { + "phpunit/phpunit":"3.7.*", + "phpmd/phpmd" : "@stable", + "cakephp/cakephp-codesniffer": "1.*" + }, + "extra": { + "installer-name": "Wizard" + } +} diff --git a/controllers/components/wizard.php b/controllers/components/wizard.php deleted file mode 100755 index 79be0a2..0000000 --- a/controllers/components/wizard.php +++ /dev/null @@ -1,536 +0,0 @@ - array('college', 'degree_type'), 'nodegree' => 'experience'), 'confirm'); - * - * The 'branchnames' (ie 'degree', 'nodegree') are arbitrary but used as selectors for the branch() and unbranch() methods. Branches - * can point to either another steps array or a single step. The first branch in a group that hasn't been skipped (see branch()) - * is included by default (if $defaultBranch = true). - * - * @var array - * @access public - */ - var $steps = array(); -/** - * Controller action that processes your step. - * - * @var string - * @access public - */ - var $wizardAction = 'wizard'; -/** - * Url to be redirected to after the wizard has been completed. - * Controller::afterComplete() is called directly before redirection. - * - * @var mixed - * @access public - */ - var $completeUrl = '/'; -/** - * Url to be redirected to after 'Cancel' submit button has been pressed by user. - * - * @var mixed - * @access public - */ - var $cancelUrl = '/'; -/** - * Url to be redirected to after 'Draft' submit button has been pressed by user. - * - * @var mixed - * @access public - */ - var $draftUrl = '/'; -/** - * If true, the first "non-skipped" branch in a group will be used if a branch has - * not been included specifically. - * - * @var boolean - * @access public - */ - var $defaultBranch = true; -/** - * If true, the user will not be allowed to edit previously completed steps. They will be - * "locked down" to the current step. - * - * @var boolean - * @access public - */ - var $lockdown = false; -/** - * If true, the component will render views found in views/{wizardAction}/{step}.ctp rather - * than views/{step}.ctp. - * - * @var boolean - * @access public - */ - var $nestedViews = false; -/** - * Internal step tracking. - * - * @var string - * @access protected - */ - var $_currentStep = null; -/** - * Holds the session key for data storage. - * - * @var string - * @access protected - */ - var $_sessionKey = null; -/** - * Other session keys used. - * - * @var string - * @access protected - */ - var $_configKey = null; - var $_branchKey = null; -/** - * Holds the array based url for redirecting. - * - * @var array - * @access protected - */ - var $_wizardUrl = array(); -/** - * Other components used. - * - * @var array - * @access public - */ - var $components = array('Session'); -/** - * Initializes WizardComponent for use in the controller - * - * @param object $controller A reference to the instantiating controller object - * @access public - */ - function initialize(&$controller, $settings = array()) { - $this->controller =& $controller; - $this->_set($settings); - - $this->_sessionKey = $this->Session->check('Wizard.complete') ? 'Wizard.complete' : 'Wizard.' . $controller->name; - $this->_configKey = 'Wizard.config'; - $this->_branchKey = 'Wizard.branches.' . $controller->name; - } -/** - * Component startup method. - * - * @param object $controller A reference to the instantiating controller object - * @access public - */ - function startup(&$controller) { - $this->steps = $this->_parseSteps($this->steps); - - $this->config('wizardAction', $this->wizardAction); - $this->config('steps', $this->steps); - - if (!in_array('Wizard.Wizard', $this->controller->helpers) && !array_key_exists('Wizard.Wizard', $this->controller->helpers)) { - $this->controller->helpers[] = 'Wizard.Wizard'; - } - } -/** - * Main Component method. - * - * @param string $step Name of step associated in $this->steps to be processed. - * @access public - */ - function process($step) { - if (isset($this->controller->params['form']['Cancel'])) { - if (method_exists($this->controller, '_beforeCancel')) { - $this->controller->_beforeCancel($this->_getExpectedStep()); - } - $this->reset(); - $this->controller->redirect($this->cancelUrl); - } - if (isset($this->controller->params['form']['Draft'])) { - if (method_exists($this->controller, '_saveDraft')) { - $draft = array('_draft' => array('current' => array('step' => $step, 'data' => $this->controller->data))); - $this->controller->_saveDraft(array_merge_recursive((array)$this->read(), $draft)); - } - - $this->reset(); - $this->controller->redirect($this->draftUrl); - } - - if (empty($step)) { - if ($this->Session->check('Wizard.complete')) { - if (method_exists($this->controller, '_afterComplete')) { - $this->controller->_afterComplete(); - } - $this->reset(); - $this->controller->redirect($this->completeUrl); - } - - $this->autoReset = false; - } elseif ($step == 'reset') { - if (!$this->lockdown) { - $this->reset(); - } - } else { - if ($this->_validStep($step)) { - $this->_setCurrentStep($step); - - if (!empty($this->controller->data) && !isset($this->controller->params['form']['Previous'])) { - $proceed = false; - - $processCallback = '_' . Inflector::variable('process_' . $this->_currentStep); - if (method_exists($this->controller, $processCallback)) { - $proceed = $this->controller->$processCallback(); - } elseif ($this->autoValidate) { - $proceed = $this->_validateData(); - } else { - trigger_error(sprintf(__('Process Callback not found. Please create Controller::%s', true), $processCallback), E_USER_WARNING); - } - - if ($proceed) { - $this->save(); - - if (next($this->steps)) { - if ($this->autoAdvance) { - $this->redirect(); - } - $this->redirect(current($this->steps)); - } else { - $this->Session->write('Wizard.complete', $this->read()); - $this->reset(); - - $this->controller->redirect($this->wizardAction); - } - } - } elseif (isset($this->controller->params['form']['Previous']) && prev($this->steps)) { - $this->redirect(current($this->steps)); - } elseif ($this->Session->check("$this->_sessionKey._draft.current")) { - $this->controller->data = $this->read('_draft.current.data'); - $this->Session->delete("$this->_sessionKey._draft.current"); - } elseif ($this->Session->check("$this->_sessionKey.$this->_currentStep")) { - $this->controller->data = $this->read($this->_currentStep); - } - - $prepareCallback = '_' . Inflector::variable('prepare_' . $this->_currentStep); - if (method_exists($this->controller, $prepareCallback)) { - $this->controller->$prepareCallback(); - } - - $this->config('activeStep', $this->_currentStep); - - if ($this->nestedViews) { - $this->controller->viewPath .= '/' . $this->wizardAction; - } - - return $this->controller->autoRender ? $this->controller->render($this->_currentStep) : true; - } else { - trigger_error(sprintf(__('Step validation: %s is not a valid step.', true), $step), E_USER_WARNING); - } - } - - if ($step != 'reset' && $this->autoReset) { - $this->reset(); - } - - $this->redirect(); - } -/** - * Selects a branch to be used in the steps array. The first branch in a group is included by default. - * - * @param string $name Branch name to be included in steps. - * @param boolean $skip Branch will be skipped instead of included if true. - * @access public - */ - function branch($name, $skip = false) { - $branches = array(); - - if ($this->Session->check($this->_branchKey)) { - $branches = $this->Session->read($this->_branchKey); - } - - if (isset($branches[$name])) { - unset($branches[$name]); - } - - $value = $skip ? 'skip' : 'branch'; - $branches[$name] = $value; - - $this->Session->write($this->_branchKey, $branches); - } -/** - * Saves configuration details for use in WizardHelper or returns a config value. - * This is method usually handled only by the component. - * - * @param string $name Name of configuration variable. - * @param mixed $value Value to be stored. - * @return mixed - * @access public - */ - function config($name, $value = null) { - if ($value == null) { - return $this->Session->read("$this->_configKey.$name"); - } - $this->Session->write("$this->_configKey.$name", $value); - } -/** - * Loads previous draft session. - * - * @param array $draft Session data of same format passed to Controller::_saveDraft() - * @see WizardComponent::process() - * @access public - */ - function loadDraft($draft = array()) { - if (!empty($draft['_draft']['current']['step'])) { - $this->restore($draft); - $this->redirect($draft['_draft']['current']['step']); - } - $this->redirect(); - } -/** - * Get the data from the Session that has been stored by the WizardComponent. - * - * @param mixed $name The name of the session variable (or a path as sent to Set.extract) - * @return mixed The value of the session variable - * @access public - */ - function read($key = null) { - if ($key == null) { - return $this->Session->read($this->_sessionKey); - } else { - $wizardData = $this->Session->read("$this->_sessionKey.$key"); - return !empty($wizardData) ? $wizardData : null; - } - } -/** - * Handles Wizard redirection. A null url will redirect to the "expected" step. - * - * @param string $step Stepname to be redirected to. - * @param integer $status Optional HTTP status code (eg: 404) - * @param boolean $exit If true, exit() will be called after the redirect - * @see Controller::redirect() - * @access public - */ - function redirect($step = null, $status = null, $exit = true) { - if ($step == null) { - $step = $this->_getExpectedStep(); - } - $url = array('controller' => Inflector::underscore($this->controller->name), 'action' => $this->wizardAction, $step); - $this->controller->redirect($url, $status, $exit); - } -/** - * Resets the wizard by deleting the wizard session. - * - * @access public - */ - function resetWizard() { - $this->reset(); - } -/** - * Resets the wizard by deleting the wizard session. - * - * @access public - */ - function reset() { - $this->Session->delete($this->_branchKey); - $this->Session->delete($this->_sessionKey); - } -/** - * Sets data into controller's wizard session. Particularly useful if the data - * originated from WizardComponent::read() as this will restore a previous session. - * - * @param array $data Data to be written to controller's wizard session. - * @access public - */ - function restore($data = array()) { - $this->Session->write($this->_sessionKey, $data); - } -/** - * Saves the data from the current step into the Session. - * - * Please note: This is normally called automatically by the component after - * a successful _processCallback, but can be called directly for advanced navigation purposes. - * - * @access public - */ - function save($step = null, $data = null) { - if (is_null($step)) { - $step = $this->_currentStep; - } - if (is_null($data)) { - $data = $this->controller->data; - } - $this->Session->write("$this->_sessionKey.$step", $data); - } -/** - * Removes a branch from the steps array. - * - * @param string $branch Name of branch to be removed from steps array. - * @access public - */ - function unbranch($branch) { - $this->Session->delete("$this->_branchKey.$branch"); - } -/** - * Finds the first incomplete step (i.e. step data not saved in Session). - * - * @return string $step or false if complete - * @access protected - */ - function _getExpectedStep() { - foreach ($this->steps as $step) { - if (!$this->Session->check("$this->_sessionKey.$step")) { - $this->config('expectedStep', $step); - return $step; - } - } - return false; - } -/** - * Saves configuration details for use in WizardHelper. - * - * @return mixed - * @access protected - */ - function _branchType($branch) { - if ($this->Session->check("$this->_branchKey.$branch")) { - return $this->Session->read("$this->_branchKey.$branch"); - } - return false; - } -/** - * Parses the steps array by stripping off nested arrays not included in the branches - * and returns a simple array with the correct steps. - * - * @param array $steps Array to be parsed for nested arrays and returned as simple array. - * @return array - * @access protected - */ - function _parseSteps($steps) { - $parsed = array(); - - foreach ($steps as $key => $name) { - if (is_array($name)) { - foreach ($name as $branchName => $step) { - $branchType = $this->_branchType($branchName); - - if ($branchType) { - if ($branchType !== 'skip') { - $branch = $branchName; - } - } elseif (empty($branch) && $this->defaultBranch) { - $branch = $branchName; - } - } - - if (!empty($branch)) { - if (is_array($name[$branch])) { - $parsed = array_merge($parsed, $this->_parseSteps($name[$branch])); - } else { - $parsed[] = $name[$branch]; - } - } - } else { - $parsed[] = $name; - } - } - return $parsed; - } -/** - * Moves internal array pointer of $this->steps to $step and sets $this->_currentStep. - * - * @param $step Step to point to. - * @access protected - */ - function _setCurrentStep($step) { - $this->_currentStep = reset($this->steps); - - while(current($this->steps) != $step) { - $this->_currentStep = next($this->steps); - } - } -/** - * Validates controller data with the correct model if the model is included in - * the controller's uses array. This only occurs if $autoValidate = true and there - * is no processCallback in the controller for the current step. - * - * @return boolean - * @access protected - */ - function _validateData() { - $controller =& $this->controller; - - foreach ($controller->data as $model => $data) { - if (in_array($model, $controller->uses)) { - $controller->{$model}->set($data); - - if (!$controller->{$model}->validates()) { - return false; - } - } - } - return true; - } -/** - * Validates the $step in two ways: - * 1. Validates that the step exists in $this->steps array. - * 2. Validates that the step is either before or exactly the expected step. - * - * @param $step Step to validate. - * @return mixed - * @access protected - */ - function _validStep($step) { - if (in_array($step, $this->steps)) { - if ($this->lockdown) { - return (array_search($step, $this->steps) == array_search($this->_getExpectedStep(), $this->steps)); - } - return (array_search($step, $this->steps) <= array_search($this->_getExpectedStep(), $this->steps)); - } - return false; - } -} -?> \ No newline at end of file diff --git a/phpunit-clover.xml b/phpunit-clover.xml new file mode 100644 index 0000000..3831142 --- /dev/null +++ b/phpunit-clover.xml @@ -0,0 +1,20 @@ + + + + + Controller + Model + View + + Cake + Composer + Test + *Test.php + + + + + + + + diff --git a/views/helpers/wizard.php b/views/helpers/wizard.php deleted file mode 100644 index daf464f..0000000 --- a/views/helpers/wizard.php +++ /dev/null @@ -1,117 +0,0 @@ -Session->read('Wizard.config'); - } else { - $wizardData = $this->Session->read('Wizard.config.'.$key); - if (!empty($wizardData)) { - return $wizardData; - } else { - return null; - } - } - } -/** - * undocumented function - * - * @param string $title - * @param string $step - * @param string $htmlAttributes - * @param string $confirmMessage - * @param string $escapeTitle - * @return string link to a specific step - */ - function link($title, $step = null, $htmlAttributes = array(), $confirmMessage = false, $escapeTitle = true) { - if ($step == null) { - $step = $title; - } - $wizardAction = $this->config('wizardAction'); - - return $this->Html->link($title, $wizardAction.$step, $htmlAttributes, $confirmMessage, $escapeTitle); - } -/** - * Retrieve the step number of the specified step name, or the active step - * - * @param string $step optional name of step - * @param string $shiftIndex optional offset of returned array index. Default 1 - * @return string step number. Returns false if not found - */ - function stepNumber($step = null, $shiftIndex = 1) { - if ($step == null) { - $step = $this->config('activeStep'); - } - - $steps = $this->config('steps'); - - if (in_array($step, $steps)) { - return array_search($step, $steps) + $shiftIndex; - } else { - return false; - } - } -/** - * Returns a set of html elements containing links for each step in the wizard. - * - * @param string $titles - * @param string $attributes pass a value for 'wrap' to change the default tag used - * @param string $htmlAttributes - * @param string $confirmMessage - * @param string $escapeTitle - * @return string - */ - function progressMenu($titles = array(), $attributes = array(), $htmlAttributes = array(), $confirmMessage = false, $escapeTitle = true) { - $wizardConfig = $this->config(); - extract($wizardConfig); - - $attributes = array_merge(array('wrap' => 'div'), $attributes); - extract($attributes); - - $incomplete = null; - - foreach ($steps as $title => $step) { - $title = empty($titles[$step]) ? $step : $titles[$step]; - - if (!$incomplete) { - if ($step == $expectedStep) { - $incomplete = true; - $class = 'expected'; - } else { - $class = 'complete'; - } - if ($step == $activeStep) { - $class .= ' active'; - } - $this->output .= "<$wrap class='$class'>" . $this->Html->link($title, array('action' => $wizardAction, $step), $htmlAttributes, $confirmMessage, $escapeTitle) . ""; - } else { - $this->output .= "<$wrap class='incomplete'>" . $title . ""; - } - } - - return $this->output; - } -} -?> \ No newline at end of file