Building a Multistep Registration Form in Drupal 7 using Ctools

This article provides a step-by-step tutorial for creating a custom, multistep registration form via the Ctools Form Wizard in Drupal 7. If you'd prefer to solely use the core Form API, take a look at Building a Multistep Registration Form in Drupal 7, a previous blog post. In the interest of saving time, I'm going to be lifting some text directly from that post, given that there are a number of overlapping tasks.

Why use the Chaos Tools module to build a multistep form? Well, Ctools offers a number of tools that build upon the core Form API, allowing you to create a multistep form faster. This includes providing a method for caching data in between steps, adding 'next' and 'back' buttons with associated callbacks, generating a form breadcrumb, etc.

The Tut

First things first— create a new, empty, custom module. In this example, the module will be named grasmash_registration. In the interest of reducing our bootstrapping footprint and keeping things organized, we're also going to create an include file. This will store the various construction and helper functions for our form. Let's name it grasmash_registration_ctools_wizard.inc.

We'll start by defining a "master" ctools form wizard callback. This will define all of the important aspects of our multistep form, such as the child form callbacks, titles, display settings, etc. Please take a look at the help document packaged with ctools in ctools/help/wizard.html for a full list of the available parameters.

<?php
/**
* Create callback for standard ctools registration wizard.
*/
function grasmash_registration_ctools_wizard($step = 'register') {
 
// Include required ctools files.
 
ctools_include('wizard');
 
ctools_include('object-cache');
   
 
$form_info = array(
   
// Specify unique form id for this form.
   
'id' => 'multistep_registration',
   
//Specify the path for this form. It is important to include space for the $step argument to be passed.
   
'path' => "user/register/%step",
   
// Show breadcrumb trail.
   
'show trail' => TRUE,
   
'show back' => FALSE,
   
'show return' => FALSE,
   
// Callback to use when the 'next' button is clicked.
   
'next callback' => 'grasmash_registration_subtask_next',
   
// Callback to use when entire form is completed.
   
'finish callback' => 'grasmash_registration_subtask_finish',
   
// Callback to use when user clicks final submit button.
   
'return callback' => 'grasmash_registration_subtask_finish',
   
// Callback to use when user cancels wizard.
   
'cancel callback' => 'grasmash_registration_subtask_cancel',
   
// Specify the order that the child forms will appear in, as well as their page titles.
   
'order' => array(
     
'register' => t('Register'),
     
'groups' => t('Connect'),
     
'invite' => t('Invite'),
    ),
   
// Define the child forms. Be sure to use the same keys here that were user in the 'order' section of this array.
   
'forms' => array(
     
'register' => array(
       
'form id' => 'user_register_form'
     
),
     
'groups' => array(
       
'form id' => 'grasmash_registration_group_info_form',
       
// Be sure to load the required include file if the form callback is not defined in the .module file.
       
'include' => drupal_get_path('module', 'grasmash_registration') . '/grasmash_registration_groups_form.inc',
      ),
     
'invite' => array(
       
'form id' => 'grasmash_registration_invite_form',
      ),
    ),
  );

 

// Make cached data available within each step's $form_state array.
 
$form_state['signup_object'] = grasmash_registration_get_page_cache('signup');

 

// Return the form as a Ctools multi-step form.
 
$output = ctools_wizard_multistep_form($form_info, $step, $form_state);
 
  return
$output;
}
?>

As you can see, our registration form will have threes steps:

  1. the default user registration form
  2. the groups form
  3. the invite form

These have been respectively titled "Register," "Connect," and "Invite."

You should also see that we have referenced a number of, as of yet, non-existent callback functions, as well as a cache retreival function. Let's talk about that cache function first, then look at the callbacks.

Data caching

Ctools provides a specialized Object Cache feature that allows us to store arbitrary, non-volatile data objects. We will use this feature to store user-submitted form values in between the form's multiple steps. Once the entire form has been completed, we will use that data for processing.

To efficiently utilize the Object cache, we will define a few wrapper functions. These wrapper function will be used to create, retreive, and destroy cache objects.

