ShvetsGroup

 

Captions

RSS Our blog, keeping you up-to-date on our latest news.

 

Multistep registration form in Drupal 6

2 comments

Multistep registration form in Drupal 6

Steps

Every Drupal user is probably familiar with its registration—we’ve all been through that. Same thing with the developers—a login and password form with a "Register" clicking which starts form processing.

But things change once there’s a task to collect additional data about the user during registration. For example, you may want to know where the visitor comes from, how old he is or where he works. Pros and cons of collecting such data at the registration stage in terms of User Experience can be discussed separately, in this article we are going to analyze the technical part from a programmer's viewpoint.

So, we need to collect additional data—it means, that in addition to login and password fields the registration form will require other ones that will make the form huge. Most probably, the large number of fields will scare away the user and make him leave without clicking the button. User interface designers have found a solution to this problem—a multistep form. This form allows users to enter data step by step: this way each step features a reasonable number of fields and user doesn’t need to answer some unnecessary questions based on his previous input. In addition, the user can see the registration progress in the form of step indicator, which is also important ( "When will this end?" , "How much more do I have to fill in?" ).

Possible Solutions

Multistep forms are usually enabled in Drupal 6 in one of two ways:

  • By means of core method (using Forms API) – the submitted form saves intermediate values in $form_state['storage'] and then rearranges by setting $form_state['rebuild'] to TRUE

    The disadvantage of this approach is the lack of flexibility - all steps should feature the same form. Moreover, Forms API provides us with only the form itself, therefore the transfer of intermediate values and everything else, including the interface, we’ll have to do ourselves.

  • CTools form wizard ("master form") comes within the Chaos Tools module and is a suite of tools for implementing multistep forms: the developer gets tools for creating steps, saving and transferring intermediate values between the steps, an out of the box interface with buttons to navigate between the steps and the progress indicator. In this article we are going to expand on the description of the second solution.

Wizard, make us a form

It should be noted that the form wizard works a bit different from Forms API. In particular, instead of drupal_get_form() the wizard uses ctools_build_form(). Because every form generation function is processed separately, each of them receives sort of a "blank" at the input with ready-made buttons to navigate between steps.

These findings made by Earl Miles were integrated into Drupal core, and in version 7 we’ll be able to see similar work with (multistep) forms.

The detailed CTools form wizard documentation can be found in the CTools module itself - either by manually opening the HTML file or using the Advanced Help module by Earl Miles. In addition, this wizard has a nice description in the article by Nick Lewis. We’ll briefly dwell on the basic properties of the given solution.

Single point of entry into all the steps

For building the multistep form a general page callback function is created.
This is where we add the general logic processing for all steps.
Each step has its identifier: at the entry point we determine the current step and define this identifier, which then becomes accessible for functions working with forms through $form_state['step'].

It should be noted that that most of the work is done by the form wizard. We prepare the parameters of our Form Wizard in an array that we pass as function argument ctools_wizard_multistep_form(), which the whole "magic" happens.

Own form caching

Drupal core wrongly stores form data in the default cache. Cache, by definition is temporary data that can be easily lost and rebuild from scratch. However, form data loss is highly undesirable, that’s why Earl Miles has created a more reliable cache version within the CTools module, storing objects in the database in a serialized form - CTools object cache (see "path_to_the_module_ctools/includes/object-cache.inc"). The CTools author recommends it for transferring values between the steps.

You may wonder whether: "Drupal's cache is also stored in the database?"
As a matter of fact, Drupal allows transferring the cache into memory, for example, using Memcache. The safety of data storage will decline, while CTools object cache will continue to work in the database with any configuration of the standard Drupal cache.

Every step has its own, unique form

Each step is processed by a separate form generation function. This way, each step’s form is assigned a unique identifier, its own submit and validation callback - where general or individual logic of step’s work can be described. This property is complemented with a simple structuring of steps’ code - each step can get a separate include-file.

Examples

