Wizard Component 1.2 Tutorial

By jaredhoyt (jaredhoyt)
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.
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.

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:

  1. Account Info
  2. Mailing Address
  3. Billing Info
  4. Review

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.

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.

Page 2: View Preparation and Data Processing

Comments 774

CakePHP Team Comments Author Comments
 

Comment

1 excellent

A very well written tutorial!
Posted Nov 18, 2008 by Hannibal Lecter
 

Comment

2 Thank you

A very well written tutorial!
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. :)
Posted Nov 18, 2008 by jaredhoyt
 

Comment

3 Great Tutorial

This component is going to save so much time so a big thank you.

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?
Posted Nov 19, 2008 by Penfold
 

Comment

4 Auto-validate

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?

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.
Posted Nov 19, 2008 by jaredhoyt
 

Comment

5 Private Actions

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.

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
Posted Nov 19, 2008 by Penfold
 

Comment

6 Will fix

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
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.
Posted Nov 19, 2008 by jaredhoyt
 

Comment

7 Elegant when used with useTable=false and multivalidatable

I have implemented a solution that is very elegant using this component, non-db models and the multivalidatable behavior.

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.
Posted Nov 20, 2008 by David Bennett
 

Comment

8 Elegant but has draw backs

I have implemented a solution that is very elegant using this component, non-db models and the multivalidatable behavior.

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.

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.

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.
Posted Nov 22, 2008 by Penfold
 

Comment

9 Re: $useTable=false + multivalidatable

Yes, I agree, it doesn't always apply. In my 'primary' model, I want to validate the entire record before saving, but during the wizard I only want to validate small bits and implement some wizard specific validations for capacities, etc... However, there is some duplication of validations across models. It would be nice if there was a way to inherit certain validation rules from one model to another. Perhaps this is would be a good candidate for a new behavior... InheritValidation.
Posted Nov 24, 2008 by David Bennett
 

Comment

10 afterComplete() and beforeReceipt()

I am wondering about the usability of these methods. They seem have limited accessibility between controller and view. Database actions performed in afterComplete() don't have an obvious way of reporting failures to the user and beforeReceipt() no longer has access to the collected wizard data and appears to not allow $this->set to provide dynamic information.

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?
Posted Nov 24, 2008 by David Bennett
 

Comment

11 Re: afterComplete() and beforeReceipt()

I am wondering about the usability of these methods. They seem have limited accessibility between controller and view. Database actions performed in afterComplete() don't have an obvious way of reporting failures to the user and beforeReceipt() no longer has access to the collected wizard data and appears to not allow $this->set to provide dynamic information.
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().
Posted Nov 25, 2008 by jaredhoyt
 

Comment

12 Re: Wizard Helper

I was wondering as to the status of the helper you hinted at...
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.
Posted Dec 1, 2008 by jaredhoyt
 

Question

13 Steps in one method

How can I have multiple wizards within one controller?
Posted Dec 4, 2008 by Chey
 

Comment

14 Re: Steps in one method

How can I have multiple wizards within one controller?
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.
Posted Dec 5, 2008 by Penfold
 

Comment

15 Fix for PBN Issue

Hi,

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.

Posted Dec 5, 2008 by Penfold
 

Comment

16 be back soon

Hey guys.my wife delivered twins a couple days ago so can't respond to everything right now.but I'm sure I'll be back soon.
Posted Dec 6, 2008 by jaredhoyt
 

Question

17 very nice

I finally got it working. Very nice piece of work. However, I have two issues:

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!
Posted Dec 7, 2008 by dennis
 

Comment

18 Re: 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?
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.
Posted Dec 7, 2008 by jaredhoyt
 

Comment

19 intuitivity

I guess if W can make up words, so can I. Anyway, I am just at the reading stage, and this all looks very nifty. However, I might suggest a small(?) change for the next version. The code "branch('male', true)" is not very intuitive. Without re-reading your tutorial a few times, it would not make sense, as I would expect this to mean "take the male branch". Instead, if I understand correctly, this means "skip the male branch."

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.

Posted Dec 18, 2008 by webdev
 

Comment

20 Issue: File Uploading

Hi,

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.
Posted Dec 22, 2008 by Penfold
 

Comment

21 Wonderfull

I registered only to comment...

Wonderfull tutorials here @bakery

Keep up the good work! I love it!!
Posted Jan 4, 2009 by Nico Sap
 

Comment

22 DOUBLE-path-problem

hey, dennis (and others),

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
Posted Jan 5, 2009 by Mark
 

Comment

23 Bug ? - Problems with Security Component

Hi there,

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.

Posted Jan 30, 2009 by Stefan
 

Comment

24 Re: Bug ? - Problems with Security Component

@stefan: I'm going out of town this weekend, but I'll look at this as soon as I can next week. Thank you!
Posted Jan 30, 2009 by jaredhoyt
 

Comment

25 Multiple model validation in one step

