Initializing and Refreshing a Multi-Step Form

I've been working on a relatively complex multi-step form for a while now. It takes a URL argument that represents a set of stored options to control display and behavior. Thanks to $form_state['storage'] that is pretty easy to grab once and keep through all the steps.

However, I was having some occasional strange behavior with JS settings and page refreshes. Poking around a bit, it turns out that assigning values into storage outside of a submit handler isn't a great idea. That's good to know, since pretty much every example of multi-step forms I've seen makes a step assignment within the constructor.

My solution, outlined below, is to do a programmatic submit when the form first loads. Then we can grab the options and save them from within the submit handler.

This has a nice side-effect of allowing the form to detect browser refreshes. For this form, it just starts over on a hard refresh, but other behaviors should be possible.

<?php
// form constructor
// $option_id is a URL argument
function multi_step_form(&$form_state, $option_id = NULL) {
 
$form = array();
 
$step = $form_state['storage']['step'];

  if (
$step) {
    switch (
$step) {
     
// SNIP - build the various steps
   
}
  }
  else {
   
// we're just starting out. programmatically submit the form.
   
$form['option_id'] = array(
     
'#type' => 'hidden',
     
'#value' => check_plain($option_id),
    );
   
$form_state['submitted'] = TRUE;
   
multi_step_form_submit($form, $form_state);
  }
  return
$form;

// submit handler
function multi_step_form_submit($form, &$form_state) {
 
// use the resubmit detected in _multi_step_load_options to reset
 
if ($form_state['storage']['resubmitted']) {
   
$form_state['storage']['step'] = 'one';
   
$form_state['storage']['resubmitted'] = FALSE;
  }
  else {
    switch (
$form_state['storage']['step']) {
     
// SNIP - process flow depending on step
     
default:
       
// initialize if no step is set
        // this is the result of the programmatic submit above
       
_multi_step_load_options($form['option_id']['#value'], $form_state);
        break;
    }
  }
}

function
_multi_step_load_options($option_id = NULL, &$form_state) {
 
// get the option set, or a default
 
$options = multi_step_options_load($option_id);
  if (!
$options) {
   
$options = multi_step_options_load(DEFAULT_OPTION_ID);
  }
 
 
// store the options and start the workflow
 
$form_state['storage']['options'] =  $options;
 
$form_state['storage']['step'] = 'one';

 
// since this gets programatically called on initial generation,
  // it should never see a $_POST. if it does, we know the user
  // refreshed the form. let's throw them back to the start
  // in multi_step_form_submit()
 
if ($_POST) {
   
$form_state['storage']['resubmitted'] = TRUE;
  }
}
?>