Suppose that we have already created an element for our wizard in hook_menu(). Let’s take a look at this element’s page callback (the same single point of entry):

function mymodule_signup_wizard($step) {
  // Include CTools form wizard code and CTools object cache. 
  ctools_include('wizard');
  ctools_include('object-cache');
 
  // Wizard configuration. A complete set of parameters is best viewed in 
  // documentation. 
  $form_info = array(
  // Wizard’s identifier. 
    'id' => 'my_signup',
    // Path to the pages of the wizard (the current step is a variable 
    // argument). Should be the same as the path defined in hook_menu(). 
        'path' => "user/join/%step",
    // To show or not to show the progress
    'show trail' => TRUE
    // Manage the visibility of additional navigation buttons. 
    'show back' => FALSE,
    'show cancel' => FALSE,
    'show return' => FALSE,
    // Button texts
    'next text' => t('Next'),
    // Special functions prompted by the wizard 
    // - when navigating the to the next step 
    'next callback' =>  'mymodule_wizard_next',
    // - at the completion of the final step 
    'finish callback' => 'mymodule_wizard_finish',
    // Array that describes the identifiers and the names of steps 
    // and their order. 
    'Order' => array ( 
    'order' => array(
      '1' => t('Welcome'),
      '2' => t('Profile'),
      '3' => t('Finish'),
    ),
 
    // Array, where the parameters are set for each form (the form of each step) 
    // The form_id sets the form identifier, which is 
    // also the name of the form building function. 
    // The specified form_id are also used to determine the function name 
    // for the current step: 
    // - the function $form_id . '_validate' is used for validation, 
    // - $form_id . '_submit' is used for submission. 
    // Here you can define additional parameters, such as 
    // include files that contain the code for each step (for the full list 
    // of parameters, see documentation). 
    'forms' => array(
      '1' => array(
        'form id' => 'mymodule_step1',
      ),
      '2' => array(
        'form id' => 'mymodule_step2',
      ),
      '3' => array(
        'form id' => 'mymodule_step3',
      ),
    ),
  );
 
  // An example of working with cache: this is where the transfer of
  // information between the steps is carried out. Earl Miles recommends
  // writing wrapper functions around CTools cache objects - 
  // mymodule_cache_get() and mymodule_cache_set() functions.
  // In this example it’s assumed that we store data transferred 
  // between the steps within the $signup object. 
    $signup = mymodule_cache_get();
  if (!$signup) {
    // Set step = 1 - we have no data from the cache. 
    $step = current(array_keys($form_info['order']));
    $signup = new stdClass();
    mymodule_cache_set($signup);
  }
  // This way the data from the cache are available within each step within 
  // $form_state array. 
  $form_state['signup_object'] = $signup;
 
  // Generate the form output for the current step. 
$output = ctools_wizard_multistep_form($form_info, $step, $form_state);
 
  return $output;
}

It is worth noting that we can manipulate the array $form_info in all possible ways, for example, change the available buttons and their texts, depending on the step.

Example code for step 1:

/**
 * Generation of a form. 
 * The difference from usual form generation functions is that we do not return 
 * the form array, but change the existing form transferred by a link. 
 * Note that $form already contains buttons for the current step thanks to
 * the wizard. 
 */ 
function mymodule_step1(&$form, &$form_state) {
  $form['age'] = array(
    '#type' => 'textfield',
    '#title' => t('Please enter your age'),
  );
}
 
/**
 * Check the form values - there is nothing special compared to 
 * standard Forms API.
 * / 
function mymodule_step1_validate(&$form, &$form_state) {
  if ($form_state['values']['age'] < 18) {
    form_set_error('age', t('You are too young!'));
  }
}
 
/**
 * In most cases we do not save data within the steps, and only 
 * validate and prepare them for wizard’s finish, where they are saved 
 * inside the function, which we defined as "finish callback" (see 
 * wizard settings). 
 */ 
function mymodule_step1_submit(&$from, &$form_state) {
 
  // Transfer the information to the following steps by saving in the object
  // that goes to cache. 
  // This object should be saved in cache within the function, 
  // defined as the "next callback".
  $form_state['signup_object']->age = $form_state['values']['age'];
}

Example of a registration wizard

I'll describe the nuances that have arisen during the implementation of multistep registration for one of our clients. The task was to register users, collect a certain amount of personal information, create a profile based on the Content Profile module and save the collected data in the profile, as well as to offer the user to invite friends to the website.

The requirements included:

  • to sign in the user had to confirm he owns the e-mail he specified during the registration
  • we must allow the user to skip all the steps except from the first one
  • after step 1 the user can quit the wizard without having to come back

Pretext: the given pieces of code are significantly simplified/abridged for this article. The author does not guarantee their performance in case of copy-pasting.

Step 1. Registration

In general, the basic actions necessary for registration take place on step 1, but the user does not know about it. This is done to collect the maximum information.

Build the form:

function mymodule_step1(&$form, &$form_state) {
  $form['mail'] = array(
    '#title' => t('Email'),
    '#type' => 'textfield',
    '#required' => TRUE,
    '#size' => 30,
    '#weight' => 2,
  );
  // In this case, user name is the value, meaning it’s 
  // a value the user doesn’t change and fills with random data. 
  // This is done because the email registration and
realname modules were used.  
  // Readers may use the standard solution, where 
  // the user chooses his name himself. 
    $form['name'] = array(
    '#type' => 'value',
    '#value' => user_password(),
  );
 
  $form['pass'] = array(
    // You can also use the password_confirm type that outputs 
    // two fields with match test
    '#type' => 'password',
    '#title' => t('Password');
    '#required' => TRUE,
    '#size' => 30,
    '#weight' => 3,
  );
 
  // Add the profile field. The work with CCK fields will be discussed later, 
  // in the description of step 2. 
  $fields = mymodule_step_fields($form_state['step']);
  mymodule_add_profile_fields($fields, $form, $form_state, FALSE);
}

We process the form fields and register the user. Data verification is made simple with core native functions. To complete such verification we need our form elements to have the same names as in the core-this is what we’ve already taken care of.

function mymodule_step1_validate(&$form, &$form_state) {
  // Call the registration field validation function from the core 
  user_register_validate($form, $form_state);
 
  // Check the profile data. 
  // CCK field procession will be discussed in detail in the description of step 2. 
  mymodule_profile_validate_fields($form, $form_state);  
}
 
function mymodule_step1_submit(&$form, &$form_state) {
  ..
  $user_array = array(
    'name' => $form_state['values']['name'],
    'mail' => $form_state['values']['mail'],
    'pass' => $form_state['values']['pass'],
    'init' => $form_state['values']['mail'],
    'status' => 1,
  );
 
  $account = user_save('', $user_array);
  // At this stage the user account has been created. If necessary, 
  // Alert the user and create a watchdog entry. 
  .. 
 
  // Create user profile. This functionality is discussed later, 
  // in step 2. 
  mymodule_profile_submit_fields(&$form, &$form_state);
}

Note that you want to save data after every step. Therefore, in this case form caching forms is hardly used.

Because the user is unable to sign in prior to the e-mail confirmation, but must be able to complete the wizard, the task was to link the current user to his account and profile. This problem was solved by storing the service information about the user in his session ($_SESSION).

This information is transferred there on submit form stage on step 1.

Step 2. Data collection for the additional profile fields.

During the implementation of the form wizard it turned out that the collected data can be divided into 2 types depending on their processing complexity:

  • own fields
  • CCK fields (the Content Profile fields in our case)

Own fields are easy for those, who know Forms API – own elements are created in a form and processed in a standard way within the validate and submit functions. CCK fields are quite different: they have their own processing and data validation logic (each field has his own, actually) both on field and widget levels that should work in our wizard, too. It was found that writing logic from scratch separately for each field is too labor-intensive and irrational. Therefore, we worked out 2 ways to implement CCK fields within their own forms:

  1. an element is created within the form that simulates the work of the field.
  2. a fake node form is created, all the necessary fields from which are copied into our form.

In case of the first technique the data are validated via validation callback, to which the verification logic is copied from the code of the field itself, then the validated data within submit callback are placed into profile node object and saved using node_save(). The technique works if the field is simple in terms of form and processing logic. Basically, this is a standard solution.

The second technique was taken from the Content Profile User Registration module. There are different opinions as to whether it is a hack, but for complex CCK fields it is much more efficient than the first technique, so its use is justified.

Now let’s discuss the second technique. In this case working with fields includes the following actions:

  1. creating an auxiliary node form of the desired type and copy the required fields int our form
  2. transforming the $form_state['values'] into the node object at the stage of validation and validate the object using the content_validate() function . This function starts the native validation of the values of each field
  3. after a successful validation, depending on the step, save the ready object as a user profile (in case if one does not exist), or copy the field values from $form_state['values'] into the new array we pass to drupal_execute() (if the profile already exists).

Let us turn to examples. To begin - function

 mymodule_add_profile_fields () </ code>. This function creates an auxiliary form of nodes and copies out the required fields in our form. 
 
<pre><code class="php">
/**
 * A modified version of the function content_profile_registration_add_profile_form()
 * 
 * $node - the node object from which the fields are copied 
 * $fields contains an array of added CCK fields 
 * $type - type of profile node, "profile"  in our case 
*/
function mymodule_add_profile_fields($fields, &$form, &$form_state, $node = FALSE, $type = 'profile') {
  // Depending on the step, the existing node is passed into the function, or 
  // a new one is created. 
   if (!$node) {
    $node = array('uid' => 0, 'name' => '', 'type' => $type);
  }
 
  // Create an additional node form. 
  $node_form = drupal_retrieve_form($type .'_node_form', $form_state, $node);
  drupal_prepare_form($type .'_node_form', $node_form, $form_state);
 
  $node_form += array('#field_info' => array());
  $form_add = array();
 
  // If form elements not associated with CCK are not added, copy 
  // only CCK fields. 
  if (!in_array('other', $fields)) {
    foreach ($node_form['#field_info'] as $field_name => $info) {
      if (isset($node_form[$field_name])) {
        $form_add[$field_name] = $node_form[$field_name];
      }
    }
    // Copy the field groups. 
        $keys = array_keys($node_form);
    foreach ($keys as $key) {
      if (stristr($key, 'group_')) {
        $form_add[$key] = $node_form[$key];
      }
    }
    // Add title 
    $form_add['title'] = $node_form['title'];
 
    // Set these values equal to the value of the node (from 
    // the auxiliary form) as it can be necessary for 
    // #ahah callbacks.
    $form_add['#node'] = $node_form['#node'];
    $form_add['type'] = $node_form['type'];
  }
  else {
    foreach (array('uid', 'name', 'author', 'buttons', 'language', '#theme', 'options') as $key) {
      unset($node_form[$key]);
    }
    $form_add = $node_form;
  }
 
  // Correct the form taking into account the list of fields we need. 
  $all_fields = _content_profile_registration_get_fields($type);
  $all_fields['title'] = 'title';
  $all_fields['other'] = 'other';
  foreach ($all_fields as $field_name => $field_info) {
    if (!in_array($field_name, $fields)) {
      if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type, $field_name))) {
        unset($form_add[$group_name][$field_name]);
        if (count(element_children($form_add[$group_name])) == 0) {
          unset($form_add[$group_name]);
        }
      }
      else {
        unset($form_add[$field_name]);
      }
    }
  } 
 
  // Add new elements to the form $form. 
  $form += array('#field_info' => array());
  $form['#field_info'] += $node_form['#field_info'];
  $form += $form_add;
 
  // This function is called for shifting the fields - 
  // you don’t need to use it if you plan to use your own 
  // mechanism of field "weight" processing. 
  $form['#pre_render'][] = 'content_profile_registration_alter_weights';
 
 
  // Field order ("weight") processing 
  $form += array('#content_profile_weights' => array());
  $weight = content_profile_get_settings($type, 'weight') + 1;
  foreach (element_children($form_add) as $key) {
    $form['#content_profile_weights'] += array($key => $weight);
  }
 
  if (isset($node_form['#attributes']['enctype'])) {
    $form['#attributes']['enctype'] = $node_form['#attributes']['enctype'];
  }
}

