Tuesday, September 11, 2007

Creating multi-step forms and wizards in PHP

By Quentin Zervaas, 30 November 2005


There will be many situations when creating web forms, that either you cannot accept all data on one page, either because certain responses result in a different set of subsequent questions, or because you form is so long that you need to split it up into multiple pages. The case could even be that you have a 1 page form, but you want to show confirmation of the form data prior to processing the data (e.g. showing a user their order before processing their credit card).


This tutorial covers how to implement such forms using PHP. This will include covering the various issues that need to be taken into consideration, as well as a class to help build such forms. Finally, there will be real-world example of implementing a multi-page form using the class.


One thing you should be aware of though, is that there is a lot of work on properly developing multi-step forms. Even with a package like PEAR's HTML QuickForm Controller, it still takes time in setting everything up. We won't be looking at that package in this tutorial, as there are many aspects I personally don't like about the class (which is why I developed my own lightweight Wizard class).


Multi-page form considerations


There are several aspects of multi-page forms that need to be considered in the building process, including passing of form values between steps, preparing form data (e.g. dropdown box options on a particular step) and jumping between steps. Here I will cover each of these and suggest they can be dealt with.


Form values


Generally speaking, each step of the form will generate a set of values. These values will be used for different things. For example, at the completion of the form all values may be saved into your database. Or maybe a certain value determines which step is shown to the user next. Either way, we need to track the values somehow between each page.


There are basically two ways to achieve this. The first is to save every value as a hidden HTML input element. This way every submitted value is submitted on each subsequent form step and then reprocessed each time. For example:


Highlight: PHP


<input type="hidden" name="some_earlier_value" value=" <?= htmlSpecialChars ( $wizard -> values [ ' some_earlier_value ' ]) ?> " />

The easiest way to implement this would be to simply loop over all the previously submitted values and echo them in an input element such as above.


Personally though, I would store all previously submitted values in the PHP session. There are many advantages to doing this, including:



  • Don't have to worry about transporting all data between each step as this will be implicitly handled

  • Can implement a page refreshing prevention (see below)

  • Makes it easier to jump between arbitrary steps (see below)

  • Not as much processing required, as with session we can assume previously validated values are still valid, whereas when resubmitting all values, all values should be reprocessed regardless of which step they came from.


So as you can see, implementing your wizard so form values are stored in session is probably the way to go.


Knowing the current step


The next consideration is knowing the current step that is being displayed, or the step that is being processed. This is generally as simple as passing a parameter as to which step the submitted data is for.


However, there is an extra consideration for this to be made. That is, we must ensure that the step that is being processed (based on what the form says) is actually allowed to be processed. For example, if step 2 of the wizard requires a person's name to have been entered in step 1, then when we try to process step 2 we must ensure that step 1 has previously been successfully processed.


Normally there isn't much issue here, as if people use your form correctly they would only proceed to step 2 if step 1 was correct, however, if somebody was trying to forge your form and submit step 2 directly, we need to ensure they have first completed step 1.


Additionally, there may be forms where a certain step doesn't rely on another step. For example, a user may want to go directly to step 3 rather than filling out step 1. Or they may want to jump backwards from step 2 to change a value in step 1. Somehow we need to handle these step changes.


Different paths


Closely related to the previous issue of knowing the current step, is the is issue of having multiple paths in your form. In other words, the sequence of form steps to be displayed depends on the data the user enters.


For example, let's say the user is filling out a questionnaire, and there is a different set of questions for males and females. On the first step, there is question that asks the user's gender. Now, if the user chooses male, then the step which male questions should be shown, and likewise for females. Let's say the gender question step is step, the male questions are step 2 and the female questions are step 3. In this case, step 2 and step 3 rely on step 1 being completed, however, step 3 does not rely on step 2.


Put logically, if gender is male, jump to step 2, else (if gender is female), jump to step 3.


Confirmation page step


Often you will want the ability to show the user confirmation of what they have entered. This is simply a form step that has no inputs (other than to confirm the data is correct).