I'm trying to validate two Models in one step. I tried both - $autoValidate and validation in callback functions. But it shows error messages only for one Model and not for both, even when both Models' fields are invalid are invalid.
Posted Feb 4, 2009 by Peter Shevtsov
 

Comment

26 Re:Multiple model validation in one step

I'm trying to validate two Models in one step. I tried both - $autoValidate and validation in callback functions. But it shows error messages only for one Model and not for both, even when both Models' fields are invalid are invalid.
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; 
Posted Feb 4, 2009 by Penfold
 

Comment

27 I tried it but...

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; 

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.
Posted Feb 4, 2009 by Peter Shevtsov
 

Comment

28 of course

of course it does show only errors on the first validation rule
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 :)
Posted Feb 4, 2009 by Mark
 

Comment

29 How to validate passwords?

It seems that when I try to validate password field it tries to validate already hashed string. How to validate it?
Posted Feb 5, 2009 by Peter Shevtsov
 

Comment

30 Re: How to validate passwords?

It seems that when I try to validate password field it tries to validate already hashed string. How to validate it?
Peter, this has to do with AuthComponent, not the WizardComponent.

See: http://book.cakephp.org/view/565/Troubleshooting-Auth-Problems
Posted Feb 5, 2009 by jaredhoyt
 

Comment

31 small suggestion..

in your tutorials, it would be helpful to not assume that users were familiar enough with cake to "insert x into your view/controller", and actually include and explain the default paths. database information like the Clients model as well. it may seem redundant to people with experience, but it can't hurt and just makes the tutorial more polished. thank you for your hard work!

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
Posted Feb 19, 2009 by bo
 

Comment

32 _getExpectedStep

recommend public method to get current step. would come in very handy, and i'm having to hack this out a bit as is. _getExpectedStep is close, but should it return the first step instead of false?
Posted Feb 25, 2009 by bo
 

Comment

33 Re: bo

recommend public method to get current step. would come in very handy, and i'm having to hack this out a bit as is. _getExpectedStep is close, but should it return the first step instead of false?

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.
Posted Mar 7, 2009 by jaredhoyt
 

Comment

34 Re: jaredhoyt

1. Are you trying to access the expectedstep in the controller or in the view.
controller. (ps, reading what i wrote i think i misspoke-- sorry)

2. It only returns false if $this->steps hasn't been setup. Otherwise, the first step not completed (saved in the Session) is returned.
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.
Posted Mar 8, 2009 by bo
 

Comment

35 Re: jaredhoyt

one more problem:

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])) {

Posted Mar 8, 2009 by bo
 

Comment

36 component vanished

the wizard component seems to have vanished from the cake site.
Posted Mar 8, 2009 by bo
 

Comment

37 Latest Wizard Crashes Apache?

I had downloaded the latest wizard code, linked to by this article, and replaced the wizard class I had been using previously. The new wizard code would crash my apache everytime I tried to proceed from one step to the next. Replace new wizard with old wizard, everything works. Not sure if this is at all related to the disappearing Wizard component, but its quite the coincidence.
Posted Mar 9, 2009 by Shane
 

Comment

38 re: latest wizard

Not sure if this is at all related to the disappearing Wizard component, but its quite the coincidence.
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.
Posted Mar 9, 2009 by bo
 

Comment

39 Crash Remains

I have downloaded the latest Wizard code (again) and advancing to the next step in my application causes apache to crash. Everytime. If the validation fails, everything is fine. But if it succeeds apache will crash. Any thoughts? I am running cake 1.2 on WAMP server 2.0
Posted Mar 9, 2009 by Shane
 

Comment

40 Re: Latest Wizard Crashes Apache?

Not sure if this is at all related to the disappearing Wizard component, but its quite the coincidence.
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.
Posted Mar 10, 2009 by jaredhoyt
 

Comment

41 Process Callback not found

I was getting this error, after I tried to implement this. I noticed within the error message it referred to my controller process method with an underscore in front of it. So I changed my process method from this:

function processAccount()....

to this:
function _processAccount()...

and it worked!
Posted Mar 11, 2009 by Justin Kennedy
 

Comment

42 Step 1 - ok, step 2 - ??

Hi there

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
Posted Mar 15, 2009 by Jon Austin
 

Comment

43 Resolved..

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]

Never mind, found the culprit - a trailing space on the end of my controller.

D'oh!
Posted Mar 15, 2009 by Jon Austin
 

Question

44 header error issues

I'm having the same issue as the person above. After I submit the first screen, I get this header error message:

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.

Posted Mar 23, 2009 by Christopher Rankin
 

Comment

45 problem accessing the posted data

First of all, the wizard component is great but i am having problems accessing submitted data during my _processCallback functions. I have gon through the code and cant seem to locate the point where i cant access my data the Wizard component line

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
Posted Mar 25, 2009 by kiti chigiri
 

Comment

46 Re: problem accessing the posted data