<?php
/**
* Retreives an object from the cache.
*
* @param string $name
*  The name of the cached object to retreive.
*/
function grasmash_registration_get_page_cache($name) {
 
ctools_include('object-cache');
 
$cache = ctools_object_cache_get('grasmash_registration', $name);

 

// If the cached object doesn't exist yet, create an empty object.
 
if (!$cache) {
   
$cache = new stdClass();
   
$cache->locked = ctools_object_cache_test('grasmash_registration', $name);
  }

  return

$cache;
}

/**
* Creates or updates an object in the cache.
*
* @param string $name
*  The name of the object to cache.
*
* @param object $data
*  The object to be cached.
*/
function grasmash_registration_set_page_cache($name, $data) {
 
ctools_include('object-cache');
 
$cache = ctools_object_cache_set('grasmash_registration', $name, $data);
}

/**
* Removes an item from the object cache.
*
* @param string $name
*  The name of the object to destroy.
*/
function grasmash_registration_clear_page_cache($name) {
 
ctools_include('object-cache');
 
ctools_object_cache_clear('grasmash_registration', $name);
}
?>

Submit callbacks

Now, we will define the various callbacks that were referenced in our $form_info array. These callbacks are executed when a user clicks the 'next', 'cancel', or 'finish' buttons in the multi-step form.

<?php
/**
* Callback executed when the 'next' button is clicked.
*/
function grasmash_registration_subtask_next(&$form_state) {
 
// Store submitted data in a ctools cache object, namespaced 'signup'.
 
grasmash_registration_set_page_cache('signup', $form_state['values']);
}

/**
* Callback executed when the 'cancel' button is clicked.
*/
function grasmash_registration_subtask_cancel(&$form_state) {
 
// Clear our ctools cache object. It's good housekeeping.
 
grasmash_registration_clear_page_cache('signup');
}

/**
* Callback executed when the entire form submission is finished.
*/
function grasmash_registration_subtask_finish(&$form_state) {
 
// Clear our Ctool cache object.
 
grasmash_registration_clear_page_cache('signup');

 

// Redirect the user to the front page.
 
drupal_goto('<front>');
}
?>

Child Form callbacks

These forms comprise the individual steps in the multistep form.

<?php
function grasmash_registration_group_info_form($form, &$form_state) {
 
$form['item'] = array(
   
'#markup' => t('This is step 2'),
  );

  return

$form;
}
function
grasmash_registration_invite_form($form, &$form_state) {
 
$form['item'] = array(
   
'#markup' => t('This is step 3'),
  );

  return

$form;
}
?>

You can use all of the magic of the Form API with your child forms, including separate submit and validation handlers for each step.

Integration with Drupal core user registration form

Now for the tricky part— we're going to override the Drupal core user registration form with our multistep ctools form, making user registration the first step.

We will do this by modifying the menu router item that controls the 'user/register' path via hook_menu_alter(). By default, the 'user/register' path calls drupal_get_form() to create the registration form. We're going to change that so that it calls our ctools multistep form callback instead.

Note, all hook implementations should be place in your .module file.

<?php
/**
* Implements hook_menu_alter().
*/
function grasmash_registration_menu_alter(&$items) {
 
// Ctools registration wizard for standard registration.
  // Overrides default router item defined by core user module.
 
$items['user/register']['page callback'] = array('grasmash_registration_ctools_wizard');
 
// Pass the "first" step key to start the form on step 1 if no step has been specified.
 
$items['user/register']['page arguments'] = array('register');
 
$items['user/register']['file path'] = drupal_get_path('module', 'grasmash_registration');
 
$items['user/register']['file'] = 'grasmash_registration_ctools_wizard.inc';

  return

$items;
}
?>

We will also need to define a new menu router item to handle the subsequent steps of our multistep form. E.g., user/register/%step:

<?php
/**
* Implements hook_menu().
*/
function grasmash_registration_menu() {
 
$items['user/register/%'] = array(
   
'title' => 'Create new account',
   
'page callback' => 'grasmash_registration_ctools_wizard',
   
'page arguments' => array(2),
   
'access callback' => 'grasmash_registration_access',
   
'access arguments' => array(2),
   
'file' => 'grasmash_registration_ctools_wizard.inc',
   
'type' => MENU_CALLBACK,
  );

  return

$items;
}
?>

