Index: CHANGELOG.txt ========================================================= --- CHANGELOG.txt (revision 1.327) +++ CHANGELOG.txt Sun Aug 16 12:56:02 PDT 2009 @@ -101,6 +101,12 @@ http://drupal.org/project/chameleon and http://drupal.org/project/pushbutton). * Added Stark theme to make analyzing Drupal's default HTML and CSS easier. * Added Seven theme as the default administration interface theme. + * Variable preprocessing of theme hooks prior to template rendering now goes + through two phases: a 'preprocess' phase and a new 'process' phase. See + http://api.drupal.org/api/function/theme/7 for details. + * Theme hooks implemented as functions (rather than as templates) can now + also have preprocess (and process) functions. See + http://api.drupal.org/api/function/theme/7 for details. - File handling: * Files are now first class Drupal objects with file_load(), file_save(), and file_validate() functions and corresponding hooks. Index: includes/theme.inc ========================================================= --- includes/theme.inc (revision 1.504) +++ includes/theme.inc Sun Aug 16 13:07:55 PDT 2009 @@ -365,49 +365,53 @@ } // Check for sub-directories. $result[$hook]['theme paths'][] = isset($info['path']) ? $info['path'] : $path; + } - + + // Allow variable processors for all theming hooks, whether the hook is + // implemented as a template or as a function. - foreach ($template_phases as $phase_key => $template_phase) { - // Check for existing variable processors. Ensure arrayness. - if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { - $info[$phase_key] = array(); - $prefixes = array(); - if ($type == 'module') { - // Default variable processor prefix. - $prefixes[] = 'template'; - // Add all modules so they can intervene with their own variable processors. This allows them - // to provide variable processors even if they are not the owner of the current hook. - $prefixes += module_list(); - } - elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { - // Theme engines get an extra set that come before the normally named variable processors. - $prefixes[] = $name . '_engine'; - // The theme engine registers on behalf of the theme using the theme's name. - $prefixes[] = $theme; - } - else { - // This applies when the theme manually registers their own variable processors. - $prefixes[] = $name; - } - foreach ($prefixes as $prefix) { + foreach ($template_phases as $phase_key => $template_phase) { + // Check for existing variable processors. Ensure arrayness. + if (!isset($info[$phase_key]) || !is_array($info[$phase_key])) { + $info[$phase_key] = array(); + $prefixes = array(); + if ($type == 'module') { + // Default variable processor prefix. + $prefixes[] = 'template'; + // Add all modules so they can intervene with their own variable processors. This allows them + // to provide variable processors even if they are not the owner of the current hook. + $prefixes += module_list(); + } + elseif ($type == 'theme_engine' || $type == 'base_theme_engine') { + // Theme engines get an extra set that come before the normally named variable processors. + $prefixes[] = $name . '_engine'; + // The theme engine registers on behalf of the theme using the theme's name. + $prefixes[] = $theme; + } + else { + // This applies when the theme manually registers their own variable processors. + $prefixes[] = $name; + } + foreach ($prefixes as $prefix) { - if (function_exists($prefix . '_' . $template_phase)) { + // Only use non-hook-specific variable processors for theming hooks implemented as templates. + // See comment of theme() for why. + if (isset($info['template']) && function_exists($prefix . '_' . $template_phase)) { - $info[$phase_key][] = $prefix . '_' . $template_phase; - } - if (function_exists($prefix . '_' . $template_phase . '_' . $hook)) { - $info[$phase_key][] = $prefix . '_' . $template_phase . '_' . $hook; - } - } - } - // Check for the override flag and prevent the cached variable processors from being used. - // This allows themes or theme engines to remove variable processors set earlier in the registry build. - if (!empty($info['override ' . $phase_key])) { - // Flag not needed inside the registry. - unset($result[$hook]['override ' . $phase_key]); - } - elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { - $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); - } - $result[$hook][$phase_key] = $info[$phase_key]; + $info[$phase_key][] = $prefix . '_' . $template_phase; + } + if (function_exists($prefix . '_' . $template_phase . '_' . $hook)) { + $info[$phase_key][] = $prefix . '_' . $template_phase . '_' . $hook; + } + } + } + // Check for the override flag and prevent the cached variable processors from being used. + // This allows themes or theme engines to remove variable processors set earlier in the registry build. + if (!empty($info['override ' . $phase_key])) { + // Flag not needed inside the registry. + unset($result[$hook]['override ' . $phase_key]); + } + elseif (isset($cache[$hook][$phase_key]) && is_array($cache[$hook][$phase_key])) { + $info[$phase_key] = array_merge($cache[$hook][$phase_key], $info[$phase_key]); + } + $result[$hook][$phase_key] = $info[$phase_key]; - } } } @@ -418,13 +422,15 @@ // Let themes have variable processors even if they didn't register a template. if ($type == 'theme' || $type == 'base_theme') { foreach ($cache as $hook => $info) { - // Check only if it's a template and not registered by the theme or engine. - if (!empty($info['template']) && empty($result[$hook])) { + // Check only if not registered by the theme or engine. + if (empty($result[$hook])) { foreach ($template_phases as $phase_key => $template_phase) { if (!isset($info[$phase_key])) { $cache[$hook][$phase_key] = array(); } - if (function_exists($name . '_' . $template_phase)) { + // Only use non-hook-specific variable processors for theming hooks implemented as templates. + // See comment of theme() for why. + if (isset($info['template']) && function_exists($name . '_' . $template_phase)) { $cache[$hook][$phase_key][] = $name . '_' . $template_phase; } if (function_exists($name . '_' . $template_phase . '_' . $hook)) { @@ -568,9 +574,6 @@ * registry is checked to determine which implementation to use, which may * be a function or a template. * - * If the implementation is a function, it is executed and its return value - * passed along. - * * If the implementation is a template, the arguments are converted to a * $variables array. This array is then modified by the module implementing * the hook, theme engine (if applicable) and the theme. The following @@ -664,14 +667,43 @@ * - THEME_process_HOOK(&$variables) * The same applies from the previous function, but it is called for a * specific hook. - * + * - * There are two special variables that these hooks can set: + * If the implementation is a function, only the hook-specific preprocess + * and process functions (the ones ending in _HOOK) are called from the + * above list. There are two reason why the non-hook-specific preprocess + * and process functions (the ones not ending in _HOOK) are not called for + * function-implemented theme hooks: + * + * 1. Function-implemented theme hooks need to be fast, and calling the + * non-hook-specific preprocess and process functions on them would incur + * a noticeable performance penalty. + * + * 2. Function-implemented theme hooks can only make use of variables + * declared as arguments within the hook_theme() function that registers + * the theme hook, and cannot make use of additional generic variables. + * For the most part, non-hook-specific preprocess and process functions + * add/modify variables other than the theme hook's arguments, variables + * that are potentially useful in template files, but unavailable to + * function implementations. + * + * For template-implemented theme hooks, there are two special variables that + * these preprocess and process functions can set: * 'template_file' and 'template_files'. These will be merged together * to form a list of 'suggested' alternate template files to use, in * reverse order of priority. template_file will always be a higher * priority than items in template_files. theme() will then look for these - * files, one at a time, and use the first one - * that exists. + * files, one at a time, and use the first one that exists. If none exist, + * theme() will use the original registered file for the theme hook. + * + * For function-implemented theme hooks, there are two special variables that + * these preprocess and process functions can set: + * 'theme_function' and 'theme_functions'. These will be merged together + * to form a list of 'suggested' alternate functions to use, in + * reverse order of priority. theme_function will always be a higher + * priority than items in theme_functions. theme() will then call the + * highest priority function that exists. If none of the suggestions exist, + * theme() will call the original registered function for the theme hook. + * * @param $hook * The name of the theme function to call. May be an array, in which * case the first hook that actually has an implementation registered @@ -723,18 +755,77 @@ } if (isset($info['function'])) { // The theme call is a function. - if (drupal_function_exists($info['function'])) { + - // If a theme function that does not expect a renderable array is called - // with a renderable array as the only argument (via drupal_render), then - // we take the arguments from the properties of the renderable array. If - // missing, use hook_theme() defaults. - if (isset($args[0]) && is_array($args[0]) && isset($args[0]['#theme']) && count($info['arguments']) > 1) { - $new_args = array(); - foreach ($info['arguments'] as $name => $default) { - $new_args[] = isset($args[0]["#$name"]) ? $args[0]["#$name"] : $default; - } - $args = $new_args; - } + // If a theme function that does not expect a renderable array is called + // with a renderable array as the only argument (via drupal_render), then + // we take the arguments from the properties of the renderable array. If + // missing, use hook_theme() defaults. + if (isset($args[0]) && is_array($args[0]) && isset($args[0]['#theme']) && count($info['arguments']) > 1) { + $new_args = array(); + foreach ($info['arguments'] as $name => $default) { + $new_args[] = isset($args[0]["#$name"]) ? $args[0]["#$name"] : $default; + } + $args = $new_args; + } + + // Invoke the variable processors, if any. + // We minimize the overhead for theming hooks that have no processors and are called + // many times per page request by caching '_no processors'. If we do have processors, + // then the overhead of calling them overshadows the overhead of calling empty(). + if (!isset($info['_no processors'])) { + if (!empty($info['preprocess functions']) || !empty($info['process functions'])) { + $variables = array( + 'theme_functions' => array(), + ); + if (!empty($info['arguments'])) { + $count = 0; + foreach ($info['arguments'] as $name => $default) { + $variables[$name] = isset($args[$count]) ? $args[$count] : $default; + $count++; + } + } + // We don't want a poorly behaved process function changing $hook + $hook_clone = $hook; + foreach (array('preprocess functions', 'process functions') as $template_phase) { + foreach ($info[$template_phase] as $template_function) { + if (drupal_function_exists($template_function)) { + $template_function($variables, $hook_clone); + } + } + } + if (!empty($info['arguments'])) { + $count = 0; + foreach ($info['arguments'] as $name => $default) { + $args[$count] = $variables[$name]; + $count++; + } + } + + // Get suggestions for alternate functions out of the variables + // that were set. This lets us dynamically choose a function + // from a list. The order is FILO, so this array is ordered from + // least appropriate functions to most appropriate last. + $suggestions = array(); + if (isset($variables['theme_functions'])) { + $suggestions = $variables['theme_functions']; + } + if (isset($variables['theme_function'])) { + $suggestions[] = $variables['theme_function']; + } + foreach (array_reverse($suggestions) as $suggestion) { + if (drupal_function_exists($suggestion)) { + $info['function'] = $suggestion; + break; + } + } + } + else { + $hooks[$hook]['_no processors'] = TRUE; + } + } + + // Call the function. + if (drupal_function_exists($info['function'])) { $output = call_user_func_array($info['function'], $args); } }