I have gone through the code and cant seem to locate the point where i cant access my data the Wizard component line

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();?> 

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.
Posted Mar 25, 2009 by jaredhoyt
 

Question

47 How di I save data before the final step

Thanks for the great component. I've set it up and its working perfectly.
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
Posted Apr 1, 2009 by juma vincent
 

Question

48 How di I save data before the final step

Thanks for the great component. I've set it up and its working perfectly.
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
Posted Apr 1, 2009 by juma vincent
 

Comment

49 Warning (512): Step validation: xxxx is not a valid step

Warning (512): Step validation: image is not a valid step. [APP/controllers/components/wizard.php, line 232]

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
Posted Apr 10, 2009 by Ishmael Riles
 

Question

50 Repeating steps

Hi,

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.
Posted Apr 24, 2009 by Joel Calado
 

Comment

51 Source code for this example

Can you give the source code for the example that you have given in this tutorial. I want to check and want to understand more before using this component.

Thanks,
Durga
Posted Apr 27, 2009 by Durga Prasad
 

Question

52 Location of ctp files

In views folder under which folder we have to place these account.ctp address.ctp and billing.ctp ?

Thanks,
Durga
Posted Apr 29, 2009 by Durga Prasad
 

Question

53 there is something missing, please tell me...

okay, this is keeping me busy for some time now... but i can't get behind it:
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:

<?php 
    
class 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
Posted May 3, 2009 by Manuel Klarmann
 

Comment

54 app/views/signup/account.ctp

In views folder under which folder we have to place these account.ctp address.ctp and billing.ctp ?

Thanks,
Durga
app/views/signup/account.ctp
for the tutorial
Posted May 3, 2009 by Manuel Klarmann
 

Bug

55 found the mistake

okay, this really wasn't easy for me (beginner) to find out!
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')); 

    } 

?>

Posted May 3, 2009 by Manuel Klarmann
 

Question

56 image uploading

hey, i tried this component with file uploading one, and it seems that the wizard skips the uploading step. I have two steps in my wizard, the second being the one with image upload form. But when i submit i only get names, but i can't process files... what am i missing?
Posted May 5, 2009 by Tomaz Zaman
 

Bug

57 Invalid url if $this->here

In the form tag if i use the url as $this->here the base url is getting added, i.e if my url is http://localhost/test then for step2 it is giving as http://localhost/test/test/wizard/step1. If i use the direct steps in url that is if i give the url='/wizard/step2' it is just showing a blank page. Could some one help us how to fix this. I am new to cakePhp.

Thanks,
Durga
Posted May 8, 2009 by Durga Prasad
 

Question

58 method signature _prepareStep1 or _prepare_Step1

Hi,

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
Posted May 9, 2009 by Durga Prasad
 

Comment

59 Branching:

Hello,

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.
Posted May 9, 2009 by jerome
 

Comment

60 Why not?

Why isn't this component not part of the CakePHP standard distribution?!
Posted May 9, 2009 by jerome
 

Comment

61 header errors

First of all this is a GREAT!! component. And what it makes it better then most is your obvious dedication to it. Thank you very much.

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
Cannot modify header information - headers already sent by ....
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.
Posted May 11, 2009 by Dane
 

Comment

62 GREAT component

This component helped improve user experience in my application a great deal. I also wanted to mention that it works great as an 'admin_' action in my app. All I had to do was change the primary function from 'wizard' to 'admin_wizard' and then I had a protected wizard.

Great work.
Posted Jun 4, 2009 by tyler
 

Bug

63 $this->here issue

I am new to CakePHP and I think this is an excellent wizard, if only I could get pass the $this->here URL problem. I have noticed that several people have mentioned this but no one has answer why a duplicate path is generated. Stuck until I can resolve this. Also, I had to change the HTML/PHP in the views from "!= to !php echo". I guess that that is a PHP configuration bug. Anyway, any help with the $this->here would be appreciated.
Posted Jun 6, 2009 by Ed
 

Question

64 Passing URL args to Wizard

Great Wizard but one issue...

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
Posted Jun 10, 2009 by David W
 

Comment

65 RE: Passing URL args to Wizard

Great Wizard but one issue...

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

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'.
Posted Jun 10, 2009 by tyler
 

Comment

66 Cheers!

Great Wizard but one issue...

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

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

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
Posted Jun 10, 2009 by David W
 

Comment

67 RE: $this->here bug

I am new to CakePHP and I think this is an excellent wizard, if only I could get pass the $this->here URL problem. I have noticed that several people have mentioned this but no one has answer why a duplicate path is generated. Stuck until I can resolve this. Also, I had to change the HTML/PHP in the views from "!= to !php echo". I guess that that is a PHP configuration bug. Anyway, any help with the $this->here would be appreciated.
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.
Posted Jun 10, 2009 by tyler
 

Comment

68 headers already sent by possible solution

First a note on the header error a few have mentioned
Cannot modify header information - headers already sent by ....
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

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!
Posted Jun 17, 2009 by Jaime Martinez