Wizard Component 1.2 Tutorial
Automates several aspects of multi-page forms including data persistence, form preparation and unique data processing, wizard resetting (manual and automatic), user navigation, and plot-branching navigation while maintaining flexibility with custom validation and completion callbacks.
This is a tutorial for my Wizard Component 1.2 found here: http://bakery.cakephp.org/articles/view/wizard-component-1-2-1.
This is a tutorial for my Wizard Component 1.2 found here: http://bakery.cakephp.org/articles/view/wizard-component-1-2-1.
If you have ever tried creating a multi-page form, you have probably realized that adding even a second page introduces several new considerations, such as: data persistence, user navigation, page refreshing and double submission, and cleanly handling each page's data validation and error handling. The wizard component automates the nitty gritty that comes with multi-page forms and lets you focus on handling the user's data.
For this example, I am going to be creating a 4 step signup wizard that includes the following steps:
It is important to note that though we will using multiple models, the entire wizard will be contained in one controller. Also, I will be using the words 'step' and 'page' interchangeably - I'm merely referring to a page in the multi-page wizard.
So after downloading wizard.php into our project's component folder, we include it in our controller's $components array just as we would any other component:
Next, we're going to setup our $steps array, which is an ordered list of steps for the wizard to follow. Each step will have its own view and will be processed by its own controller callback method. There is also another optional callback for each step that will be discussed later.
The steps array is setup in your controller's beforeFilter():
Download code
The next step is to create the views used in the signup wizard. The names of the views correspond to steps names included in $steps (account.ctp, address.ctp, etc). I'll include the first view (account.ctp) just to highlight a couple things.
The first thing I want to point out is the url that the form is submitted to. Rather than submitting to the next step in the wizard, each step submits to itself, just as a normal form would do. (My favorite method is above : 'url'=>$this->here.) This is important because one of my main goals in creating this component was to allow the wizard to be easily setup and easily modified. This meant keeping the views divorced, as much as possible, from their inclusion or position in the steps array. To further this goal, I have created a WizardHelper that will be published in the bakery soon. In the above example, "Step 1" would be replaced with the $wizard->stepNumber() method.
The second thing I wanted to highlight was the component's ability to handle data for multiple models (the same as single page forms). This is possible because every step has its own custom callback to process its data.
Simple Example
Lets create a simple example to show how rapidly a multi-page form can be created with the WizardComponent.For this example, I am going to be creating a 4 step signup wizard that includes the following steps:
There will also be a "confirmation" page at the end to confirm the user's signup. This is a very simple example and a wizard like it probably doesn't have a whole lot of real world usefulness, but I just want to demonstrate how the component is used and highlight a couple things as we go along.
- Account Info
- Mailing Address
- Billing Info
- Review
It is important to note that though we will using multiple models, the entire wizard will be contained in one controller. Also, I will be using the words 'step' and 'page' interchangeably - I'm merely referring to a page in the multi-page wizard.
So after downloading wizard.php into our project's component folder, we include it in our controller's $components array just as we would any other component:
Controller Class:
Download code
<?php
class SignupController extends AppController {
var $components = array('Wizard');
}
?>
Next, we're going to setup our $steps array, which is an ordered list of steps for the wizard to follow. Each step will have its own view and will be processed by its own controller callback method. There is also another optional callback for each step that will be discussed later.
The steps array is setup in your controller's beforeFilter():
Download code
function beforeFilter() {
$this->Wizard->steps = array('account', 'address', 'billing', 'review');
}
The next step is to create the views used in the signup wizard. The names of the views correspond to steps names included in $steps (account.ctp, address.ctp, etc). I'll include the first view (account.ctp) just to highlight a couple things.
View Template:
Download code
<?=$form->create('Signup',array('id'=>'SignupForm','url'=>$this->here));?>
<h2>Step 1: Account Information</h2>
<ul>
<li><?=$form->input('Client.first_name', array('label'=>'First Name:','size'=>20,'div'=>false));?></li>
<li><?=$form->input('Client.last_name', array('label'=>'Last Name:','size'=>20,'div'=>false));?></li>
<li><?=$form->input('Client.phone', array('label'=>'Phone Number:','size'=>20,'div'=>false));?></li>
</ul>
<ul>
<li><?=$form->input('User.email', array('label'=>'Email:','size'=>20,'div'=>false));?></li>
<li><?=$form->input('User.password',array('label'=>'Password:','size'=>20,'div'=>false,));?></li>
<li><?=$form->input('User.confirm',array('label'=>'Confirm:','size'=>20,'div'=>false,'type'=>'password'));?></li>
</ul>
<div class="submit">
<?=$form->submit('Continue', array('div'=>false));?>
<?=$form->submit('Cancel', array('name'=>'Cancel','div'=>false));?>
</div>
<?=$form->end();?>
The first thing I want to point out is the url that the form is submitted to. Rather than submitting to the next step in the wizard, each step submits to itself, just as a normal form would do. (My favorite method is above : 'url'=>$this->here.) This is important because one of my main goals in creating this component was to allow the wizard to be easily setup and easily modified. This meant keeping the views divorced, as much as possible, from their inclusion or position in the steps array. To further this goal, I have created a WizardHelper that will be published in the bakery soon. In the above example, "Step 1" would be replaced with the $wizard->stepNumber() method.
The second thing I wanted to highlight was the component's ability to handle data for multiple models (the same as single page forms). This is possible because every step has its own custom callback to process its data.
Comments
Comment
1 excellent
Comment
2 Thank you
I hope this one is a little more straightforward than the last. From the comments I received last time, I'd say the first tutorial was a fail. :)
Comment
3 Great Tutorial
I have a question, could the validated be move to out side of the $processCallback? So $processCallback is only called when the data so far collected has been validated?
I thinks its possible as $this->controller->uses contains all the loaded models and $this->controller->data contains the data, only compare routine is needed to compare the two in a logical way.
I will have a look into the possibility of adding this tonight, but what is your view?
Comment
4 Auto-validate
Glad the component is going to work for you. Are you asking if the validation can be handled by the component automatically rather than having to manually validate?
I don't think that would be hard if that would be useful to a lot of people. To me, it just seemed more practical and more customizable to have the model validation done by the developer in the callback. It allows the data to be manipulated before validation if need be.
Comment
5 Private Actions
I see your point about the manual-validate vs auto-validate, i hadn't thought about the need to manipulate the data before validation.
one other question shouldn't $processCallback eg processAccount be __processAccount. This would make them private actions and not accessible from www.example.com\signup\processAccount
Comment
6 Will fix
Not sure how that slipped by. I think I originally had "private function ..." but took it out to make it php4 compatible. I'll fix the component and update the tutorial today. Thanks.
Comment
7 Elegant when used with useTable=false and multivalidatable
For each wizard I create a Model with $useTable=false and an embedded schema via the Model schema() method that contains all data that the wizard collects.
Model Class:
<?php
<?
/**
* This model handles the validations for the wizard. Data is persisted to
* multiple tables at the end of the wizard process.
*
*/
class WizModel extends AppModel {
var $name = "WizModel";
var $useTable = false;
var $actsAs = array('Multivalidatable');
/**
* Define data we collect during the wizard process
*/
function schema() {
return array(
'id' => array('type' => 'integer', 'null' => '', 'default' =>'1', 'length' => '8', 'key'=>'primary'),
'customer_id' => array('type' => 'integer', 'null' => '', 'default' =>'1', 'length' => '8'),
'customer_name' => array('type' => 'string', 'null' => '', 'default' =>'', 'length' => '64'),
'start' => array('type' => 'date', 'null' => ''),
'end' => array('type' => 'date', 'null' => ''),
'product_id' => array('type' => 'integer', 'null' => '', 'default' =>'1', 'length' => '8')
'store_id' => array('type' => 'integer', 'null' => '', 'default' =>'1', 'length' => '8')
);
}
var $validate = array();
/**
* Validation rules for various steps
*/
var $validationSets = array(
'step1' => array(
'customer_name' => array('rule' => VALID_NOT_EMPTY,'message'=>'Please select a valid customer.'),
'start' => array('rule' => array('date','mdy'),'message'=>'Please select a valid start date.'),
'end' => array('rule' => array('date','mdy'),'message'=>'Please select a valid end date.'),
'product_id' => array('rule' => 'numeric','message'=>'Please select a product.')
),
'step2' => array(
'store_id' => array('rule' => 'numeric','message'=>'Please select a store.'),
)
);
}
?>
?>
For another example of a non-DB model visit:
http://snook.ca/archives/cakephp/contact_form_cakephp/
Info the the multivalidatable behavior is here:
http://bakery.cakephp.org/articles/view/multivalidatablebehavior-using-many-validation-rulesets-per-model
I setup a validation set using $validationSets in the model, one for each wizard step. In the wizard controller, I select the validation set at the beginning of each processXXX() method.
Controller Class:
<?php
/**
* uses multivalidatable behavior to check the values
*/
function processStep1() {
$this->WizModel->setValidation('step1');
$this->WizModel->set($this->data);
if ($this->WizModel->validates()) {
return true;
} else {
return false;
}
}
?>
I didn't see it mentioned in the docs but the wizard info is stored in the $_SESSION as:
{Controller}Wizard
{step}
{model}
value
value
value
{step}
{model}
value
value
value
This provides a great way of handling wizard validations directly in the session allowing you to save information in the session without reference DB models and save at the end of the process.
Comment
8 Elegant but has draw backs
There is one draw back to implementing validation this way, if any of the data is edited outside of the wizard such as a user edit profile routine all the validation rules would need to be duplicated in the user model.
Comment
9 Re: $useTable=false + multivalidatable
Comment
10 afterComplete() and beforeReceipt()
I am leaning towards performing all final database operation in the last processXXX() method then redirecting to another controller in the afterComplete() to display the final 'receipt'. Since the user will be able to later access or print the transaction from a 'My Account' function.
Any thoughts?
Comment
11 Re: afterComplete() and beforeReceipt()
1. beforeReceipt() no longer exists in version 1.2.
2. afterComplete() occurs (as mentioned in the tutorial) after a redirect subsequent to the final processCallback(). Almost immediately afterward, the user is redirected to the $completeUrl. This is where you should set the action for the user to be redirected after the afterComplete() - whether an error page, success page, or wherever.
I do not recommend manually redirecting out of processCallback() because the component resets the wizard data before the $completeUrl redirect. Not resetting the wizard would cause the afterComplete() callback to be run once again if the user were to navigate to /controller/wizard.
Also, the data is still accessible during afterComplete(), it has just been moved to a different location in the session. Among other things, this is to avoid double submission of data. The new location is "Wizard.complete.{stepName}.{model}.{fieldName}" but can still be accessed as you normally would with Wizard->read().
Comment
12 Re: Wizard Helper
Marcus, appreciate the comments! I haven't really developed much on the WizardHelper. I actually just created it because I needed some functionality (a progress menu) for a project I was working on. If anyone would like to see what I've got so far (with a couple examples), just email me (at the email address in the component comments) and I'll email you back.
If I get too many emails though, I'll probably just delete this message and post the "beta" :) version in the bakery.
Question
13 Steps in one method
Comment
14 Re: Steps in one method
It would be best to have a separate controller for each wizards process.
For example if you had your sign up process in your users_controller but also wanted an admin user creation wizard also.
It would be best to create a signup_controller and have it separate from users.
Comment
15 Fix for PBN Issue
I was getting undefined index issue when i used separate branches in the steps array like Example 2 in part 5 of your tutorial.
I tracked down the issue to the parseSteps function.
when it first runs on startup it doesn't have any knowledge what branches it will take, and gets lost parsing the steps.
I have fixed the function, but the steps array need to be in a slightly different format.
Previous Format
$this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3')), 'step4', array('female' => array('step5')));
New Format
For Example 1 on Page 5
$this->Wizard->steps = array('step1', 'step2', 'gender','male' => array('step3', 'step4','female' => array('step4', 'step5')));
For Example 2 on Page 5
$this->Wizard->steps = array('step1', 'step2', 'gender', 'male' => array('step3'), 'step4', 'female' => array('step5'));
New parseSteps() function
function _parseSteps($steps)
{
$parsed = array();
foreach($steps as $key => $value)
{
if(is_array($value))
{
$branchType = $this->_branchType($key);
if ($branchType)
{
if($branchType !== 'skip')
{
$parsed = Set::Merge($parsed, $this->_parseSteps($value));
}
}
}
else
{
$parsed[] = $value;
}
}
return $parsed;
}
There are only 3 more changes left to make.
Insert the below line just before the close bracket for
branch and unbranch functions.
$this->steps = $this->_parseSteps($this->_steps);
Last change is to add the below line after the if statement in the startup function
$this->_steps = $this->steps;
the above changes modifies the $this->steps to hold information on the path that it needs to taken and not every branch to could possible take.
$this->steps then gets updated after every branch or unbranch.
Comment
16 be back soon
Question
17 very nice
After having gone halfway through the wizard (current step "address") and then modifying the url to step "review" drops me the error "Warning (512): Step validation: review is not a valid step". This is fine, but since my $autoReset = false, isn't it supposed to jump back to address?
I also experience problems with the form creation in your example:
'url'=>$this->here won't work. I have to write 'url' => 'wizard/STEPNAME' instead. The form automatically prepends the path my cakephp is installed in.
Using $this->here brings me to action="/path/to/cake/path/to/cake/wizard/step" while my installation is in "/path/to/cake". Anyone else having this troubles?
Thanks a lot!
Comment
18 Re: very nice
That is a developer warning. It goes away when your debug level in core.php is 0.
As for your other problem I'm not sure. I've always used $this->here for all my forms and it's always worked. Might ask in #cakephp.
Comment
19 intuitivity
I think there are two issues here. One is the name of defaultBranch which is not very descriptive. Perhaps branchNavigation (or the more geeky chic 'pBN') would more intuitively and accurately describe the property. The other issue is with the second parameter to branch(). I think you should reverse the logic, so branch(male,false) means "skip the male branch."
Those are fairly minor issues, however, and I congratulate you on delivering your 'baby' while rassling twins. Whew!! Hats off.
Comment
20 Issue: File Uploading
I have noticed an issue if you are handling file uploads, if you have uploaded a file on a step and want to rename or move the file in _afterComplete it's not possible as the file has been deleted by php.
The file is delete due to a redirect, this redirect causes php to think you have finished with the file and so deletes it.
The work around is to move the file in the _processCallBack and update the tmp_name path then the file will be available in _afterComplete.
Comment
21 Wonderfull
Wonderfull tutorials here @bakery
Keep up the good work! I love it!!
Comment
22 DOUBLE-path-problem
did you already solve that mystery about the double paths in the $this->here var?
i have the some problem with this component (although this seems to be more a cake bug than a problem of this component^^)
a pain in the ... though, that the forms always post to incorrect paths.
thx
Comment
23 Bug ? - Problems with Security Component
first of all: thanks for a great component. It works really well, but we discovered some strange issues yesterday i want to share:
the wizard was working perfectly. the first wizard had 8 steps, an worked. the second wizard had 7 steps, but on the second step, there was only a blank white page. long stor short: we had the security component loaded without using it and it seemed to caused trouble. after removing it from the app_controller everything worked fine.
maybe you can look into this, cause i did not have the time to go into detail with this.
hopes this saves some of you time.
Comment
24 Re: Bug ? - Problems with Security Component
Comment
25 Multiple model validation in one step
Comment
26 Re:Multiple model validation in one step
Hi Peter,
Here is how to validate two models
$this->Client->set($this->data);
$this->User->set($this->data);
if($this->Client->validates() && $this->User->validates()) {
return true;
}
return false;
Comment
27 I tried it but...
Thanks for your reply, Penfold!
I've already tried this method but it shows error messages only for one Model and not for both, even when both Models' fields are invalid.
Comment
28 of course
this is PHP :)
if you know how if () elseif() else works, you will be able to explain that.
its quite simple
AND connects both, which means, they will have to be both TRUE to go on.
any "higher" programming language does not look on the following conditions if the first one already failed!
if (true && false && true) {}
-> the 3rd one will not be checked on if the second one returns false
to solve it, never use them together inside an if clause:
$val1 = $this->Client->validates();
$val2 = $this->User->validates();
if($val1 && $val2) {
return true;
}
return false;
happy validating :)
Comment
29 How to validate passwords?
Comment
30 Re: How to validate passwords?
Peter, this has to do with AuthComponent, not the WizardComponent.
See: http://book.cakephp.org/view/565/Troubleshooting-Auth-Problems
Comment
31 small suggestion..
also, i was getting
empty($this->controller->data)
because the form was incorrectly sending users to /signup/wizard, which was then forwarding them to /signup/wizard/address with no data. this was a cake bug, but it might be nice to carry the data over?
also, the underscore in _processAddress etc still needs to be updated in the tutorial
Comment
32 _getExpectedStep
Comment
33 Re: bo
1. Are you trying to access the expectedstep in the controller or in the view.
2. It only returns false if $this->steps hasn't been setup. Otherwise, the first step not completed (saved in the Session) is returned.
Comment
34 Re: jaredhoyt
controller. (ps, reading what i wrote i think i misspoke-- sorry)
understood, but imagine this scenario: you complete steps 1,2,3 and then send the user back to step 2, or he does it manually for some reason. _getExpectedStep() will return step 3 instead of the current step you're viewing, which is step 2.
you can get the step easily in something like wizard($step), but one may need to set the step earlier. this leaves:
if (isset($this->passedArgs[0]) && isset($this->steps[$this->passedArgs[0]]))
{
inside of beforeFilter(), which seems a little clunky. would be nice if there was a cleaner method for this, as it seems a pretty standard requirement to know which step you're viewing.
Comment
35 Re: jaredhoyt
1. user completes step 1, is sent to step 2
2. user manually goes back to step 1 and invalidates one of the fields in some way
this means that step one is incomplete/invalid, but the wizard component will still send them over to step two from _getExpectedStep().
another problem:
1. user branches to step 3, goes back to step 2.
2. user branches elsewhere, to step 4
user will be sent back to step 3. various fixes still produce a double forward before the steps array is rebuilt properly. this problem seems a little more complicated than an easy fix, but this works in the meantime.. when re-branching:
$this->Wizard->unbranch($this->Wizard->_getExpectedStep());
$this->Wizard->branch('NEW-BRANCH');
// Reset your Wizard->steps array. Mine is located in $this->steps (controller)
$this->Wizard->steps = $this->steps;
$this->Wizard->steps = $this->Wizard->_parseSteps($this->Wizard->steps);
$this->Wizard->config('steps', $this->Wizard->steps);
return true;
should branch() automatically remove older branches?
Also, line 397 in wizard.php, within _parseSteps(), produces non-existent index warnings and should be changed to this:
if(!empty($branch) && isset($name[$branch])) {
Comment
36 component vanished
Comment
37 Latest Wizard Crashes Apache?
Comment
38 re: latest wizard
strange! i'm running the latest wizard code (to my knowledge), but wanted to pull the original down again. no crashes for me under apache.
Comment
39 Crash Remains
Comment
40 Re: Latest Wizard Crashes Apache?
The component has 'disappeared' because I made a small bug fix and resubmitted the it to the bakery. It takes a few days for approval. Sorry for any inconvenience.
As for the component crashing your Apache service, I'm not sure. I'm running compiled versions of Apache/PHP on OSX Leopard and it's working fine. Maybe check your error logs.
Comment
41 Process Callback not found
function processAccount()....
to this:
function _processAccount()...
and it worked!
Comment
42 Step 1 - ok, step 2 - ??
Firstly, thanks for your investment of time in creating this component!
I'm a bit of a CakePHP noob (and have not done any web development for a few years), so please excuse my ignorance and feel free to tell me to RTFM if it's something obvious I've missed.
I've followed your tutorial as closely as possible, however I'm finding when I submit the form in step 1, nothing is displayed and I get an error about headers already being sent - I believe this is stopping it from redirecting to step 2.
The warning and trace is:
Warning (2): Cannot modify header information - headers already sent by (output started at /var/www/opes/cake/basics.php:111) [CORE/cake/libs/controller/controller.php, line 615]
Code | Context
$status = "Location: http://localhost/opes/new_property/wizard/property"
header - [internal], line ??
Controller::header() - CORE/cake/libs/controller/controller.php, line 615
Controller::redirect() - CORE/cake/libs/controller/controller.php, line 596
WizardComponent::redirect() - APP/controllers/components/wizard.php, line 312
WizardComponent::process() - APP/controllers/components/wizard.php, line 208
NewPropertyController::wizard() - APP/controllers/new_property_controller.php, line 16
Object::dispatchMethod() - CORE/cake/libs/object.php, line 117
Dispatcher::_invoke() - CORE/cake/dispatcher.php, line 245
Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 211
[main] - APP/webroot/index.php, line 88
I've had to implement some of the workarounds mentioned in the comments (prefixing call back functions with _ and hard setting the URL for the form). My controller is pretty straight forward and is 90% based on the example you have provided.
Any insight would be appreciated.
Kind Regards,
Jon
Comment
43 Resolved..
Never mind, found the culprit - a trailing space on the end of my controller.
D'oh!
Question
44 header error issues
Warning (2): Cannot modify header information - headers already sent by (output started at /export/httpd/data/architecture/sitemanager/cake/libs/configure.php:476) [CORE/cake/libs/controller/controller.php, line 577]
I don't think I have a trailing space on the end of my controller (or I might not be looking in the right place). I am being forced to run in PHP safe mode but I haven't had a problem with CakePHP like this before.
Thanks for any suggestions.
Comment
45 problem accessing the posted data
Component Class:
<?php
function process($step) {
if (isset($this->controller->params['form']['Cancel'])) {
if (method_exists($this->controller, '_beforeCancel')) {
$this->controller->_beforeCancel($this->_getExpectedStep());
}
$this->resetWizard();
$this->controller->redirect($this->cancelUrl);
}
if (empty($step)) {
if ($this->Session->check('Wizard.complete')) {
if (method_exists($this->controller, '_afterComplete')) {
$this->controller->_afterComplete();
}
$this->resetWizard();
$this->controller->redirect($this->completeUrl);
}
$this->autoReset = false;
} elseif ($step == 'reset') {
if (!$this->lockdown) {
$this->resetWizard();
}
} else {
if ($this->_validStep($step)) {
$this->_setCurrentStep($step);
if (!empty($this->controller->data) && !isset($this->controller->params['form']['Previous'])) {
?>
My view file is as follows
View Template:
<?php e($form->create('Profile',array('url'=>'/profiles/wizard/','type'=>'post')));?>
<h2>Step 1: Personal Information</h2>
<ul>
<li><?=$form->input('last_name', array('label'=>'Last Name:','size'=>20,'div'=>false));?></li>
</ ul>
<div class="submit">
<?php e($form->submit('Continue', array('div'=>false)));?>
</div>
<?=$form->end();?>
the code for my controller ;
Controller Class:
<?php
function beforeFilter() {
$this->Wizard->steps = array('personal', 'travel', 'visa', 'review');
}
function wizard($step = null) {
$this->Wizard->autoValidate =false;
$this->Wizard->process($step);
}
function _preparePersonal(){
//pr($this);die();
echo "preparing the personal form";
}
function _processPersonal(){
$this->Profile->set($this->data);
return true;
}
function processTravel() {
$this->Application->set($this->data);
return true;
}
?>
i am using cake version 1.2.2.8120 on ubuntu 8,10 with mysql v5 + php v5.2.6
Any help will be appreciated
Comment
46 Re: problem accessing the posted data
Kiti, I think the problem is your form url in your view. Every step view needs to post to itself (step included in the url)... You can either do this manually on each form ('url' => '/profiles/wizard/personal' for example) or just use $this->here : ('url' => $this->here).
The problem with posting to '/profiles/wizard/' is that the $step is null so WizardComponent::process() redirects to the "expected step" which is where you're losing your POSTed data.
Question
47 How di I save data before the final step
My application is such that one should be able to resume the process at another time without necessarily starting afresh.
I want to be able to save data at every stage.
Please assist
Question
48 How di I save data before the final step
My application is such that one should be able to resume the process at another time without necessarily starting afresh.
I want to be able to save data at every step.
here is my controller code
Controller Class:
<?php
function beforeFilter(){
parent::beforeFilter();
$this->Wizard->steps = array('personal','passport','travel','justification','visit','payment');
$this->Wizard->completeUrl = '/applications/confirm';
}
function confirm() {
$refno = $this->Application->find('first', array('conditions'=>array('Application.profile_id'=>$this->Session->read('Auth.User.profile_id')), 'order'=>'Application.created desc','recursive'=>'-1'));
$this->set('applications', $refno);
}
function wizard($step =null){
$this->Wizard->process($step);
}
function _preparePersonal(){
$this->set('applications', $this->Application->Profile->find('first', array('conditions'=>array('Profile.user_id'=>$this->Session->read('Auth.User.profile_id')))));
$this->set('countries',$this->Application->Profile->Country->find('list'));
}
function _processPersonal(){
$this->set('profiles', $this->Application->Profile->find('first', array('conditions'=>array('Profile.user_id'=>$this->Session->read('Auth.User.profile_id')))));
return true;
}
function _preparePassport(){
$this->set('countries',$this->Application->Profile->Country->find('list'));
}
function _processPassport(){
return true;
}
function _prepareTravel(){
$this->set('countries',$this->Application->Profile->Country->find('list'));
$this->set('visatypes', $this->Application->Visatype->find('list'));
}
function _processTravel(){
return true;
}
function _prepareJustification(){
$this->loadModel('Entryreason');
$this->set('entryreasons', $this->Application->Entryreason->find('list'));
}
function _processJustification(){
return true;
}
function _processVisit(){
return true;
}
function _processPayment(){
return true;
}
function _afterComplete() {
$wizardData = $this->Wizard->read();
extract($wizardData);
$personal['Profile']['id'] = $this->Session->read('Auth.User.profile_id');
if( !$this->Application->Profile->save($personal['Profile'])){
die("problem updating profile");
}
$application = array_merge($justification['Application'],$passport['Application'],$travel['Application']);
$application['profile_id'] = $this->Session->read('Auth.User.profile_id');
if (!$this->Application->save($application)){
die("probem saving app");
}
$visit['Visit']['profile_id']=$this->Session->read('Auth.User.profile_id');
if (!$this->Application->Profile->Visit->save($visit)){
die("problem updating visits");
}
}
?>
Is there a better way I can implement this logic?
Please assist
Comment
49 Warning (512): Step validation: xxxx is not a valid step
I'm having problems with this warning too when using PBN. Setting 'debug' to 0 in core.php does make the warning go away. But is this really a solution. What is the purpose of the warning?
It looks like the problem is in the method _validStep() it uses in_array() to check if the $step is in the $steps array. But won't this not work with PBN because in_array() only works with single dimension arrays?
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;
}
Any help will be greatly appreciated.
Ishmael
Question
50 Repeating steps
Can this component do the following:
Logic: If some submited variable says "repeat", repeat the active step.
if it says next, go to the next step.
This of course without loosing the inserted data on the previous form submissions.
Comment
51 Source code for this example
Thanks,
Durga
Question
52 Location of ctp files
Thanks,
Durga
Question
53 there is something missing, please tell me...
i have set everything up, cakephp-framework is doing fine
wizard.php is in the component folder
i have a signup_controller.php
Controller Class:
<?php
class SignupController extends AppController {
var $components = array('Wizard');
var $uses = array('Client');
function beforeFilter() {
$this->Wizard->steps = array('account');
}
function wizard($step = null) {
$this->Wizard->process($step);
}
function _processAccount() {
$this->Client->set($this->data);
if($this->Client->validates()) {
return true;
}
return false;
}
function afterComplete() {
$wizardData = $this->Wizard->read();
extract($wizardData);
$this->Client->save($account['Client'], false, array('vorname', 'name'));
}
}
?>
a simple Client.php Model:
Model Class:
<?phpclass Client extends AppModel
{
var $name = 'Client';
}
?>
and also a simple account.ctp
View Template:
<?=$form->create('Signup',array('id'=>'SignupForm','url'=>$this->here));?>
<h2>Step 1: Account Information</h2>
<ul>
<li><?=$form->input('Client.vorname', array('label'=>'First Name:','size'=>20,'div'=>false));?></li>
<li><?=$form->input('Client.name', array('label'=>'Last Name:','size'=>20,'div'=>false));?></li>
</ul>
<div class="submit">
<?=$form->submit('Continue', array('div'=>false));?>
<?=$form->submit('Cancel', array('name'=>'Cancel','div'=>false));?>
</div>
<?=$form->end();?>
my database is up and running with the expected table
DROP TABLE IF EXISTS `clients`;
CREATE TABLE `clients` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) default NULL,
`vorname` varchar(255) default NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
but nothing is saved in the database...
what is missing?
cheers manuel
Comment
54 app/views/signup/account.ctp
for the tutorial
Bug
55 found the mistake
afterComplete() is wrong:
_afterComplete() is correct so!
Controller Class:
<?php
class SignupController extends AppController {
var $components = array('Wizard');
var $uses = array('Client');
function beforeFilter() {
$this->Wizard->steps = array('account');
}
function wizard($step = null) {
$this->Wizard->process($step);
}
function _processAccount() {
$this->Client->set($this->data);
if($this->Client->validates()) {
return true;
}
return false;
}
function _afterComplete() {
$wizardData = $this->Wizard->read();
extract($wizardData);
$this->Client->save($account['Client'], false, array('vorname', 'name'));
}
}
?>
Question
56 image uploading
Bug
57 Invalid url if $this->here
Thanks,
Durga
Question
58 method signature _prepareStep1 or _prepare_Step1
In the wizard.php that i have downloaded the procss($step) method is having the following code
$processCallback = '_' . Inflector::variable('process_' . $this->_currentStep);
if (method_exists($this->controller, $processCallback))
as per the above code do we have to define the process step as _process_step1
Thanks,
Durga
Comment
59 Branching:
First of all many thanks for your great job.
I hope your twins are doing well and that you are an happy father.
Although I think the component is just great, it is too bad that the branching mechanism is broken:
I have to run complex wizards and get the same pbm as Penfold: Getting undefined index issue when i used separate branches in the steps array like Example 2 in part 5 of your tutorial.
For now I applied Penfold's fix, but you really should fix this.
BTW your tutorial is also really well written.
Thanks again.
Comment
60 Why not?
Comment
61 header errors
I'm a total noob to Cake. So I've spent much of the day trying to get the darn thing to do what I want.
First a note on the header error a few have mentioned
I had this issue on my web server running php 4.3.9 but not on my local server runnin php 5.0. Finally I found this article
[url=http://www.tech-recipes.com/rx/1489/solve-php-error-cannot-modify-header-information-headers-already-sent/]http://www.tech-recipes.com/rx/1489/solve-php-error-cannot-modify-header-information-headers-already-sent/
that convinced me the only answer is I was missed something obvious. that's when I found the trailing lines at the bottom of my wizard component.
Other issue I had was trying to figure out how to start in the middle of the wizard based on variables already gathered else where. I finally figured this out
Controller Class:
<?php$this->Wizard->steps=array(array('package'=>array('package_detail'),'custom'=>array('custom_detail')),'verify');
$this->Wizard->branch($this->params['url']['type']);
?>
In this case I passed the branch I want to use through GET. But you could pull the variable(s) from anywhere.
Hope this helps other noobs out there.
Comment
62 GREAT component
Great work.
Bug
63 $this->here issue
Question
64 Passing URL args to Wizard
I need to pass a variable in the URL to the first step of the Wizard such as the id of another model record (through a url link)
ie /model_with_wizard/wizard/step1/id
I intend to use the wizard to add a related record so obviously need to know the id of the parent model record. Such as adding a contact record to a company record. (Hope that makes sense!)
Is there any way of doing this?
Cheers
Comment
65 RE: Passing URL args to Wizard
Add an instance variable to your controller for the parentid:
var $parentid = null;
In your controller's wizard method, modify so that it accepts 2 params like so:
function wizard($step = null, $parent_id = null) {
if ($parent_id != null) {
$this->parentid = $parent_id
}
$this->Wizard->process($step);
}
Now, in your '_prepareStep' function for your first step, you will have accees to the parentid via '$this->parentid'.
Comment
66 Cheers!
Great thanks! Mucho appreciated!
Tried something like that but being fairly new to Cake, I was probably a little bit short of the full solution!
Just quickly tried your suggestion and it works!
Kudos
Comment
67 RE: $this->here bug
This could be related to a mod_rewrite problem. Check that your .htaccess files are correctly in place. Also, the variable that $this->here references is closely tied to a baseUrl setting in your ./config/core.php file. Check the comments on App.baseUrl. Mine is commented out and I use mod_rewrite.
(I deduced the above with some greps on cake's lib directories. If I m way offbase, then sorry.... :) I'd help more if I could reproduce on my end.)
$this->here is a variable on cake's default controller class, and it's not even being referenced in the WizardComponent. If it is broken, it is a cake core issue, not a wizard issue.
Comment
68 headers already sent by possible solution
Make sure that if you are in step "foobar", you have the form url of that step directed to "foobar" as well and not the next step. The form must post to itself.
Plz correct me if I'm wrong.
Jaime!