2

I have a simple menu callback that draws a page with a form on it. The page callback does a very expensive operation (requests a bunch of data from an external API).

function test_menu() {
  $items = array();
  $items['test'] = array(
    'title' => 'Test',
    'page callback' => 'test_page_handler',
    'access callback' => TRUE,
  );
  return $items;
}

function test_page_handler() {
  $data = very_expensive_data_operation();
  $output = drupal_get_form('test_simple_form', $data);
  return $output;
}

function test_simple_form(&$form_state, $data) {
  $form['create'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

function test_simple_form_submit(&$form, &$form_state) {
  // do something
}

The problem I am seeing is that when the form is submitted, Drupal will re-execute the page callback in the background, resulting in the very_expensive_data_operation() running again.

There are probably lots of great reasons for Drupal to do this, but how can I change my app so that the very_expensive_data_operation() doesn't have to run again when submitting the form (after all, that's why I'm passing $data to the form, rather than having the form run it directly.)

One workaround I can think of is to toss $data into a cache table, but it feels like I'm missing something that available to me in the Drupal+FAPI api.

2 Answers 2

1

You can also try using drupal_static

Provides central static variable storage.

All functions requiring a static variable to persist or cache data within a single page request are encouraged to use this function unless it is absolutely certain that the static variable will not need to be reset during the page request. By centralizing static variable storage through this function, other functions can rely on a consistent API for resetting any other function's static variables.

function test_menu() {
  $items = array();
  $items['test'] = array(
    'title' => 'Test',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('test_simple_form'),
    'access callback' => TRUE,
  );
  return $items;
}

function expensive_data() {
  $data = &drupal_static(__FUNCTION__);
  if (!isset($data)) {
    $data = very_expensive_data_operation();
  }
  return $data;
}

function test_simple_form(&$form_state) {
  $data = expensive_data();
  $form['create'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

function test_simple_form_submit(&$form, &$form_state) {
  // do something
}

You might notice that the static variable technique only stores data for the duration of a single page load. For even better performance try this

function expensive_data() {
  $data = &drupal_static(__FUNCTION__);
  if (!isset($data)) {
    if ($cache = cache_get('my_module_data')) {
      $my_data = $cache->data;
    }
    else {
      $data = very_expensive_data_operation();
      cache_set('my_module_data', $my_data, 'cache');
    }

  }
  return $data;
}

If you want to refresh your cache from time to time you can try

cache_set('my_module_data', $my_data, 'cache', time() + 360); 

OR you can manually rebuild your cache using

cache_clear_all('my_module', 'cache', TRUE); 
0

I think the best place to run your expensive function is in the form itself, even though you've mentioned you're currently avoiding that.

As it stands, your page callback has no link to the form that's currently being submitted; there are ways you could get it from the cache_form table, but it's a bit messy.

Instead, I'd advise saving the data to a variable in the form, and checking to see if that variable has been submitted in the $form_state array. You'll also need to flag the form to rebuilt in the submit handler:

function test_simple_form(&$form_state) {
  // If the form has already been biult, and we already have the data, don't load it again.
  if (isset($form_state['values']['expensive_data'])) {
    $data = $form_state['values']['expensive_data'];
  }
  else {
    // Otherwise get the data from source.
    $data = my_expensive_function();
  }

  // Store the data
  $form['expensive_data'] = array(
    '#type' => 'value',
    '#value' => $data
  );

  // ...

  return $form;
}

function test_simple_form_submit(&$form, &$form_state) {
  $form_state['rebuild'] = TRUE;
}   

In your submit handler you'll have access to $form_state['values']['expensive_data'], which will contain the results of the function call.

This method has the added benefit of caching the data if the form validation fails, not just when it's submitted successfully.

Judging from the function signature you're using for the form I'm guessing you have a Drupal 6 site, although the above method will work equally well for a Drupal 7 site (the function signature would just need to change).

8
  • This actually doesn't solve the problem. When you originally draw the form, my_expensive_function() will be run. When you submit the form, it will be run again. Here's my simple test: pastebin.com/07WfQTkw
    – rcourtna
    Commented Jun 7, 2012 at 19:52
  • @rcourtna Sorry about that I forgot the most important bit (flagging the form to be rebuilt). I've updated the answer; the code's tested and very_expensive_data_operation() doesn't run when the form is submitted
    – Clive
    Commented Jun 7, 2012 at 20:05
  • My test shows that very_expensive_data_operation() does run again. So it gets run when originally drawing the form, and when submitting the form. You can see this by setting a status message in both very_expensive_data_operation() and in the submit handler. pastebin.com/yiJNGrSC
    – rcourtna
    Commented Jun 7, 2012 at 20:30
  • I copied your original code directly, added the submit handler, and the message in very_expensive_data_operation() was only shown when the form was originally built. When the form was submitted, there was no message shown. It's on a blank Drupal 6.26 installation so I'm not really sure what to suggest
    – Clive
    Commented Jun 7, 2012 at 20:33
  • I'm also on a blank 6.26. Draw original form, VEDO message gets set. Submit, a new VEDO message gets set and then finally the form submit function is called. The only effect that setting $form_state['rebuild'] = TRUE has is that the expensive function doesn't get called a third time (ie: after the form is submitted and the client simply redraws the form page) - the same effect can be achieved by setting form_state['redirect'] = 'node' (instead of setting $form_state['rebuild'] = TRUE).
    – rcourtna
    Commented Jun 7, 2012 at 21:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.