Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.454 diff -u -p -r1.454 form.inc --- includes/form.inc 24 Apr 2010 14:49:13 -0000 1.454 +++ includes/form.inc 27 Apr 2010 19:19:29 -0000 @@ -774,11 +774,8 @@ function drupal_prepare_form($form_id, & } } - // Invoke hook_form_FORM_ID_alter() implementations. - drupal_alter('form_' . $form_id, $form, $form_state); - - // Invoke hook_form_alter() implementations. - drupal_alter('form', $form, $form_state, $form_id); + // Invoke hook_form_alter() and hook_form_FORM_ID_alter() implementations. + drupal_alter(array('form', 'form_' . $form_id), $form, $form_state, $form_id); } Index: includes/module.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/module.inc,v retrieving revision 1.190 diff -u -p -r1.190 module.inc --- includes/module.inc 22 Apr 2010 22:36:01 -0000 1.190 +++ includes/module.inc 27 Apr 2010 19:27:38 -0000 @@ -786,7 +786,12 @@ function drupal_required_modules() { * * @param $type * A string describing the data type of the alterable $data. 'form', 'links', - * 'node_content', and so on are several examples. + * 'node_content', and so on are several examples. Alternatively can be an + * array, in which case hook_TYPE_alter() is invoked for each value in the + * array, ordered first by module, and then for each module, in the order of + * values in $type. For example, when Form API is using drupal_alter() to + * execute both hook_form_alter() and hook_form_FORM_ID_alter() + * implementations, it passes array('form', 'form_' . $form_id) for $type. * @param &$data * The primary data to be altered. * @param &$context1 @@ -804,14 +809,69 @@ function drupal_alter($type, &$data, &$c } $functions = &$drupal_static_fast['functions']; + // Most of the time, $type is passed as a string, so for performance, + // normalize it to that. When passed as an array, usually the first item in + // the array is a generic type, and additional items in the array are more + // specific variants of it, as in the case of array('form', 'form_FORM_ID'). + if (is_array($type)) { + $cid = implode(',', $type); + $extra_types = $type; + $type = array_shift($extra_types); + // Allow if statements in this function to use the faster isset() rather + // than !empty() both when $type is passed as a string, or as an array with + // one item. + if (empty($extra_types)) { + unset($extra_types); + } + } + else { + $cid = $type; + } + // Some alter hooks are invoked many times per page request, so statically // cache the list of functions to call, and on subsequent calls, iterate // through them quickly. - if (!isset($functions[$type])) { - $functions[$type] = array(); + if (!isset($functions[$cid])) { + $functions[$cid] = array(); $hook = $type . '_alter'; - foreach (module_implements($hook) as $module) { - $functions[$type][] = $module . '_' . $hook; + $modules = module_implements($hook); + if (!isset($extra_types)) { + // For the more common case of a single hook, we do not need to call + // function_exists(), since module_implements() returns only modules with + // implementations. + foreach ($modules as $module) { + $functions[$cid][] = $module . '_' . $hook; + } + } + else { + // For multiple hooks, we need $modules to contain every module that + // implements at least one of them. + $extra_modules = array(); + foreach ($extra_types as $extra_type) { + $extra_modules = array_merge($extra_modules, module_implements($extra_type . '_alter')); + } + // If any modules implement one of the extra hooks that do not implement + // the primary hook, we need to add them to the $modules array in their + // appropriate order. + if (array_diff($extra_modules, $modules)) { + // Order the modules by the order returned by module_list(). + $modules = array_intersect(module_list(), array_merge($modules, $extra_modules)); + } + foreach ($modules as $module) { + // Since $modules is a merged array, for any given module, we do not + // know whether it has any particular implementation, so we need a + // function_exists(). + $function = $module . '_' . $hook; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + foreach ($extra_types as $extra_type) { + $function = $module . '_' . $extra_type . '_alter'; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + } + } } // Allow the theme to alter variables after the theme system has been // initialized. @@ -825,12 +885,22 @@ function drupal_alter($type, &$data, &$c foreach ($theme_keys as $theme_key) { $function = $theme_key . '_' . $hook; if (function_exists($function)) { - $functions[$type][] = $function; + $functions[$cid][] = $function; + } + if (isset($extra_types)) { + foreach ($extra_types as $extra_type) { + $function = $theme_key . '_' . $extra_type . '_alter'; + if (function_exists($function)) { + $functions[$cid][] = $function; + } + } } } } } - foreach ($functions[$type] as $function) { + + foreach ($functions[$cid] as $function) { $function($data, $context1, $context2); } } + Index: includes/database/select.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/select.inc,v retrieving revision 1.36 diff -u -p -r1.36 select.inc --- includes/database/select.inc 27 Apr 2010 10:42:58 -0000 1.36 +++ includes/database/select.inc 27 Apr 2010 19:19:29 -0000 @@ -1069,10 +1069,11 @@ class SelectQuery extends Query implemen // Modules may alter all queries or only those having a particular tag. if (isset($this->alterTags)) { - drupal_alter('query', $query); + $hooks = array('query'); foreach ($this->alterTags as $tag => $value) { - drupal_alter("query_$tag", $query); + $hooks[] = 'query_' . $tag; } + drupal_alter($hooks, $query); } return $this->prepared = TRUE; } Index: modules/block/block.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.api.php,v retrieving revision 1.11 diff -u -p -r1.11 block.api.php --- modules/block/block.api.php 22 Apr 2010 09:12:35 -0000 1.11 +++ modules/block/block.api.php 27 Apr 2010 19:29:45 -0000 @@ -192,7 +192,7 @@ function hook_block_view($delta = '') { * - delta: The identifier for the block within that module, as defined within * hook_block_info(). * - * @see hook_block_view_alter() + * @see hook_block_view_MODULE_DELTA_alter() * @see hook_block_view() */ function hook_block_view_alter(&$data, $block) { @@ -213,10 +213,6 @@ function hook_block_view_alter(&$data, $ * Modules can implement hook_block_view_MODULE_DELTA_alter() to modify a * specific block, rather than implementing hook_block_view_alter(). * - * Note that this hook fires before hook_block_view_alter(). Therefore, all - * implementations of hook_block_view_MODULE_DELTA_alter() will run before all - * implementations of hook_block_view_alter(), regardless of the module order. - * * @param $data * An array of data, as returned from the hook_block_view() implementation of * the module that defined the block: Index: modules/block/block.module =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block.module,v retrieving revision 1.419 diff -u -p -r1.419 block.module --- modules/block/block.module 26 Apr 2010 14:10:40 -0000 1.419 +++ modules/block/block.module 27 Apr 2010 19:19:29 -0000 @@ -762,9 +762,8 @@ function _block_render_blocks($region_bl $array = module_invoke($block->module, 'block_view', $block->delta); // Allow modules to modify the block before it is viewed, via either - // hook_block_view_MODULE_DELTA_alter() or hook_block_view_alter(). - drupal_alter("block_view_{$block->module}_{$block->delta}", $array, $block); - drupal_alter('block_view', $array, $block); + // hook_block_view_alter() or hook_block_view_MODULE_DELTA_alter(). + drupal_alter(array('block_view', "block_view_{$block->module}_{$block->delta}"), $array, $block); if (isset($cid)) { cache_set($cid, $array, 'cache_block', CACHE_TEMPORARY); Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.48 diff -u -p -r1.48 form.test --- modules/simpletest/tests/form.test 11 Apr 2010 19:00:27 -0000 1.48 +++ modules/simpletest/tests/form.test 27 Apr 2010 19:19:29 -0000 @@ -210,6 +210,40 @@ class FormsTestCase extends DrupalWebTes } /** + * Test form alter hooks. + */ +class FormAlterTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Form alter hooks', + 'description' => 'Tests hook_form_alter() and hook_form_FORM_ID_alter().', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests execution order of hook_form_alter() and hook_form_FORM_ID_alter(). + */ + function testExecutionOrder() { + $this->drupalGet('form-test/alter'); + // Ensure that the order is first by module, then for a given module, the + // id-specific one after the generic one. + $expected = array( + 'block_form_form_test_alter_form_alter() executed.', + 'form_test_form_alter() executed.', + 'form_test_form_form_test_alter_form_alter() executed.', + 'system_form_form_test_alter_form_alter() executed.', + ); + $content = preg_replace('/\s+/', ' ', filter_xss($this->content, array())); + $this->assert(strpos($content, implode(' ', $expected)) !== FALSE, t('Form alter hooks executed in the expected order.')); + } +} + +/** * Test form validation handlers. */ class FormValidationTestCase extends DrupalWebTestCase { Index: modules/simpletest/tests/form_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v retrieving revision 1.37 diff -u -p -r1.37 form_test.module --- modules/simpletest/tests/form_test.module 11 Apr 2010 19:00:27 -0000 1.37 +++ modules/simpletest/tests/form_test.module 27 Apr 2010 19:19:29 -0000 @@ -10,6 +10,13 @@ * Implements hook_menu(). */ function form_test_menu() { + $items['form-test/alter'] = array( + 'title' => 'Form altering test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_alter_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); $items['form-test/validate'] = array( 'title' => 'Form validation handlers test', 'page callback' => 'drupal_get_form', @@ -142,6 +149,46 @@ function form_test_menu() { } /** + * Form builder for testing hook_form_alter() and hook_form_FORM_ID_alter(). + */ +function form_test_alter_form($form, &$form_state) { + // Elements can be added as needed for future testing needs, but for now, + // we're only testing alter hooks that do not require any elements added by + // this function. + return $form; +} + +/** + * Implements hook_form_FORM_ID_alter() on behalf of block.module. + */ +function block_form_form_test_alter_form_alter(&$form, &$form_state) { + drupal_set_message('block_form_form_test_alter_form_alter() executed.'); +} + +/** + * Implements hook_form_alter(). + */ +function form_test_form_alter(&$form, &$form_state, $form_id) { + if ($form_id == 'form_test_alter_form') { + drupal_set_message('form_test_form_alter() executed.'); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function form_test_form_form_test_alter_form_alter(&$form, &$form_state) { + drupal_set_message('form_test_form_form_test_alter_form_alter() executed.'); +} + +/** + * Implements hook_form_FORM_ID_alter() on behalf of system.module. + */ +function system_form_form_test_alter_form_alter(&$form, &$form_state) { + drupal_set_message('system_form_form_test_alter_form_alter() executed.'); +} + +/** * Form builder for testing drupal_validate_form(). * * Serves for testing form processing and alterations by form validation Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.158 diff -u -p -r1.158 system.api.php --- modules/system/system.api.php 27 Apr 2010 09:59:00 -0000 1.158 +++ modules/system/system.api.php 27 Apr 2010 19:19:29 -0000 @@ -748,7 +748,11 @@ function hook_page_alter(&$page) { * altering a node form, the node object retrieved at from $form['#node']. * * Note that instead of hook_form_alter(), which is called for all forms, you - * can also use hook_form_FORM_ID_alter() to alter a specific form. + * can also use hook_form_FORM_ID_alter() to alter a specific form. For each + * module (in system weight order) the general form alter hook implementation + * is invoked first, then the form ID specific alter implementation is called. + * After all module hook implementations are invoked, the hook_form_alter() + * implementations from themes are invoked in the same manner. * * @param $form * Nested array of form elements that comprise the form. @@ -757,6 +761,8 @@ function hook_page_alter(&$page) { * @param $form_id * String representing the name of the form itself. Typically this is the * name of the function that generated the form. + * + * @see hook_form_FORM_ID_alter() */ function hook_form_alter(&$form, &$form_state, $form_id) { if (isset($form['type']) && $form['type']['#value'] . '_node_settings' == $form_id) { @@ -776,15 +782,12 @@ function hook_form_alter(&$form, &$form_ * rather than implementing hook_form_alter() and checking the form ID, or * using long switch statements to alter multiple forms. * - * Note that this hook fires before hook_form_alter(). Therefore all - * implementations of hook_form_FORM_ID_alter() will run before all implementations - * of hook_form_alter(), regardless of the module order. - * * @param $form * Nested array of form elements that comprise the form. * @param $form_state * A keyed array containing the current state of the form. * + * @see hook_form_alter() * @see drupal_prepare_form() */ function hook_form_FORM_ID_alter(&$form, &$form_state) {