You could even consider a one page form that also has a confirmation form to be a multi-page form. This is commonly used in order forms to confirm the person's billing and shipping addresses, as well as the option to change them.


Final processing


Once the form is complete (or in the context of the previous paragraph, the user has confirmed the form is correct), you will likely need to do extra processing. For example, submitting the details into the database.


In fact, you could once again do something like the confirmation page step. It would be the same thing, but just to say the form has completed (or the order has been finalized, etc.).


For example:



  • Step 1: Enter billing and shipping details

  • Step 2: Confirm details are correct

  • [ process form, ship the item and send the user an email]

  • Step 3: Show confirmation the order has completed





ZervWizard class


I have developed a class that I've used in several projects, specifically for managing custom multi-step forms (or wizards, whatever you want to call them). This is a generic class that deals with the issues covered on the previous page. That is, storing of data, managing jumping between steps and various other useful aspects.


The full class is available from the Zervaas Enterprises, on the ZervWizard page . This code is free under the terms of the Apache License, which basically means you can use it as you please.


Here I will go over the general concepts for creating your own wizards. Then on the page we'll create a full example of a custom wizard, and then on the following page we'll create the full example including HTML forms and processing the form. The example we'll create will be for accepting a user's shipping and billing details to fulfill a product order.


General concepts


The ZervWizard class is fairly straightforward to use. As it is a generic handler, it does not deal with the actual creation of the HTML markup or JavaScript like HTML_Quickform tries to do. There's really only a few important things you need to know when developing custom wizards.


Step 1: Extending the class


When creating your custom class, you need to extend the ZervWizard class. You then call the constructor, passing in the container where form data is stored (usually $_SESSION), and also a unique name for your wizard (this is so if you have several wizards, the data will be stored in separate places within the container). Usually I just use the built in macro <i>CLASS</i> as this will generally give it a unique name.


