Ordering Drupal hooks and callbacks

A while ago someone was testing me on my Drupal knowledge.  He inquired which hook gets called first, hook_form_alter() or hook_form_FORM_ID_alter().  I was a bit puzzled since nothing in the system api specifies that one is called first and wondered why he was not using other approaches that are in the api.  Hook_form_FORM_ID_alter() he crowed triumphantly.  I thought it strange to rely on the order of calling these two hooks since there is nothing that guarantees that the order will always be the same.  In fact, his claim is only true for Drupal 6.  The order is switched in Drupal 7.  And it is possible the order is not the same even within Drupal 6 or Drupal 7 versions. 

We can verify this by looking at the source code.  For Drupal 6.19 here is the code:

$data = &$form;
  $data['__drupal_alter_by_ref'] = array(&$form_state);
  drupal_alter('form_'. $form_id, $data);

  // __drupal_alter_by_ref is unset in the drupal_alter() function, we need
  // to repopulate it to ensure both calls get the data.
  $data['__drupal_alter_by_ref'] = array(&$form_state);
  drupal_alter('form', $data, $form_id);

So we see the first call to drupal_alter() has the first agrument as 'form_'. $form_id while the second call to drupal_alter is just 'form'.  So for Drupal 6.19 the more specific form of the hook is called first.

For Drupal 7.0 here is the code:

  // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
  // hook_form_FORM_ID_alter() implementations.
  $hooks = array('form');
  if (isset($form_state['build_info']['base_form_id'])) {
    $hooks[] = 'form_' . $form_state['build_info']['base_form_id'];
  }
  $hooks[] = 'form_' . $form_id;
  drupal_alter($hooks, $form, $form_state, $form_id);

If we print out the hooks array for a simple search form we get:

array(3) {
 [0]=> string(4) "form"
 [1]=> string(15) "form_search_box"
 [2]=> string(22) "form_search_block_form" 
}

 

So the less specific hook is called first in this case.

So what is the proper way to get the calls for altering a form in the proper order?  There are several other ways to alter a form before it is presented to the visitor.  The most popular way is to use the #after_build callback.  This is guaranteed to be called after all the hook_form_alter() and hook_form_FORM_ID_alter() functions are called.   In your hook_form_FORM_ID_alter() provide the code

function MYMODULE_form_FORM_ID_alter(&$form, &$form_state) {
  ...
  $form['#after_build'][] = 'MYMODULE_after_build_callback';
  ...
}

function MYMODULE_after_build_callback($form, &$form_state) {
  // changes to the form
  return $form;
}

Notice the brackets after the $form['#after_build'].  This adds your callback without wiping out any that have already been added.  Also, the callback has to have a return $form at the end since it is not passed by reference.  If you run into a problem that the order of the after build callbacks is causing problems then change the order of the callbacks.  The code above places your callback at the end of the existing callbacks.  Something like

$form['#after_build'] = array_merge(array('MYMODULE_after_build_callback'), $form['#after_build']);

will place your callback at the beginning of the callbacks.

Recently I encountered a situation where my module's call to hook_nodeapi() and another module's hook_nodeapi() were executing in the wrong order.  This hook is for manipulating nodes but the idea is the same as for hook_form_alter().  How do you change the order that the hooks are called in a way that can be guaranteed to work?  One way is to use the Util module.  This allows weighting the modules and the hooks will be called in ascending order of the weights.  But having to use a whole module for this is overkill.  It is also possible to set the module weight yourself.  In your MYMODULE.install file, put in the code:

function MYMODULE_install() {
  db_query('Update {system} SET weight=1 WHERE name="MYMODULE"');
}

The weight should be set to the appropriate value.  The Util module is handy for finding out the weights of other modules and testing to get the right value before using this code.  This code gets executed when the module is installed and alters the system table.

Another possible approach is to change the module name.  I have heard that the hooks are ordered alphabetically if they have the same weight.  I have tried this when debugging a problem but cannot say that I have actually successfully been able to use a different module name.

Categories