Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
05cb1fa
Added documentation from bakery.cakephp.org
Aug 18, 2010
388e9e3
Updated method names
Aug 19, 2010
8f1ae06
Minor updates to documentation
Aug 19, 2010
c4e6a0e
Fixed glitch i made when i renamed the reset method
Aug 19, 2010
eac5f18
Finished adding the rest of the docs.
Aug 26, 2010
e127120
Created a Form->create() wrapper so the submit url is automatically set
Dec 22, 2010
768a9ab
Merge branch 'master' of git://github.com/jaredhoyt/cakephp-wizard
Dec 22, 2010
ae7f7c4
Renamed 'wizardAction' attribute to 'action'
Dec 22, 2010
455c707
Better way to retrieve the controller name
Dec 23, 2010
aa12f9a
Made the completion redirect an array for more specific uses (such as…
Dec 23, 2010
cf84595
Instead of triggering an error when reaching a wrong step, jump to th…
Jan 24, 2011
525261a
Adding missing variable to progressMenu.
tomasm- May 7, 2011
9c1818b
CakePHP 2.0 adaptations :
philgagnon12 Feb 20, 2012
9f5fe7c
Merge pull request #1 from philgagnon12/master
Feb 21, 2012
03ebfdf
Merge branch 'patch-1' of https://github.com/tomasm-/cakephp-wizard i…
Feb 21, 2012
835b8fc
Merged mikerogerz fixing #6
Feb 21, 2012
6915c99
Merging master branch from @kindred
Feb 21, 2012
ca79799
Merge branch 'cake1.3'
Feb 21, 2012
bd8e2d2
Update controllers/components/wizard.php
philgagnon12 Feb 21, 2012
8568e53
Update controllers/components/wizard.php
philgagnon12 Feb 21, 2012
0f595c2
Merge pull request #2 from philgagnon12/master
Feb 21, 2012
9d687e1
class WizardComponent extends Component { ... }
philgagnon12 Feb 21, 2012
a72e0a6
Merge branch 'master' of https://github.com/philgagnon12/wizard
Feb 27, 2012
2b22e70
Corrected folder structure to cake2.0 convention
Feb 27, 2012
fbbcd84
$this->params() has been replaced by $this->request in Cake 2.*
destinydriven Jul 7, 2012
f8b92cc
Merge pull request #3 from destinydriven/master
Jul 7, 2012
e4029fc
Update Controller/Component/WizardComponent.php
destinydriven Jul 25, 2012
1c34870
Update View/Helper/WizardHelper.php
destinydriven Jul 25, 2012
cfcb978
Update README.md
destinydriven Jul 25, 2012
966c079
Update README2.md
destinydriven Jul 25, 2012
3e9f1ce
Update README3.md
destinydriven Jul 25, 2012
87abd39
Update README3.md
destinydriven Jul 25, 2012
b0f38eb
Update README4.md
destinydriven Jul 25, 2012
3fea909
Update README5.md
destinydriven Jul 25, 2012
6f333a2
Merge pull request #4 from destinydriven/master
Jul 25, 2012
7a2568e
Updated callback signatures, visibility keywords
destinydriven Sep 4, 2012
e9d9a1a
Merge pull request #5 from destinydriven/master
ProLoser Sep 4, 2012
6663f18
Add roaming option
idev247 Jan 31, 2013
95b12d4
Merge pull request #8 from idev247/master
ProLoser Jan 31, 2013
2e9cbd5
Use NotImplementedException for missing process Callbacks
destinydriven Apr 11, 2013
58d7aef
Merge pull request #9 from destinydriven/master
ProLoser Apr 11, 2013
89d2619
Update README.md
ProLoser Apr 11, 2013
b6cc30e
clean code formating. implements SaveAndBack mode for wizard
skie May 16, 2013
ae628e9
fix cake2 style
skie May 21, 2013
42e3d7a
Adding initial composer.json file
lucasff Feb 15, 2014
d47f6b2
Packagist requires lowercase package names
lucasff Feb 15, 2014
5be2e21
Code reformatting and PHPDoc fixing
lucasff Feb 24, 2014
76fa47e
Code reformatting and PHPDoc fixing
lucasff Feb 24, 2014
05b644c
add return before every redirect()
Mar 17, 2014
e24c1fe
Merge pull request #1 from webcrab/patch-2
Mar 17, 2014
d3205c5
Update README.md
destinydriven Aug 13, 2014
a3ba5e5
Merge remote-tracking branch 'skie/master'
lucasff Aug 22, 2014
f80e450
Merge pull request #15 from destinydriven/master
ProLoser Aug 7, 2015
7462433
Merge pull request #16 from lucasff/master
ProLoser Aug 7, 2015
5a9f441
Update composer.json
ProLoser Aug 7, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,196 changes: 660 additions & 536 deletions controllers/components/wizard.php → Controller/Component/WizardComponent.php
100755 → 100644

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
89 changes: 89 additions & 0 deletions README2.md
Original file line number Diff line number Diff line change
@@ -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 <code>example.com/signup/wizard/account</code> 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:

<pre><code>&lt;?php
class SignupController extends AppController {
public $components = array('Wizard');

public function beforeFilter() {
$this-&gt;Wizard-&gt;steps = array('account', 'address', 'billing', 'review');
}

public function wizard($step = null) {
$this-&gt;Wizard-&gt;process($step);
}
}
?&gt;</code></pre>

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 <code>example.com/signup</code> to be handled by SignupController::wizard(), which would then redirect to /signup/wizard/account (or the first incomplete step in the wizard).

<pre><code>Router::connect('/signup', array('controller' =&gt; 'signup', 'action' =&gt; 'wizard'));</code></pre>

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-&gt;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:

<pre><code>&lt;?php
class SignupController extends AppController {
public $uses = array('Client', 'User', 'Billing');
public $components = array('Wizard');

public function beforeFilter() {
$this-&gt;Wizard-&gt;steps = array('account', 'address', 'billing', 'review');
}

public function wizard($step = null) {
$this-&gt;Wizard-&gt;process($step);
}
/**
* [Wizard Process Callbacks]
*/
public function processAccount() {
$this-&gt;Client-&gt;set($this-&gt;data);
$this-&gt;User-&gt;set($this-&gt;data);

if($this-&gt;Client-&gt;validates() &amp;&amp; $this-&gt;User-&gt;validates()) {
return true;
}
return false;
}

public function processAddress() {
$this-&gt;Client-&gt;set($this-&gt;data);

if($this-&gt;Client-&gt;validates()) {
return true;
}
return false;
}

public function processBilling() {
$this-&gt;Billing-&gt;set($this-&gt;data);

if($this-&gt;Billing-&gt;validates()) {
return true;
}
return false;
}

public function processReview() {
return true;
}
}
?&gt;</code></pre>
101 changes: 101 additions & 0 deletions README3.md
Original file line number Diff line number Diff line change
@@ -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 <code>SessionComponent::read()</code> 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:

<pre><code>public function processReview() {
$email = $this->Wizard->read('account.User.email');
/* do something with the $email here */

return true;
}</code></pre>

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 <code>Wizard->reset()</code> 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:

<pre><code><?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');
$this->Wizard->completeUrl = '/signup/confirm';
}

