Wizard Component 1.2 Tutorial

This article is also available in the following languages:
By 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.
[07/10/10] Update: The code and tutorial has been moved to github and can be found at http://github.com/jaredhoyt.


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:

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


    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:


<?=$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.


Next we are going to setup our controller to handle each of the steps in the form wizard.

Very important: Rather than creating a separate controller action for each of the steps in the form, all the steps are tied together through one action (the default is 'wizard'). This means, for our example, our urls will look like http://www.example.com/signup/wizard/account etc. This way, everything is handle by the component and customization is handled through controller callbacks.

Because of this, the wizard action itself can be very basic. It merely needs to pass the step requested to the component's main method - process():

Controller Class:

<?php 
class SignupController extends AppController {
    var 
$components = array('Wizard');

    function 
beforeFilter() {
        
$this->Wizard->steps = array('account''address''billing''review');
    }

    function 
wizard($step null) {
        
$this->Wizard->process($step);
    }
}
?>

Something to consider if your wizard is the controller's main feature (as it would be in our example), is to route the default action for the controller to the wizard action. This would allow prettier links such as http://www.example.com/signup to be handled by SignupController::wizard(), which would then redirect to /signup/wizard/account (or the first incomplete step in the wizard.)

Router::connect('/signup', array('controller' => 'signup', 'action' => 'wizard'));
Next, we are going to create controller callbacks to handle each step. Each step has two controller callbacks: prepare and process.

The prepare callback is optional and occurs before the step's view is loaded. This is a good place to set any data or variables that you want available for the view. The name of the callback is prepareStepName. So for our example, our prepare callbacks would be prepareAccount(), prepareAddress(), etc.

The process callback is required and occurs after data has been posted. This is where data validation should be handled. The process callback must return either true or false. If true, the wizard will continue to the next step; if false, the user will remain on the step and any validation errors will be presented. The name of the callback is processStepName. So for our example, our process callbacks would be processAccount(), processAddress(), etc. You do not have to worry about retaining data as this is handled automatically by the component. Data retrieval will be discussed later in the tutorial.

It's very important to note that every step in the wizard must contain a form with a field. The only way for the wizard to continue to the next step is for the process callback to return true. And the process callback is only called if $this->data is not empty.

So lets create some basic process callbacks. Real world examples would most likely be more complicated, but this should give you the basic idea (don't forget to add any needed models):

Controller Class:

<?php 
class SignupController extends AppController {
    var 
$uses = array('Client''User''Billing');
    var 
$components = array('Wizard');

    function 
beforeFilter() {
        
$this->Wizard->steps = array('account''address''billing''review');
    }

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

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

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

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

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

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

    function 
processReview() {
        return 
true;
    }
}
?>


At this point in the tutorial, your wizard should have of four steps - each consisting of a view and process callback (plus any optional prepare callbacks). Also, the wizard should be automatically handling data persistence and navigation between the steps. The next question is how to retrieve the data stored by the component and what happens at the completion of the wizard.

Data Retrieval

Retrieving data from the component is possible at any point in the wizard. While our example will not manipulate or store the data permanently until the completion of the wizard, it's also reasonable that some applications may need to store data before the end of the wizard. For example, a job application may not be completed in one session but rather over a period of time. The progress, then, would need to be kept up with between sessions, rather than manipulated/stored all at once during the wizard completion.

Wizard data is stored with the following path: sessionKey.stepName.modelName.fieldName. The sessionKey will be explained in the Wizard Completion section below. The component method for retrieving data is read($key = null) which works pretty much like SessionComponent::read() except that the sessionKey is handled automatically by the WizardComponent and doesn't need to be passed into read(). Passing null into read() returns all Wizard data.

So, for example, if we wanted to do something with the client's email address (which was obtained in the account step) while processing the review step, we would use the following code:

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

        return true;
    }

An example showing how to retrieve all the current data with read() will be given below.

Wizard Completion

One of my goals when writing this component was to prevent double submission of user data. One of the ways I accomplished this was by using the process callbacks for each step and redirecting to rather than rendering the next step.

The second way was including an extra redirect and callback during the wizard completion process that creates a sort of "no man's land" for the wizard data. The way this works is, after the process callback for the last step is completed, the wizard data is moved to a new location in the session (Wizard.complete), the wizard redirects to a null step and another callback is called - afterComplete().

afterComplete() is an optional callback and is the ideal place to manipulate/store data after the wizard has been completed by the user. The callback does not need to return anything and the component automatically redirects to the $completeUrl (default '/') after the callback is finished.