Lastly, we need to make a slight alteration to the user_register_form. It will now have at least two submit handlers bound to it: user_register_submit, and ctools_wizard_submit. We need to make sure that the user_register_submit callback is called first!

<?php
/**
* Implements hook_form_FORM_ID_alter().
*/
function hook_form_user_register_form_alter(&$form, &$form_state) {
 
$form['#submit'] = array(
   
'user_register_submit',
   
'ctools_wizard_submit',
  );
}
?>

That's it! You should now be able to navigate to user/register and see the first step of your multistep form. Subsequent steps will take you to user/register/[step-name].

Now, for a bonus snippet snack, here's how you can take the first step of your form and put it into a block!

Displaying the first step of our form in a block

<?php
/**
* Implements hook_block_info().
*/
function grasmash_registration_block_info() {
 
$blocks['register_step1'] = array(
   
'info' => t('Grasmash Registration: Step 1'),
   
'cache' => DRUPAL_NO_CACHE,
  );

  return

$blocks;
}
/**
* Implements hook_block_view().
*
* This hook generates the contents of the blocks themselves.
*/
function grasmash_registration_block_view($delta = '') {
  switch (
$delta) {
    case
'register_step1':
     
$block['subject'] = 'Create an Account';
     
$block['content'] = grasmash_registration_block_contents($delta);
      break;
  }
  return
$block;
}
/**
* A module-defined block content function.
*/
function grasmash_registration_block_contents($which_block) {
  global
$user;
 
$content = '';
  switch (
$which_block) {
    case
'register_step1':
      if (!
$user->uid) {
       
module_load_include('inc', 'grasmash_registration', 'grasmash_registration_ctools_wizard');
        return
grasmash_registration_ctools_wizard('register');
      }
      break;
  }
}
?>

Good luck!

Drupal Version Compatibility: 

Comments

Hey Matt,
Great tutorial! I was able to successfully implement ctools form wizard with the core user registration form. A couple things that came up for me that I thought might help others who follow the steps. First, you'll want to make sure to run a hook_form_alter on the user registration form and re-order the submit callbacks so that the ctools_wizard_submit is called last. See this issue over at drupal.org http://drupal.org/node/1732904#comment-6355618

Also, I also after submitting the user_register_form I was getting an "access denied" message which I think might be related to the issue http://drupal.org/node/118498 which has been a long standing thread on drupal.org. One work around that I found for this was to define my 'access callback' for the menu router item like so:
$item['user/register/%'] = array( 'access callback' => 'registration_access');
...
function registration_callback(){
return variable_get('user_register',1);
}
**Make sure that you remove the 'access arguments' definition in the item as well, because this registration_callback does not accept params.

One last thing that might not be so obvious in the post is that you'll want to have everything in your .inc file except the hook_menu_alter and hook_menu definitions (as well as any hook_form_alter functions).

Thanks again Matt for posting! Keep up the good work.

Great points Troy!

Awesome tutorial Matt! Also thanks for the extra comments Troy, helped me avoid a head-scratcher

For those like me who are newbies to Drupal (or also like me and are braindead), if you can't get around the "Access denied" it is probably because [user/register/%] is for non-authenticated users. The link is for registering onto your site. Log out and try [<url>/user/register/%]

@Matt: Wanted to let you know this tutorial is helping people beyond developers. I'm lucky enough to get paid time-off from work to code for a charity fighting Duchenne muscular dystrophy (http://jettfoundation.org/), but that time is limited to 2-weeks so things like this really help us help the charities faster. We've done some stuff in the past and hope to do more, and want to get all companies who can doing similar stuff, thanks again!

http://cfac.nextjump.com/ (<-- shameless PR'ing but we are trying to spread the word)

And one more important step. In the .info definition for your custom module you'll want to make sure that you set:
dependencies[] = ctools
^^obvious for most, but could be overlooked by some.

I will just leave this here http://www.drupal4hu.com/node/246

Except that way feels unnatural and doesn't grant advantages like wizard steps or step URLs for custom forms, plus it looks like it's creating nodes.

The CTools way like this article is honestly the best and most bulletproof way I have seen it done even in Drupal 6.

The reasoning? Every step that is hit, you can aggregate the data and save it. Like a multistep registration form, you can store form submissions into the profile of that user and if they quit, they can always return later and be right back on that step. I don't feel you get that with the reverse engineering of form workflows like in chx article above.