public function confirm() {
}

public function wizard($step = null) {
$this->Wizard->process($step);
}
/**
* [Wizard Process Callbacks]
*/
protected function _processAccount() {
$this->Client->set($this->data);
$this->User->set($this->data);

if($this->Client->validates() &amp;&amp; $this->User->validates()) {
return true;
}
return false;
}

protected function _processAddress() {
$this->Client->set($this->data);

if($this->Client->validates()) {
return true;
}
return false;
}

protected function _processBilling() {
$this->Billing->set($this->data);

if($this->Billing->validates()) {
return true;
}
return false;
}

protected 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 ...
}
}
?></code></pre>

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.
29 changes: 29 additions & 0 deletions README4.md
Original file line number Diff line number Diff line change
@@ -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:

<pre><code>public function beforeFilter() {
$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3', 'step4'), 'female' => array('step4', 'step5')));
}</code></pre>

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.

<pre><code>public function beforeFilter() {
$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3')), 'step4', array('female' => array('step5')));
}</code></pre>

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 - <code>array(stepName, array(branchName => array(stepName, etc...)))</code> - the steps array can be as complex as resources allow for.
47 changes: 47 additions & 0 deletions README5.md
Original file line number Diff line number Diff line change
@@ -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:

<pre><code>*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')));</code></pre>

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:

<pre><code>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;
}</code></pre>

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.

<pre><code>$steps = array('step1', array('male' => ..., 'female' => ...), 'step2', array('cyborg' => ..., 'male' => ..., 'alien' => ...)); </code></pre>

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.
Loading