It's important to note that immediately after the afterComplete() callback and before the user is redirected to $completeUrl, the wizard is reset completely (all data is flushed from the session). If you need to redirect manually from afterComplete(), be sure to call Wizard->resetWizard() manually.

So, to complete our tutorial example, we will pull all the data out of the wizard, store it in our database, and redirect the user to a confirmation page.

Controller Class:

<?php 
class SignupController extends AppController {
    var 
$uses = array('Client''User''Billing');
    var 
$components = array('Wizard');

    function 
beforeFilter() {
        
$this->Wizard->steps = array('account''address''billing''review');
        
$this->Wizard->completeUrl '/signup/confirm';
    }

    function 
confirm() {
    }

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

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

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

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

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

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

    function 
processReview() {
        return 
true;
    }
/**
 * [Wizard Completion Callback]
 */
    
function afterComplete() {
        
$wizardData $this->Wizard->read();
        
extract($wizardData);

        
$this->Client->save($account['Client'], false, array('first_name''last_name''phone'));
        
$this->User->save($account['User'], false, array('email''password'));
        
        ... 
etc ...
    }
}
?>

Please note the addition to beforeFilter() and the new confirm() method. You would also need to create a view file (confirm.ctp) with something like "Congrats, your sign-up was successful!" etc. It would also be good to create some sort of token during the afterComplete() callback and have it checked for in the confirm() method, but that's outside the scope of this tutorial.

A new addition to the WizardComponent 1.2 is plot-branching navigation (pbn). If you ever read a book as a child in which you interacted with the plot - i.e. If the knight slays the dragon, turn to page 64, if the knight runs for safety, turn to page 82. - then you've experienced pbn. In some applications, the steps in a wizard may not be a simple linear path, but might instead require the ability to "change course" based on user input.

For example, a survey that has varying questions for men or women might ask gender on the first page and would then need to navigate to different pages depending on the answer. While this is a simple example, some wizards can become very complicated when all the different options occur at different points in the wizard and "paths" begin to cross.

In some instances, it may not be a different path altogether, but merely a step being skipped over. Integrating Paypal Pro, for instance, requires the application allow the user to either enter their billing information on the site, or hop over to Paypal, login to their account and "skip" the billing page on the original site.

Advanced $steps Array

When using pbn, the $steps array becomes a bit more complex. Instead of adding/removing steps on the fly, all the steps are included into the array like they normally would. Then, "branches" are selected or skipped using the component methods. The trick to understanding the WizardComponent's pbn implementation is understanding the $steps array - the rest is pretty simple.

A simple $steps array is a single-tiered structure with each element corresponding to a step in the wizard. The array is ordered and the steps are handled sequentially.

An advanced $steps array setup for pbn is a multi-tiered structure consisting of simple $steps arrays separated by branch arrays (or branch groups). The branch arrays are associative arrays with branch names as indexes and simple $steps arrays as elements.

For example, lets say we had six steps: step1, step2, gender, step3, step4, and step5. The gender step would determine the user's gender and the subsequent steps would vary accordingly. If male, step3 and step4 would be used; if female, step4 and step5 would be used. So lets setup our $steps array:

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

It's important to understand that there is almost always more than one way to accomplish the same effect with different $steps arrays. For example, I could have instead, setup a 'male' branch that used step3, included step4 for both, and then another branch for 'female' that would include step5.

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

Also, although these examples are simple, I should point out that the $steps array is not limited to a three-tiered array. As long as the pattern is followed - array(stepName, array(branchName => array(stepName, etc...))) - the steps array can be as complex as resources allow for.


After the the $steps array is setup, the question becomes, "How does the component navigate through all the branches?" This is done be selecting which branch will be used in a "branch group". By default, the first branch in a group is always used (unless it has been "skipped" - more on that later). You can turn this feature off by setting Wizard->defaultBranch = false.

So, lets look at our two previous examples:

Example 1:
    $this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3', 'step4'), 'female' => array('step4', 'step5')));

Example 2:
    $this->Wizard->steps = array('step1', 'step2', 'gender', array('male' => array('step3')), 'step4', array('female' => array('step5')));

In example 1, 'male' and 'female' are two branches in the same branch group. Therefore, without any interference, the component would automatically use the 'male' branch and 'female' would be skipped. The steps would occur: step1, step2, gender, step3, step4. If $defaultBranch = false, both would be skipped and the steps would occur: step1, step2, gender.

In example 2, 'male' and 'female' are in separate branch groups. Therefore, without any interference, both branches would be used since they are the first branch in their respective groups. The steps would occur: step1, step2, gender, step3, step4, step5. If $defaultBranch = false, both would be skipped and the steps would occur: step1, step2, gender, step4.