Note that if you are using $_SESSION for your container (and I'm almost certain you will be), you must ensure you have already called session_start() somewhere in your code.


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class MyWizard extends ZervWizard

{

function MyWizard ()

{

session_start () ;

parent :: ZervWizard ( $_SESSION , __CLASS__ ) ;



// ... other code ...

}



// ... other code ...

}

?>

Step 2: Creating the steps


Next, we define the steps that make up the form. This is achieved using the addStep() method. This method takes two arguments. The first is an internal name for the step, and is used when creating the various callbacks (see below). The second is the step title, which is string describing the step. This is useful to use as the header on the corresponding form page.


The order of the steps in your form is defined by the order in which you call the addStep method.


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class MyWizard extends ZervWizard

{

function MyWizard ()

{

session_start () ;

parent :: ZervWizard ( $_SESSION , __CLASS__ ) ;



// create the form steps in the order in which they should be processed



$this -> addStep ( ' userdetails ' , ' Enter your shipping details ' ) ;

$this -> addStep ( ' billingdetails ' , ' Enter your billing details ' ) ;

$this -> addStep ( ' confirm ' , ' Confirm your details ' ) ;

}



// ... other code ...

}

?>

Step 3: “Prepare” methods


For every step that is created, there may be a “prepare” callback. The purpose of this callback is to fetch all necessary data for rendering that step of the form.


Suppose for example that in a step for accepting a user's shipping details, they had to select their country. In the prepare callback for that, you would probably generate the list of countries from a database and return them so they can be used within a HTML select field.


The prepare callback is optional, because it's rather pointless having a prepare step if there's no data that needs preparing.


Prepare callbacks have the name prepare [stepname]()_. So for the ‘userdetails' step created above, the prepare callback would be called prepare_userdetails() . The callback accepts no arguments and need not return any data. Any data prepared that you will need to use you should just assign a property of your class.


Here's an example of creating the list of countries:


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class MyWizard extends ZervWizard

{

function MyWizard ()

{

// ... other code ...



$this -> addStep ( ' userdetails ' , ' Enter your shipping details ' ) ;



// ... other code ...

}



function prepare_userdetails ()

{

$this -> countries = array ( ' au ' => ' Australia ' ,

' de ' => ' Germany '

' fr ' => ' France ' ,

' uk ' => ' United Kingdom ' ,

' us ' => ' United States ' ) ;

}

}

?>

So you know that when you're outputting the form for the userdetails step, that the countries data will be available from the wizard class. You'll see this in further use in the full example.


Step 4: “Process” methods


Every step created must have a process method. While the prepare method was optional for each step, the form can't progress unless there's a process method that returns true for the corresponding step.


The process method should only return true if the form data was valid. For example, if the user must enter a valid email address but does not, then this should set an error (using the the addError() method), and the process method should return false. The easiest way to do this is to use the isError() method to generate the return value.


If a the process method returns false, then the user will be shown the same step once again. Assuming you have created code to display the generated errors, then the user will see these errors.


The process method has similar naming technique to the prepare method. That is, process [stepname]()_. So going back to the ‘userdetails' step, the process method would be called process_userdetails() .


The process method accepts the raw form data as an argument.


And one more important step—once you've validate a value, you must manually set it to the container using setValue(). This method takes two arguments: the value name and the value. You'll see it in the example just below.


Here's an example of some simple processing of the user details step:


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class MyWizard extends ZervWizard

{

function MyWizard ()

{

// ... other code ...



$this -> addStep ( ' userdetails ' , ' Enter your shipping details ' ) ;



// ... other code ...

}



function process_userdetails ( & $form )

{

// initialize the form data we're processing

$country = $this -> coalesce ( $form [ ' country ' ] , '' ) ;

$email = $this -> coalesce ( $form [ ' email ' ] , '' ) ;



// call the prepare method so we can then access the countries list.

// it is only automatically called when displaying that step, so we

// manually need to call it here.

$this -> prepare_userdetails () ;



if ( array_key_exists ( $country , $this -> countries ))

$this -> setValue ( ' country ' , $country ) ;

else

$this -> addError ( ' country ' , ' Please select your country ' ) ;



if ( isValidEmail ( $email ))

$this -> setValue ( ' email ' , $email ) ;

else

$this -> addError ( ' email ' , ' Please enter a valid email address ' ) ;



return ! $this -> isError () ;

}

}

?>

For the sake of the above example we just assume there's a method called isValidEmail which returns true if passed an email address in a valid form.


We also used the ZervWizard coalesce method, which is useful for initializing form values. Basically, you pass it the form value as the first argument and a default value as the second argument, so if the form value doesn't exist, the default value will be return and no PHP warning is generated (i.e. the undefined index warning).


Step 5: Form complete callback


Once the final step has been completed, it is possible to then run some extra code. This is basically the code that does something with all the data submitted. For example, you may write the data to a database, send the user an email, or process a credit card transaction.


Here we also introduce the getValue() method. This method takes the value name as an argument and returns the corresponding value. Additionally, you can pass a second argument which is the value to return if the specified value doesn't exist in the container.


You may end up using the getValue() method in prepare or process callbacks too, as values selected earlier may have some bearing on how you process future steps or prepare data for future steps.


Anyway, back to the form completion callback. This is just a method called completeCallback(). Just define this in your class with whatever code you need executed and it will automatically be run when the final step is complete.


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class MyWizard extends ZervWizard

{

function MyWizard ()

{

// ... other code ...



$this -> addStep ( ' userdetails ' , ' Enter your shipping details ' ) ;



// ... other code ...

}



function completeCallback ()

{

$creditCard = $this -> getValue ( ' credit_card ' ) ;

$creditExpiry = $this -> getValue ( ' credit_card ' ) ;



processCreditCard ( $creditCard , $creditExpiry ) ;

sendUserConfirmationEmail ( $this -> getValue ( ' email ' )) ;

}

}

?>

Obviously the methods here are fictional.


Once the final step is complete, the wizard is complete. As such, comleteCallback() is only ever called once: when the final step's process method returns true.


Listing of methods



  • addStep([step name], [step title]) – Adds a step to the wizard

  • coalesce([value], [default value]) – Initialize a value or set it to the default value

  • setValue([key], [value]) – Save a value in the wizard container

  • getValue([key], [default]) – Get a value from the wizard container, or return the default value (optional)

  • addError([key], [error message]) – Add an error to the container

  • isError() – Check if an error has occurred

  • clearContainer() – Empty out all values in the container, including saved values and errors

  • getStepProperty() – Get information about the current step. Currently the only step is ‘title', but this may be extended in the future to store other properties.




ZervWizard example


Here is a full example of a class that extends the ZervWizard class. In the next step, we will write the corresponding code that uses the code.


There will be various comments and notes scattered throughout the code to explain it in a top-down approach, so read carefully!


The example is of processing a shopping cart checkout section. It includes 3 steps: 1 to get the user's shipping details, 1 to get the billing details, and the final step is to confirm the user's details. Once they have confirmed the details, we will process the data.


To simplify this example, we are process the credit card as the very final thing we do. This means if it fails there's no mechanism to go back and ask for a new number. The example would be significantly longer to handle all this functionality correctly.


Also please note that the ZervWizard is quite functional and works well, but there may be some limitations with it that will hopefully be overcome in the future. It is certainly a good starting point for what we're trying to achieve.


The CheckoutWizard class


Highlight: PHP


<?php

require_once ( ' ZervWizard.class.php ' ) ;



class CheckoutWizard extends ZervWizard

{

function CheckoutWizard ()

{

// start the session and initialize the wizard

session_start () ;

parent :: ZervWizard ( $_SESSION , __CLASS__ ) ;





// create the steps, we're only making a simple 3 step form

$this -> addStep ( ' userdetails ' , ' Enter your shipping details ' ) ;

$this -> addStep ( ' billingdetails ' , ' Enter your billing details ' ) ;

$this -> addStep ( ' confirm ' , ' Confirm your details ' ) ;

}



// here we prepare the user details step. all we really need to

// do for this step is generate the list of countries.



function prepare_userdetails ()

{

$this -> loadCountries () ;

}



// now we process the first step. we've simplified things, so we're

// only collecting, name, email address and country



function process_userdetails ( & $form )

{

$name = $this -> coalesce ( $form [ ' name ' ]) ;

if ( strlen ( $name ) > 0 )

$this -> setValue ( ' name ' , $name ) ;

else

$this -> addError ( ' name ' , ' Please enter your name name ' ) ;



$email = $this -> coalesce ( $form [ ' email ' ]) ;

if ( $this -> isValidEmail ( $email ))

$this -> setValue ( ' email ' , $email ) ;

else

$this -> addError ( ' email ' , ' Please enter a valid email address ' ) ;



$country = $this -> coalesce ( $form [ ' country ' ]) ;

$this -> loadCountries () ;

if ( array_key_exists ( $country , $this -> countries ))

$this -> setValue ( ' country ' , $country ) ;

else

$this -> addError ( ' country ' , ' Please select your country ' ) ;



return ! $this -> isError () ;

}



// next, prepare the billing details step. again, not much to do here. here

// we'll generate a list of the different types of credit cards



function prepare_billingdetails ()

{

$this -> ccTypes = array ( ' VISA ' , ' MASTERCARD ' , ' AMEX ' ) ;

}



// the next thing we do is process the billing details step. this involves

// validating the credit card details. we're going to use the name accepted

// in the first step as the credit card name just to simplify things.

// additionally, we're not going to bother with the expiry date, once again

// just to simplify things.



function process_billingdetails ( & $form )

{

// load the cc types so we can validate the selected value

$this -> prepare_billingdetails () ;

$cc_type = $this -> coalesce ( $form [ ' cc_type ' ]) ;



if ( in_array ( $cc_type , $this -> ccTypes ))

$this -> setValue ( ' cc_type ' , $cc_type ) ;

else

$this -> addError ( ' cc_type ' , ' Please select a valid credit card type ' ) ;



$cc_number = $this -> coalesce ( $form [ ' cc_number ' ]) ;



if ( strlen ( $cc_number ) > 0 && $this -> validLuhn ( $cc_number ))

$this -> setValue ( ' cc_number ' , $cc_number ) ;

else

$this -> addError ( ' cc_number ' , ' The specified credit card number is invalid ' ) ;



return ! $this -> isError () ;

}



// the final step is the confirmation step. there's nothing to prepare here

// as we're just asking for final acceptance from the user



function process_confirm ( & $form )

{

$confirm = ( bool ) $this -> coalesce ( $form [ ' confirm ' ] , true ) ;



return $confirm ;

}





function completeCallback ()

{

// finally, all form data is valid and the user has confirmed they

// want to proceed. Now we do all the final processing here. Because

// we don't really have a credit card gateway for this example we

// just simulate it



$this -> processCreditCard ( $this -> getValue ( ' name ' ) ,

$this -> getValue ( ' cc_type ' ) ,

$this -> getValue ( ' cc_number ' )) ;





$this -> sendUserConfirmationEmail ( $this -> getValue ( ' email ' )) ;

}





function processCreditCard ( $name , $type , $number )

{

// communicate with CC gateway here

}



function sendUserConfirmationEmail ( $email )

{

// create and a send an email

}







/**

* Miscellaneous utility functions

*/





function isValidEmail ( $email )

{

return preg_match ( ' /^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*$/i ' , $email ) ;

}



function loadCountries ()

{

$this -> countries = array ( ' au ' => ' Australia ' ,

' de ' => ' Germany ' ,

' fr ' => ' France ' ,

' uk ' => ' United Kingdom ' ,

' us ' => ' United States ' ) ;

}



// a method to validate a credit card number. we're not actually validating

// it against the type of credit card, just to simplify the example. to do

// this, it's just a matter of checking the number prefix and the length

// of the number (depending on the card)

function validLuhn ( $number )

{

$len = strlen ( $number ) ;

$dbl = '' ;

for ( $i = $len - 2 ; $i >= 0 ; $i -= 2 ) {

$dbl .= (( int ) $number { $i }) * 2 ;

}



$dbllen = strlen ( $dbl ) ;

$dbltotal = 0 ;

for ( $i = 0 ; $i < $dbllen ; $i ++ ) {

$dbltotal += ( int ) $dbl { $i } ;

}



$total = $dbltotal ;

for ( $i = $len - 1 ; $i >= 0 ; $i -= 2 ) {

$total += $number { $i } ;

}

return $total % 10 == 0 ;



A full example using our CheckoutWizard class


Now that we've created our wizard class, we must put it to use. Here is a complete script that uses the wizard. This includes instantiating the class, generating all the forms, processing the data and outputting error messages.


Additionally, there is also output once the wizard is complete that gives the option to reset the form and start again.


Highlight: PHP


<?php

require_once ( ' CheckoutWizard.class.php ' ) ;



$wizard = new CheckoutWizard () ;

$action = $wizard -> coalesce ( $_GET [ ' action ' ]) ;



$wizard -> process ( $action , $_POST , $_SERVER [ ' REQUEST_METHOD ' ] == ' POST ' ) ;

// only processes the form if it was posted. this way, we

// can allow people to refresh the page without resubmitting

// form data



?>

<html>

<head>

<title>phpRiot() wizard example</title>

</head>

<body>

<h1>phpRiot() wizard example</h1>



<?php if ( $wizard -> isComplete ()) { ?>



<p>

The form is now complete. Clicking the button below will clear the container and start again.

</p>



<form method="post" action=" <?= $_SERVER [ ' PHP_SELF ' ] ?> ?action= <?= $wizard -> resetAction ?> ">

<input type="submit" value="Start again" />

</form>



<?php } else { ?>



<form method="post" action=" <?= $_SERVER [ ' PHP_SELF ' ] ?> ?action= <?= $wizard -> getStepName () ?> ">

<h2> <?= $wizard -> getStepProperty ( ' title ' ) ?> </h2>



<?php if ( $wizard -> getStepName () == ' userdetails ' ) { ?>

<table>

<tr>

<td>Name:</td>

<td>

<input type="text" name="name" value=" <?= htmlSpecialChars ( $wizard -> getValue ( ' name ' )) ?> " />

</td>

<td>

<?php if ( $wizard -> isError ( ' name ' )) { ?>

<?= $wizard -> getError ( ' name ' ) ?>

<?php } ?>

</td>

</tr>

<tr>

<td>Email:</td>

<td>

<input type="text" name="email" value=" <?= htmlSpecialChars ( $wizard -> getValue ( ' email ' )) ?> " />

</td>

<td>

<?php if ( $wizard -> isError ( ' email ' )) { ?>

<?= $wizard -> getError ( ' email ' ) ?>

<?php } ?>

</td>

</tr>

<tr>

<td>Country:</td>

<td>

<select name="country">

<option value=""></option>

<?php foreach ( $wizard -> countries as $k => $v ) { ?>

<option value=" <?= $k ?> " <?php if ( $wizard -> getValue ( ' country ' ) == $k ) { ?> selected="selected" <?php } ?> >

<?= $v ?>

</option>

<?php } ?>

</select>

</td>

<td>

<?php if ( $wizard -> isError ( ' country ' )) { ?>

<?= $wizard -> getError ( ' country ' ) ?>

<?php } ?>

</td>

</tr>

</table>

<?php } else if ( $wizard -> getStepName () == ' billingdetails ' ) { ?>

<table>

<tr>

<td>Credit Card Type:</td>

<td>

<select name="cc_type">

<option value=""></option>

<?php foreach ( $wizard -> ccTypes as $v ) { ?>

<option value=" <?= $v ?> " <?php if ( $wizard -> getValue ( ' cc_type ' ) == $v ) { ?> selected="selected" <?php } ?> >

<?= $v ?>

</option>

<?php } ?>

</select>

</td>

<td>

<?php if ( $wizard -> isError ( ' cc_type ' )) { ?>

<?= $wizard -> getError ( ' cc_type ' ) ?>

<?php } ?>

</td>

</tr>

<tr>

<td>Credit Card Number:</td>

<td>

<input type="text" name="cc_number" value=" <?= htmlSpecialChars ( $wizard -> getValue ( ' cc_number ' )) ?> " />

</td>

<td>

<?php if ( $wizard -> isError ( ' cc_number ' )) { ?>

<?= $wizard -> getError ( ' cc_number ' ) ?>

<?php } ?>

</td>

</tr>

</table>

<?php } else if ( $wizard -> getStepName () == ' confirm ' ) { ?>

<p>

Please verify the entered details and then click next to complete your order.

</p>



<table>

<tr>

<td>Name:</td>

<td> <?= $wizard -> getValue ( ' name ' ) ?> </td>

</tr>

<tr>

<td>Email:</td>

<td> <?= $wizard -> getValue ( ' email ' ) ?> </td>

</tr>

<tr>

<td>Credit Card Type:</td>

<td> <?= $wizard -> getValue ( ' cc_type ' ) ?> </td>

</tr>

<tr>

<td>Credit Card Number:</td>

<td> <?= $wizard -> getValue ( ' cc_number ' ) ?> </td>

</tr>

</table>

<?php } ?>



<p>

<input type="submit" name="previous" value="&lt;&lt; Previous" <?php if ( $wizard -> isFirstStep ()) { ?> disabled="disabled" <?php } ?> />

<input type="submit" value=" <?= $wizard -> isLastStep () ? ' Finish ' : ' Next ' ?> &gt;&gt;" />

</p>

</form>

<?php } ?>

</body>





</html>

}

}

?>

No comments:

About Me

Ordinary People that spend much time in the box
Powered By Blogger