As a result of this function we get our form with the addition of the required fields. After that these fields should be validated and saved, if necessary.

The function that validates the values entered into CCK fields (validation stage):

function mymodule_profile_validate_fields(&$form, &$form_state) {
  // $signup_uid must contain profile owner’s 
  // user id. In our case we’ve set it 
  // based on session data. 
  $signup_uid = ..
 
  require_once drupal_get_path('module', 'node') .'/node.pages.inc';
 
 
  // With a subtle motion of the hand we convert the form values
  // into the object we’ll validate using the standard mechanism 
  // of CCK field validation. 
  $node = (object)$form_state['values'];
  $node->type = 'profile';
 
  // This is a kind of a “duct tape” required to bypass the smart Content 
  // Profile module. If at this stage the user profile already exists 
  // and we prepare the node object with an id different from user profile id, 
  // Content Profile thinks we're trying to create a second 
  // user profile and performs a redirect that we 
  // should avoid. 
  // In our case the profile exists starting from step 2
  if ($form_state['step'] > 1) {
    $profile = content_profile_load('profile', $signup_uid);
    $node->nid = $profile->nid;
  }
 
  // Prepare a list of fields for validation. It is convenient to have a 
  // mymodule_step_fields() function that returns the array of names CCK
  // field names for adding, depending on the current step. 
  $fields = mymodule_step_fields($form_state['step']);
 
  node_object_prepare($node);
 
 
  // Unset the node_validate username if it exists. 
  unset($node->name)
 
  // Validate the fields with different functions, depending on whether we need 
  // "other" node fields – i.e. whether we really need to validate any other 
  // form elements, except for the CCK fields (see Content Profile User Registration module). 
    if (in_array('other', $fields)) {
    node_validate($node);
  }
  elseif (module_exists('content')) {
    content_validate($node);
  }
 
  if ($form_state['step'] == 1) {
 
    // The first step is interesting, because we already have a
    // prepared profile node for saving. We can save it for our submit 
    // callback. 
    $form_state['signup_temp_profile'] = &$node;
 
  // At this stage we’ve validated the fields and can remove them from the form, if necessary. 
    foreach ($fields as $field) {
      // Here we use the function from content_profile_user_registration module 
      // but you can write your own function. 
_content_profile_registration_remove_values($field, $form[$field], $form_state);
    }
  }
}