branch() and unbranch()

In order to specify to the component which branches should be used, you must use the branch() and unbranch() methods. The branch() method includes a branch (specified by its name) in the session and unbranch() removes a branch from the session. branch() also has an extra parameter that allows branches to be easily skipped - more on that below.

So lets assume "female" was selected on the gender step. During the "processGender" callback, we could specify the "female" branch to be included:

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

        if($this->Client->validates()) {
            if($this->data['Client']['gender'] == 'female') {
                 $this->Wizard->branch('female');
            } else {
                 $this->Wizard->branch('male');
            }
            return true;
        }
        return false;
    } 

In example 1, the 'female' branch would be used instead of the 'male' branch and the steps would occur: step1, step2, gender, step4, step5. However, in example 2, unless $defaultBranch = false, the 'male' branch would also be used since it is not in the same branch group as 'female'.

Important: The first branch that has been included in the session will be used. In other words, if you were to do branch('male') and branch('female') for example 1, 'male' would be used since it occurs before 'female'. If 'male' was branched previously and you later wanted 'female' to be used, you would need to use unbranch('male').

In addition to including a branch to be used, branch() can also specify branches to be "skipped" by setting the second parameter to 'true'. If, for example, we used Wizard->branch('male', true) in the previous examples, 'male' would be skipped and 'female' would be used. The steps would occur: step1, step2, gender, step4, step5 - the same as using branch('female') with $defaultBranch = true!

The last thing I want to mention about pbn is that branch names do not necessarily have to be unique. In fact, I'd imagine some complex pbn wizards could be solved with some creative branch naming schemes in which identical branch names would be used only one branch() would have to be called to alter multiple branch groups. For example, using branch('male') with the following $steps array would select the 'male' branches in both the first and second branch groups.

$steps = array('step1', array('male' => ..., 'female' => ...), 'step2', array('cyborg' => ..., 'male' => ..., 'alien' => ...)); 
Also, (the other last thing I want to mention), the $steps array that each branch name points to can be treated exactly the same as the main $steps array - i.e. branch groups can be nested and branches are selected with branch() and $defaultBranch.

Page: 1 | 2 | 3 | 4 | 5