This is great, but how would you make the multi-step form use AJAX?

And is there any way to programmatically build the AJAX-enabled form and put it with other things in a render array?

You can certainly make this form into an AJAX form. There are many articles online that show you how to do this. Here are a few:

function ajax_example_wizard
Multi Steps Form in Drupal 7 with Ctools
AJAX Forms in Drupal 7

If you plan on having users upload files in a multistep form, you might get bit by this bug: http://drupal.org/node/1205822. There's a workaround discussed in the comments but it requires you to hack core and comes with an annoying side effect.

Correct me if I'm wrong, but for the object caching mechanism that we are using, we are overriding the cached object in the _registration_subtask_next() callback? This still caches the object between forms, but for overall processing of the data in the _registration_subtask_finish callback the cached object is the $form_state['values'] for the second to last form (not the last form because our _registration_subtask_next() callback is not called on the last submit, only the _registration_subtask_finish callback is called). I know we could create submit handlers for each form and store the $form_state['values'] in another array in our $form_state parameter, but that seems like it may be getting a bit redundant from a development perspective. The document mentioned at the beginning of the post ( http://drupalcode.org/project/ctools.git/blob_plain/refs/heads/7.x-1.x:/help/wizard.html ) suggests: "The primary difference between these forms and a normal Drupal form is that the submit handler should not save any data. Instead, it should make any changes to a cached object (usually placed on the $form_state) and only the _finish or _return handler should actually save any real data." Does anyone have any suggestions on how to cache ALL of the form data so that we can process it in our _finish callback?

Thanks!

Troy,

You're right. The cached object is overwritten after the submission of the subsequent step. This was sufficient for my purposes when writing my own multistep registration form. If you'd like a more persistent cache, you could merge (rather than overwrite) new data into the cached object after each step. If that doesn't seem like a good solution to you, you could consider creating a secondary object for this purpose.

Thanks very much these types of tutorials help to understand the Drupal api and its power.

I have two different profile types and I will like the users registering to be able to select the profile type (consultant or organization) that they want to register as prior to filling the registration form. I am not a programmer and seem to miss few things. Do I have to use this code and set it up as a module, with the .info and .module in the same folder. What about the hook that was mention? What do I have to do with the ctools wizard. Please guide me through

Excellent article with elaborate explanation. This is of great help for drupal newbies like me.
Thanks a Ton.

Cheers,
RJ

Fabulous tutorial, and really easy to follow! I have noticed a teeny markup error in your second from last code snippet that hooks the form to the registration form for modification:

This line:

function hook_form_user_register_alter(&$form, &form_state) {

The "&form_state" should be "&$form_state" - missing the dollar sign! =)

I also found that the FORM_ID part of the hook (hook_form_FORM_ID_alter()) needed to be "user_register_form" rather than just "user_register" in my implementation. Making the function:

function mycustommodule_form_user_register_form_alter(&$form, &$form_state){

Please, please, disable this module.

Thanks for great tutorial!
Is there an option to copy example code with formatting? I'm getting only super long one line comments that can't be tidied up both by my editor PHP formatter nor online formatter as well.

Special handling of Copy function is handled by Javascript.
So the solution is simple:
- Disable Javascript on website
- Reload it
- Copy the text (it will behave corectly without any JS reformatting)

Hi, this is a great tutorial, I've found it to be a great help trying to create a monster registration form for my site, at this point I'm still a little unclear on exactly how all the data is stored, in terms of using the caching method. Looking at the comment on 15 March about overwriting the object after each pass, how would I make it so that all the data is stored incrementally rather than dropping out after each step, I'm looking to make a penultimate 'confirm your details' step before the data is actually submitted to Drupal, can you provide any help?

Kind regards

another thing is that the code must be in 3 section
I - grasmash_registration_ctools_wizard.inc file that include "The Tut" and "Data caching" and "Submit callbacks" sections codes.
II - grasmash_registration_groups_form.inc file that include "Child Form callbacks" codes
III - grasmash_registration.module file that include "Integration with Drupal core user registration form" and "Displaying the first step of our form in a block" sections codes.

thanks for this Amazing tutorial and comments.

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.