The function that saves the values in the fields (submit stage):

function mymodule_profile_submit_fields(&$form, &$form_state) {
  // $signup_uid must contain the user id of the 
  // profile owner. In our case we set it 
  // based on session data. 
  $signup_uid = ..
 
  // Do we have a profile on this step?
  if ($form_state['step'] > 1) {
    // Update the existing profile. 
    $profile = content_profile_load('profile', $signup_uid);
 
    module_load_include('inc', 'node', 'node.pages');
 
    $fields = mymodule_step_fields($form_state['step']);
 
    // Prepare values by copying the values from the required fields into the new array. 
    foreach ($fields as $field) {
      $form_state_new['values'][$field] = $form_state['values'][$field];
    } 
 
    // Which action is being emulated (which button is pressed)? 
    // Without it drupal_execute does not know what to do with the form. 
    $form_state_new['values']['op'] = t('Save');
 
    // mymodule_drupal_execute('profile_node_form', $form_state_new, $profile);
 
  }
  else { 
    // We are on step 1. 
    // Save the user profile, which we have prepared at the validation stage. 
    $node = &$form_state['signup_temp_profile']; 
 
    // Set the title. 
    if (empty($node->title) && (!module_exists('auto_nodetitle') || auto_nodetitle_get_setting('profile') != AUTO_NODETITLE_OPTIONAL)) {
      $node->title = $form_state['user']->name;
    } 
    // We have not reviewed all the form processing, since this article 
    // is all about the CCK field processing. 
    // In our version of the wizard step 1 features a form the processing of which
    // starts with creating a user account 
    // and the user object is saved to 
$form_state['user'].
    $node->uid = $form_state['user']->uid;
    $node->name = $form_state['user']->name;
 
 
    // Save the node. 
    $node = node_submit($node);
 
    node_save($node);
 
    if ($node->nid) {
      watchdog('content', 'Content Profile: added %user %type upon registration.', array('%user' => $node->name, '%type' => 'profile'), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
    }
  }
 
}

At the submit stage we face a problem: the profile form may contained the required fields, which we did not fill. drupal_execute() won’t let the form with blank required fields get processed.

To address this issue we wrote the mymodule_drupal_execute() function – similar to drupal_execute(), which makes all the form elements optional before the processing.

function mymodule_drupal_execute($form_id, &$form_state) {
  $args = func_get_args();
  $args[1] = &$form_state;
 
  $form = call_user_func_array('drupal_retrieve_form', $args);
 
  $form['#post'] = $form_state['values'];
 
  drupal_prepare_form($form_id, $form, $form_state);
 
  // Disable the red field property. 
  _mymodule_dont_require($form);
 
  drupal_process_form($form_id, $form, $form_state);
}
 
/** 
 * Recursive function of disabling the required field property. 
 */
function _mymodule_dont_require(&$element) {
  foreach (element_children($element) as $child) {
    _mymodule_dont_require($element[$child]);
  }
  if (isset($element['#required'])) {
    $element['#required'] = FALSE;
  }
}

It may seem that the number of auxiliary code is enormous and unjustified. However, eventually, we get the code that allows us to add any CCK fields into the form of any step, and we can change the list of added fields, by simply changing only one function - mymodule_step_fields()

This way we are saving a huge amount of time: basically the CCK and each field’s code do the job for us. It only remains for us to implement the processing of the form data, not related to the profile.

The disadvantage of this approach is that not all the fields will work in other forms out of the box. Some extra notional field may require additional care to make it work in another form.

Step 3. Invite friends

The user is shown a form of friend invitation (based on the Invite module). The form is built by simply calling the invite_form() function within our form building function.

function mymodule_step3(&$form, &$form_state) {
  ..
  $form += invite_form($form_state, 'page');
  ..
}

Thus, in the form of this step includes all the elements of the standard Invite module form and it remains to perform native submit and validate callback functions:

function mymodule_step3_validate(&$form, &$form_state) {
  invite_form_validate($form, $form_state);
}
 
function mymodule_step3_submit(&$form, &$form_state) {
  invite_form_submit($form, $form_state);
}

Step 4. Finish

"Referral code" is a simple field, processed using the referral module (there’s no point describing it within this article).

"How did you hear about us?" - a profile field, processed in the way already described in step 2.

Bottom line

As you can see from the article, step by step registration in Drupal is not simple, but it’s possible. The main source of problems during the implementation of multistep registration can be the weakness of Drupal as a framework, and excessive CMS-orientation of lots of modules. For example, the reader must have already noticed how much hack-like code we have to use to make the CCK fields work in some other forms: CCK developers did not expect someone would need it.

In Drupal 7 the whole field situation should get improved with the advent of more technically sophisticated Field API, and working with forms in general will progress thanks to Earl Miles’ core related developments.

Comments

soulston
October 29th, 2010

Wow - thanks for taking the time to write this. I've not tried it out yet but I'm sure it will come in handy.

bisaram
December 10th, 2010

линк "How to solve this?" под статьей косячный.

Got anything to add?