Comments

  • Posted 09/11/11 06:32:21 PM
    I have cake installed in a subdirectory, so there is a base path prepended.
    To get the right action in the form I used this:
    Form->create('User', array('url'=>Router::normalize($this->here)));?>
  • Posted 08/11/11 09:54:48 AM
    Is it impossible to use branch mechanism at first step?

    I can't figure out how to do it. I just need to skip first step in particular condotion.

    My variants:

    public function beforeFilter() {
    $this->Wizard->steps = array(array('childs' => array('child')), 'details', 'template', 'invite', 'message');
    $this->Wizard->defaultBranch = false;
    }


    public function wizard($step = null) {
    $this->Wizard->branch('childs', true);
    $this->Wizard->process($step);
    }

    Doesn't work. (error incorrect step. But if i returns to /wizard all goes correctly)


    public function beforeFilter() {
    $this->Wizard->steps = array(array('family' => array('details'), 'childs' => array('child', 'details')), 'template', 'invite', 'message');
    //$this->Wizard->defaultBranch = false;
    }

    doesnt work after adding

    public function wizard($step = null) {
    $this->Wizard->branch('childs');
    $this->Wizard->process($step);
    }

    but works without $this->Wizard->branch('childs');

    How can i got this work?
  • Posted 12/09/10 03:06:31 PM
    My security component is in the app_controller and my wizard only worked when I removed de beforeFilter and put the steps in the wizard action:

    function wizard($step = null) [
    $this->Wizard->steps = array('usuario', 'solicitacao', 'confirmacao');
    $this->Wizard->process($step);
    ]
    But over all it's a great component, very useful.

  • Posted 11/09/10 09:27:15 AM
    Reposted as my code got removed.

    If using the security component and having issues, ensure you are ending the form correctly with the following

    'echo $this->form->end()'

    Rather than the way it is in the examples.
  • Posted 05/11/10 12:19:23 PM
    When the security component is included all of the step post backs get black holed, even without any options being set in the security component.

    Anyone figured out why or how to fix?
    • Posted 11/09/10 09:16:43 AM
      [quote] When the security component is included all of the step post backs get black holed, even without any options being set in the security component.

      Anyone figured out why or how to fix?
      [end quote]
      I fixed this issue by editing the view, as in the example it has

      end();?>
      but it should be

      form->end();?>
      This line in the docs helped me figure it out

      'The Security Component looks for certain indicators that are created and managed by the FormHelper (especially those created in create() and end()).'

      Hope this helps.
  • Posted 01/20/10 04:53:14 PM
    Dan,

    I only noticed now that you replied right after I posted my bugreport.

    I've been fiddling with my own code a bit more, and it seems like it's all working now. It seems like it had something to do with the format of the array I had created for the branch. Still odd the array did work with autoadvance set to true and it failed when set to false though.

    In any case... Brilliant work! I'll send an e-mail to the author to let him know his code has been fixed, so he can update the code above with your adjustments (if you're ok with that of course).

    Hopefully all bugs the code contained have been fixed now!

    Cheers!
    Robin
    • Posted 01/21/10 02:24:53 AM
      Hey Robin,

      excellent that your branching is working now. :-)

      Integration of my adjustments in the core code is fine for me. It would be an honor. Also give credits to Penfold (comment #15) ;-)

      Have a nice day
      Dan
  • Posted 01/12/10 11:08:34 AM
    Great work on getting rid of those bugs Dan!

    I've just tested your fix and so far it all seems to work fine, as long as I keep the autoadvance setting set to true.

    As soon as I set it to false, the branching doesn't work anymore. The step where I put the selectbox to select a branch, doesn't redirect to the next page - but instead redirects to the previous page..

    Would you be able to look into it?? A fix might be right around the corner from here.

    Cheers

    Robin
    • Posted 01/12/10 01:47:00 PM
      Hey Robin,

      thanks for your report.

      I've just tested your fix and so far it all seems to work fine, as long as I keep the autoadvance setting set to true.

      As soon as I set it to false, the branching doesn't work anymore. The step where I put the selectbox to select a branch, doesn't redirect to the next page - but instead redirects to the previous page..

      To keep things a little bit shorter:
      You only have to check the while()-statement and its result in _setCurrentStep() which re-positions the internal array-pointer after renewing $this->steps by the additional _parseSteps()-method-call. Really no rocket science ;-)

      I guess the problem is caused by different PHP-versions and their array-handling. I don't have any other clue. It seems that the reset/next/current-array-calls don't work as expected.

      On my system which is running XAMPP 1.7.2 and PHP 5.3.0 on WinXP everythings works fine. Whether i set autoAdvance to true or not.

      How looks your $steps-configuration and where did you implement your branch()-statements?

      Cheers
      Dan
  • Posted 01/12/10 05:53:27 AM
    Hey folks,

    first of all thank you very much for that nifty cake-component, jaredhoyt. ;-)

    After playing around with the wizard the first time the last week i got a really smart order-assistant working.

    Yesterday I tried to integrate branching (just to be prepared in case of). I got stuck with the same problems some guys of you had and began to track down the problem.

    Finally i got it working and would like to share my solution with you. :-) It should be very reliable throughout my own testing. If you find a problem in certain situations please report.

    Fortunately it wasn't necessary to rewrite things. The component is fine but need some additions (ok, and one bugfix) at the right positions. No rewrite of the _parseSteps()-method like Penfold mentioned, no changes of your step-configuration!

    Thanks to Penfold (comment #15) for pointing me to the right direction.

    Here we go, add the following lines to the wizard-methods:

    1. In the startup()-method just after the if()-statement (like Penfold suggested):

    Component Class:

    <?php 
    /* Save original "$this->steps"-Array to reapply in case of (un)branching */
    $this->_steps $this->steps
    ?>


    2. In the branch()-/unbranch()-methods just before the closing brackets
    (the first statement comes from Penfold, but the second is also important to reach the next step wether you are using "autoAdvance = true" or not):

    Component Class:

    <?php 
    /* Re-trigger the steps-parser and reset the internal array pointer */
    $this->steps $this->_parseSteps($this->_steps);
    $this->_setCurrentStep($this->_currentStep);
    ?>


    3. In the ORIGINAL _parseSteps()-method (this is really a bugfix to get rid off that nasty warning-messages because the wizard doesn't init/reset the $branch-variable which will be used later in a wrong scope if you have more branching-arrays in your configuration).

    Note: this bug only appears if you are using $defaultBranch = false, but we would like to cover every situation, right?! ;-) By the way it is good coding practise.

    Respect the surrounding statements to find the right code-position:

    Component Class:

    <?php 
    [...]
    if (
    is_array($name))

        
    /* Init */
        
    $branch '';

        foreach (
    $name as $branchName => $step) {
    [...]
    ?>


    That's all. Happy branching. :-)

    CU
    Dan
  • Posted 01/09/10 09:33:13 PM
    Nice component, I've tried it out and i got two issues pending here:

    Senarior: I have 3 steps, each step includes a form with validation

    1. I puporsely leave a field empty in step one, of course i can see the validation error shown, then i filled in the value and navigate to step two. Then i went back to step one (history go -1, am i doing this wrong?) the error message is still shown even the input box is actually filled.

    2. Random fowarding issue
    To verify the wizard works, i navigate repeatly from step 1 to step 2 and step 3 back and forth, it was ok for the first few try, but after a while, the step 1 will directly navigate to step 3, the step 2 is lost.
    (Again, i'm using javascript history go -1 to go back to the previous step)

    Code Snippet

    Step One View

    &lt;form method="post" action="/mysite/wizard/step_one">

    Step Two View

    &lt;form method="post" action="/mysite/wizard/step_two"> ......
    &lt;input type="button" value="Back" onClick="nojavascript... history.go(-1)">

    Step Two View

    &lt;form method="post" action="/mysite/wizard/step_three"> ......
    &lt;input type="button" value="Back" onClick="nojavascript... history.go(-1)">

    Controller

    function beforeFilter() {
    $this->Wizard->steps = array('step_one', 'step_two', 'step_three');
    }

    function wizard($step = null) {
    $this->layout = 'wide_page';
    $this->Wizard->process($step);
    }

    function _processStepOne() {
    $this->MyData->set($this->data);
    if($this->MyData->validates()) {
    return true;
    }
    return false;
    }

    function _processStepTwo() {
    $this->MyData->set($this->data);
    return true;
    }

    function _processStepThree() {
    $this->MyData->set($this->data);
    $wizardData = $this->Wizard->read();
    extract($wizardData);
    //Save Data
    ......
    return true;
    }
  • Posted 01/07/10 09:30:01 AM
    Hi there :)

    I am intending to use your fine wizard component, to add a new Publication to my Database.

    As a publication can have multiple authors, I would like to be able, to repeat a step over and over, cumulating the data the user has entered so far.

    Is there an elegant way to do so?
    I thought about using branch('enterAuthor') in case the user clicks on an "I want to add more Authors" button, but wondered if that would work...

    Has anyone tried something like that before?
  • Posted 12/15/09 09:15:28 AM
    Has anyone actually tried this with dynamically generated steps, like a survey engine where the questions would come from a database and just be the same step over and over?
  • Posted 11/29/09 10:17:45 AM
    Thanks for taking the time to publish this excellent component and tutorial.

    Indeed, it's a time saver, another wheel which doesn't need to get re-invented.

    In order to work flawlessly with CakePHP 1.3 alpha all what was needed was to change


    $this->Session->del(....); 

    to


    $this->Session->delete(....); 

    in lines 320, 321, and 341 of wizard.php
  • Posted 11/20/09 05:15:49 PM
    Maybe I have a solution... but it doesn't seem as clean as it ought to be.

    What I did was have the controller 'edit' action load data into $this->data as usual, and then call the method defined as the 'wizardAction'.

    The 'wizardAction' method then checks for a null $step parameter and non-empty $this->data. In that case, a Session variable is written with the ID of my current record for the controller/model.

    Then I have _prepareXXXXXX() callbacks defined for each step, which all check for the Session variable, retrieve it if available, and use it to load the record intended for editing. This way it is available when each respective view is rendered.

    The final save of collected data is the same as a new entry, Except that after saving data, it is important to delete the Session variable storing the ID, so that it is not found next time a New record is entered using the form. Also, the Session variable should be deleted using the _beforeCancel() callback, for the same reason.

    This seems to work, but if anyone had a better way, I would be glad to know. Probably things could be cleaned up by incorporating some of this into the component itself.
  • Posted 11/20/09 03:11:51 PM
    I think I have things working with the process for collecting a new record with the component.

    But I don't see anything here about editing existing records with this component. Is there a convenient way to do that?

    Basically I would want to be able to have my 'index' or 'view' view list existing records, with links for editing either (1) directly to an action associated with the component, or (2) through the 'edit' action, which could then call the component's 'wizardAction'.

    Maybe this wasn't the purpose of this component, but any suggestions would be appreciated. Thank you.
  • Posted 10/21/09 03:01:38 PM
    Great work on this component.

    Still, I have been trying to get the PBN function to work, but unfortunately - even with Penfold's fix - still buggy.

    Penfold's fix works as long as $autoAdvance = true. When set to false, the step where the branch is set redirects to the step in front of the current step (step 3 goes back to step 2).

    I've been trying to fix the code myself, but unfortunately I can't get it to work.

    I've also tried the fix suggested by bo (comment #35), his fix works too, but yet again only when $autoAdvance = true. When set to false the branching is not working.

    If the PBN function gets fixed, this component would totally rock. Anyone any suggestions??
  • Posted 09/25/09 04:15:26 AM
    Hello,
    First thank you for you work.

    I'm having pain to make the review step working.
    So in the tutorial you say:
    It's very important to note that every step in the wizard must contain a form with a field. The only way for the wizard to continue to the next step is for the process callback to return true. And the process callback is only called if $this->data is not empty.
    If I have "step1", "step2", "step3", "review"
    3 first steps gets and validate data.
    Review as its name implies show the entered data to the customer to confirm.

    This step doens't needs field.
    _processReview() {return true;} is never called if I don't place a field in the view.

    How do you explain that ? What should I do ?

    Regards,

    Thibault
  • Posted 09/08/09 08:04:44 AM
    Hi, excellent work.

    Do you have a sample code of a working example of this?

    Certainly for myself a beginner, I would find it very helpful.

    Thanks
  • Posted 09/06/09 07:27:35 PM
    hi jaredhoyt,
    Would you have small sample code to explain your component.
    i am stuck with $this->here thing.
    Regards
  • Posted 09/06/09 07:16:38 PM
    hi jaredhoyt,
    Would you have any sample code, using this component.

    Kind Regards,
    Leo
  • Posted 09/06/09 07:16:31 PM
    hi jaredhoyt,
    Would you have any sample code, using this component.

    Kind Regards,
    Leo
  • Posted 08/27/09 05:55:35 AM
    Hi, firstly like many others this looks like a great piece of work.
    However I'm having a bit of a problem getting it to work.

    I have been through the tutorial and have attempted to set up my wizard to match. However whenever I click on the Continue button on the first page I always simply remain on the first page with all fields blanked out.

    As far as I can tell this is happening because the _validStep function in the wizard always return false.
    This is because on the following line
    the call to _getExpectedStep() always returns my first page and never the second page.

    array_search($this->_getExpectedStep(), $this->steps));

    If I change the _validStep to always return true I am able to get to the next page.

    This is very frustrating as I can see that this is a good piece of work and will help me greatly.

    I am developing this on a windows machine with IIS
    I should also point out I am very new to cake.

    Controller Class:

    <?php 
    <?php
    class ModulesController extends AppController {

        var 
    $name 'Modules';
        var 
    $helpers = array('Html''Form''ModuleMenu');
        var 
    $components = array('Wizard');

        function 
    index() {
            
    $this->Module->recursive 0;
            
    $this->set('modules'$this->paginate());
        }

        function 
    view($id null) {
            ...
        }

        function 
    edit($id null) {
            ...    
            }

        function 
    delete($id null) {
            ....
        }

        function 
    beforeFilter() {
            
    $this->Wizard->steps = array('details''material''review');
        }

        function 
    wizard($step null) {
            
    $this->Wizard->process($step);
        }

        
    /**
         * [Wizard Prepare Callbacks]
         */
        
    function _prepareDetails(){
            
    $moduleTypes =$this->Module->Moduletype->find('list');
            
    $this->set('moduleTypes',$moduleTypes);
        }

        function 
    _prepareMaterial(){
    //Never gets here
        
    }

        
    /**
         * [Wizard Process Callbacks]
         */
        
    function _processDetails() {
            
    $this->Module->set($this->data);

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

        function 
    _processMaterial() {
    //Never gets here
            
    $this->Module->set($this->data);

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

        
        function 
    _processReview() {
            return 
    true;
        }

        
    /**
         * [Wizard Completion Callback]
         */
        
    function _afterComplete() {
        
        }
    }
    ?>
    ?>

    View Template:


    <?php

    /**
     *
     *
     * @version $Id$
     * @copyright 2009
     */


    ?>
    <!-- make a menu for the modules using the ModulesMenu Helper -->
    <?php echo $moduleMenu->makeMenu() ?>

    <div id = "heading">
    Add Module
    </div>

    <div class="modules form">
        <?php
        
    echo $form->create('Module', array('url'=>$this->here));
        
    ?>
            <fieldset>
                 <legend><?php __('Step 1: Module details');?></legend>
            <?php
                
    echo $form->input('name');
                echo 
    $form->input('description');
                echo 
    $form->label('Type');
                echo 
    $form->select('moduletype_id'$moduleTypes$moduleTypes[1], nullFALSE);
            
    ?>
            </fieldset>
            <div>
                <?php
                    
    echo $form->submit('Continue', array('div'=>false));
                    echo 
    $form->submit('Cancel', array('name'=>'Cancel','div'=>false));
                
    ?>
            </div>
        <?php $form->end();?>
    </div>
  • Posted 07/27/09 10:49:27 AM
    Hi, First thanks for this great component. Its working great, but.. :)

    I want to call save() function on previous button action to save all data when going back - i think it is very user enemy if data are lost.

    Imagine you put some data on STEP2 but you have not continued yet. you
    go back to STEP1 and then to STEP2 and data from STEP2 were not saved
    - naturally - you did not clicked continue to STEP3 yet.

    So I did modify component's source by adding save()... it throws an error only when clicking previous button on last step.

    See more details and possible solution in my self-dialog :))
    http://groups.google.com/group/cake-php/browse_thread/thread/fa752c0384f46c4
    PS: I dont see whole consequences of my change... if you do please confirm it it is ok. for me its working now, but I dont use branches,..

    Thanks
    Tomas
  • Posted 07/25/09 06:26:31 AM
    Like everyone I agree this is a great component - thanks for taking the time to write it.

    I'm stuck on a problem, not necessarily with the component as I'm new to cake.

    Once the wizard is complete I want to redirect to another wizard, with the id of the record I've just created.

    I've tried:

    Controller Class:

    <?php 
    class SignupController extends AppController {
    ...
    var 
    $parentid null;

    function 
    beforeFilter() {
    ...
    $this->Wizard->completeUrl '/signup/wizard/details/id/'.$this->parentid;
    }


    function 
    _afterComplete() {
    ...
    $this->parentid 7;
    }
    ?>

    but the value of $this->parentid does not appear (I presume it's still null). _afterComplete is the only place I try and set the parentid.

    Where could I set the parentid once the data has been saved, and still access it in the rest of the controller? Any assistance appreciated!
    • Posted 08/10/09 12:28:13 AM
      You set your parentId to the controller variable, then _afterComplete callback is triggered and at the end it redirects to the other wizard, and your variable is lost here. So try to store it in session better, gl hf

      Like everyone I agree this is a great component - thanks for taking the time to write it.

      I'm stuck on a problem, not necessarily with the component as I'm new to cake.

      Once the wizard is complete I want to redirect to another wizard, with the id of the record I've just created.

      but the value of $this->parentid does not appear (I presume it's still null). _afterComplete is the only place I try and set the parentid.

      Where could I set the parentid once the data has been saved, and still access it in the rest of the controller? Any assistance appreciated!
  • Posted 07/20/09 09:26:42 PM
    For anyone else stuck on the this->here issue ("double paths", as it's been called here).

    Here are a couple of alternate ways of doing it:

    View Template:


    <?php
    // Should give the proper Cake-style URL beginning with '/'
    echo $form->create('Signup', array('id' => 'SignupForm''url'=> '/' $this->params['url']['url'])); 
    ?>

    View Template:


    <?php
    // Gives you the full URL with http:// (less ideal)
    echo $form->create('Signup', array('id' => 'SignupForm''url' => Router::url(nulltrue)));
    ?>
    • Posted 10/04/09 11:47:18 AM
      For anyone else stuck on the this->here issue ("double paths", as it's been called here).

      Here are a couple of alternate ways of doing it:

      View Template:


      <?php
      // Should give the proper Cake-style URL beginning with '/' (DS)
      echo $form->create('Signup',array('id'=>'SignupForm','url'=>DS.$this->params['url']['url'])); 
      ?>

      View Template:


      <?php
      // Gives you the full URL with http:// (less ideal)
      echo $form->create('Signup',array('id'=>'SignupForm','url'=>Router::url(nulltrue)));
      ?>

      According to this ticket...

      https://trac.cakephp.org/ticket/3296
      The proper usage is...

      'url' => $this->passedArgs
  • Posted 07/17/09 10:52:39 AM
    Very useful component. Reduced my work almost upto 60/70 percent heads up!

    Just a quick question, reply from anyone would be really appreciated!

    How can you go back to some specific step and then follow the path again. For example I have 5 steps (1,2,3,4,review). When I am on review, I want to go back to step 2 and follow the path 2->3->4.... Currently, it takes me to step 2 but then jumps right onto review as i submit step 2. I have tried modifying value of "expectedStep" in the session but it was not of much help.

    Thanks
    • Posted 07/25/09 05:12:17 AM
      How can you go back to some specific step and then follow the path again.
      Have you tried setting

      var $autoAdvance = false;
      in your controller?
  • Posted 06/10/09 08:38:55 AM
    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 06/10/09 09:34:59 AM
      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 06/10/09 10:06:04 AM
        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 06/04/09 11:13:24 AM
    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 05/11/09 01:05:43 AM
    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 06/17/09 09:23:19 AM
      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 05/09/09 05:21:30 PM
    Why isn't this component not part of the CakePHP standard distribution?!
  • Posted 05/09/09 05:11:31 PM
    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 05/09/09 03:15:06 AM
    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 05/08/09 01:24:36 PM
    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 06/06/09 10:50:42 AM
      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 06/10/09 02:13:46 PM
        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 05/05/09 11:58:38 AM
    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 05/03/09 09:19:37 AM
    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 05/03/09 01:04:28 PM
      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 04/29/09 12:44:50 PM
    In views folder under which folder we have to place these account.ctp address.ctp and billing.ctp ?

    Thanks,
    Durga
    • Posted 05/03/09 10:14:35 AM
      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 04/27/09 01:10:59 PM
    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 04/24/09 02:31:13 AM
    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 04/10/09 04:53:47 PM
    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 04/01/09 08:21:56 AM
    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 04/01/09 08:11:01 AM
    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 03/25/09 02:55:29 AM
    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 03/25/09 12:03:27 PM
      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 03/23/09 12:01:31 PM
    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 03/15/09 12:50:15 AM
    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 03/15/09 02:53:25 AM
      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 03/11/09 10:01:37 AM
    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 03/09/09 05:40:27 PM
    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 03/09/09 12:12:42 PM
    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 03/10/09 06:46:38 PM
      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 03/09/09 02:21:19 PM
      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 03/08/09 07:37:15 PM
    the wizard component seems to have vanished from the cake site.
  • Posted 03/08/09 01:50:49 PM
    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 03/08/09 08:57:23 PM
      Jared,

      Would you take a donation to fix up your component? There are a couple of broken things, notably the branching mechanism. I don't have time to fix it at the moment, and I'm trying to launch a project by myself. I would need it ASAP if you can schedule it in. I'm okay with all changes going back out to the public.

  • Posted 02/25/09 05:42:05 PM
    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 03/07/09 09:25:14 AM
      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 03/08/09 12:12:28 AM
        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 02/19/09 09:20:35 PM
    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 02/05/09 01:32:43 AM
    It seems that when I try to validate password field it tries to validate already hashed string. How to validate it?
  • Posted 02/04/09 07:56:55 AM
    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 02/04/09 01:04:08 AM
    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 02/04/09 03:50:19 AM
      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 02/04/09 07:47:30 AM
        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 01/30/09 07:51:16 AM
    @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 01/30/09 06:49:55 AM
    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 01/05/09 06:48:21 AM
    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 01/04/09 07:57:49 AM
    I registered only to comment...

    Wonderfull tutorials here @bakery

    Keep up the good work! I love it!!
  • Posted 12/22/08 04:07:32 PM
    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 12/18/08 04:10:09 PM
    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 12/07/08 04:35:41 AM
    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 12/07/08 10:10:18 PM
      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 12/06/08 12:31:30 PM
    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 12/05/08 01:51:19 PM
    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 12/04/08 02:05:42 PM
    How can I have multiple wizards within one controller?
    • Posted 12/05/08 01:32:20 PM
      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 12/01/08 01:53:23 PM
    Hello.

    I was browsing the bakery the other day for some form validation and came across your wizard component. I just have to say that I love it and my mind is buzzing with ideas for how to use it and once again save so much time on development. I'm already ready to dive in and use it, but I was wondering as to the status of the helper you hinted at... If you have a plan to post it any time soon I'll try to resist using it until then. I apologize if I'm annoying you, I'm just an anxious person, and I'm a big fan of your project. If I can aid in development or testing in any way, please let me know as well!

    Thanks for your time
    Marcus P.
    • Posted 12/01/08 11:13:46 PM
      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 11/24/08 10:04:37 PM
    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 11/25/08 01:38:02 AM
      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 11/24/08 04:06:33 PM
    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 11/20/08 11:58:59 AM
    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 11/22/08 12:54:33 PM
      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 11/19/08 08:47:15 AM
    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 11/19/08 01:31:43 PM
      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 11/19/08 02:46:13 PM
        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 11/19/08 04:16:40 PM
          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 11/18/08 09:49:33 AM
    A very well written tutorial!
    • Posted 11/18/08 02:20:50 PM
      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. :)

Comments are closed for articles over a year old