作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们前面,已经为Breadcrumb2实现了上下文链接,在里面也提到一个问题,就是将breadcrumb.tpl.php复制到当前主题下面,不起作用的问题。还有一些其它的问题,以及可以改善的地方。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果,我们提供了breadcrumb.tpl.php,但是却无法在当前主题下面覆写的话,这个模板文件的作用就大打折扣了。从发现这个问题以后,我就多次尝试去解决它。首先是借助于Google,有什么技术问题,先问Google。这是我在Google上使用的搜索词“hook_theme_registry_alter template file could not be override”。
很幸运,有人遇到了同样的问题http://drupal.org/node/1424048,不幸的是,没有解决办法,如果你现在浏览这个问题的话,你会发现下面已经给出了解决办法,这个解决办法就是老葛给出来的。
我找到了一个类似的:
http://www.metachunk.com/blog/adding-module-path-drupal-7-theme-registry
在这篇文章里面,作者解决一个类似的问题,和我这里解决的问题还不完全一样,但是却为我们解决问题指明了方向。它是想在自己的模块目录下面,为模板文件提供一个模板建议,比如block--module--delta.tpl.php。这篇文章在解决这个问题的时候,也是花费了很大功夫的。它是这样解决的:
/**
* Implements hook_theme_registry_alter()
**/
function mymodule_theme_registry_alter(&$theme_registry) {
$mod_path = drupal_get_path('module', 'mymodule');
$theme_registry_copy = $theme_registry; // munge on a copy
_theme_process_registry($theme_registry_copy, 'phptemplate', 'theme_engine', 'pow', $mod_path);
$theme_registry += array_diff_key($theme_registry_copy, $theme_registry);
$hooks = array('node');
foreach ($hooks as $h) {
_mymodule_insert_after_first_element($theme_registry[$h]['theme paths'], $mod_path);
}
}
/**
* Helper function for re-ordering arrays (needed by theme_registry_alter)
*/
function _mymodule_insert_after_first_element(&$a, $element) {
if(is_array($a)) {
$first_element = array_shift($a);
array_unshift($a, $first_element, $element);
}
}
在hook_theme_registry_alter调用_theme_process_registry这种方式,让我眼前一亮,我的直觉告诉我,这种方式也能够解决我们的问题。开始的时候,我并没有看到它的代码的具体含义,但是我却做了这样的一件事情,认真的阅读了_theme_process_registry这个方法,以及_theme_build_registry,这两个函数里面的代码我读了很多遍,每个函数都认真的读过3遍以上的,弄清楚了函数里面每行代码的作用。我们来看一下_theme_build_registry的代码:
function _theme_build_registry($theme, $base_theme, $theme_engine) {
$cache = array();
// First, process the theme hooks advertised by modules. This will
// serve as the basic registry. Since the list of enabled modules is the same
// regardless of the theme used, this is cached in its own entry to save
// building it for every theme.
if ($cached = cache_get('theme_registry:build:modules')) {
$cache = $cached->data;
}
else {
foreach (module_implements('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
// Only cache this registry if all modules are loaded.
if (module_load_all(NULL)) {
cache_set('theme_registry:build:modules', $cache);
}
}
// Process each base theme.
foreach ($base_theme as $base) {
// If the base theme uses a theme engine, process its hooks.
$base_path = dirname($base->filename);
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
}
_theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
}
// And then the same thing, but for the theme.
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
}
// Finally, hooks provided by the theme itself.
_theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
// Let modules alter the registry.
drupal_alter('theme_registry', $cache);
// Optimize the registry to not have empty arrays for functions.
foreach ($cache as $hook => $info) {
foreach (array('preprocess functions', 'process functions') as $phase) {
if (empty($info[$phase])) {
unset($cache[$hook][$phase]);
}
}
}
return $cache;
}
这是用来构建主题的注册表的,大致的流程如下:
而注册的具体操作,则是委托给了_theme_process_registry。走到这里的时候,我做了一个非常大胆的尝试,我修改了这里的流程,这是我修改后的流程:
这是我修改后的代码:
function _theme_build_registry($theme, $base_theme, $theme_engine) {
$cache = array();
// First, process the theme hooks advertised by modules. This will
// serve as the basic registry. Since the list of enabled modules is the same
// regardless of the theme used, this is cached in its own entry to save
// building it for every theme.
if ($cached = cache_get('theme_registry:build:modules')) {
$cache = $cached->data;
}
else {
foreach (module_implements('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
// Only cache this registry if all modules are loaded.
if (module_load_all(NULL)) {
cache_set('theme_registry:build:modules', $cache);
}
}
// Let modules alter the registry.
drupal_alter('theme_registry', $cache);
// Process each base theme.
foreach ($base_theme as $base) {
// If the base theme uses a theme engine, process its hooks.
$base_path = dirname($base->filename);
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
}
_theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
}
// And then the same thing, but for the theme.
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
}
// Finally, hooks provided by the theme itself.
_theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
// Optimize the registry to not have empty arrays for functions.
foreach ($cache as $hook => $info) {
foreach (array('preprocess functions', 'process functions') as $phase) {
if (empty($info[$phase])) {
unset($cache[$hook][$phase]);
}
}
}
return $cache;
}
然后,我清除缓存,测试,竟然可以了。我竟然通过这种方式把问题解决掉了。这个时候,我认识到了一个问题,“基主题的主题函数/模板文件注册”这个流程前面,应该放置drupal_alter('theme_registry', $cache);这段代码,或者放置一段这样的代码:
drupal_alter('theme', $cache);
为什么这么说呢? 我们知道,主题引擎、主题,都是位于模块的上层的,虽然在Drupal7里面,模块可以使用hook_theme,主题引擎、主题也可以使用hook_theme。但是由于主题本身位于最上层,所以模板是没有必要通过hook_theme_registry_alter修改主题里面提供的特有的主题函数/模板文件,如果想要修改主题里面的,只需要在主题层直接修改就是了。而主题层的_theme_process_registry,还包含了对模块层已有主题函数/模板文件的覆写功能,这也是包含了的。
在此之前,我还做过这样的调试:
function breadcrumb2_theme_registry_alter(&$theme_registry) {
//print debug($theme_registry['breadcrumb']);
if (isset($theme_registry['breadcrumb'])) {
$path = drupal_get_path('module', 'breadcrumb2');
//$path = path_to_theme();
$theme_registry['breadcrumb']['path'] = $path;
$theme_registry['breadcrumb']['theme path'] = $path;
$theme_registry['breadcrumb']['template'] = 'breadcrumb';
$theme_registry['breadcrumb']['function'] = NULL;
unset($theme_registry['breadcrumb']['function']);
}
// print debug($theme_registry['breadcrumb']);
//print debug($theme_registry['node']);
print debug($theme_registry);
}
就是直接将$theme_registry的结构在前台输出出来,然后观察里面每一元素的结构,同时重点研究了($theme_registry['node']、($theme_registry['page']、($theme_registry[' breadcrumb ']的输出。这是没有修改核心代码时的输出:
array (
'variables' =>
array (
'breadcrumb' => NULL,
),
'type' => 'module',
'theme path' => 'sites/all/modules/breadcrumb2',
'preprocess functions' =>
array (
0 => 'template_preprocess_breadcrumb',
),
'process functions' =>
array (
),
'path' => 'sites/all/modules/breadcrumb2',
'template' => 'breadcrumb',
)
Debug:
array (
'template' => 'page',
'path' => 'themes/seven',
'type' => 'theme_engine',
'theme path' => 'themes/seven',
'render element' => 'page',
'preprocess functions' =>
array (
0 => 'template_preprocess',
1 => 'template_preprocess_page',
2 => 'contextual_preprocess',
3 => 'overlay_preprocess_page',
4 => 'shortcut_preprocess_page',
5 => 'seven_preprocess_page',
),
'process functions' =>
array (
0 => 'template_process',
1 => 'template_process_page',
2 => 'rdf_process',
),
)
我们看到,这里的数组,里面包含的信息远比hook_theme里面包含的多,而对于$theme_registry['page'],从它里面的结构可以看出,已经被当前主题seven给覆写了。
好吧,关于breadcrumb.tpl.php在当前主题下面的覆写,我们先介绍到这里。我们已经找到了一个解决办法。现在我们来看另外一个问题,我们知道page.tpl.php它是有模板建议的,而我们的breadcrumb.tpl.php现在还没有这个功能,现在就为它添加模板建议。
我们可以直接到template_preprocess_page里面复制相关的代码即可:
function template_preprocess_page(&$variables) {
….
// Populate the page template suggestions.
if ($suggestions = theme_get_suggestions(arg(), 'page')) {
$variables['theme_hook_suggestions'] = $suggestions;
}
}
这是我依葫芦画瓢,复制过来的样子:
/**
* Implements MODULE_preprocess_HOOK().
*/
function template_preprocess_breadcrumb(&$variables) {
……
// Populate the breadcrumb template suggestions.
if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb')) {
$variables['theme_hook_suggestions'] = $suggestions;
}
}
我开始的时候,复制粘贴修改的时候,做成了这个样子; ' breadcrumb',结果出不来结果,后来调试,发现多了一个空格。复制粘贴的时候,经常会遇到这样的小问题。
如果,要想为模板设置模板建议的话,我们需要设置$variables['theme_hook_suggestions']这个变量。我们这里调用的是theme_get_suggestions,它能够帮助我们返回一个有关模板建议的数组。在template_preprocess_html里面,也是用的这个函数。这个函数的完整结构;
theme_get_suggestions($args, $base, $delimiter = '__')
它包含三个参数,$args,$base,$delimiter,我们这里只用到了前两个。我们看一下,三个预处理函数里面的用法:
theme_get_suggestions(arg(), 'breadcrumb')
theme_get_suggestions(arg(), 'page')
theme_get_suggestions(arg(), 'html')
这个时候,我们就能很好的理解$base的含义了,就是基模板文件的名字。arg()返回的是url里面的参数的数组。
现在,我们在bartik\templates下面,创建一个breadcrumb--user.tpl.php的模板文件,里面加点调试信息,来证明是这个模板文件起作用。清除缓存,访问“user”页面,此时调用的就是我们的模板建议breadcrumb--user.tpl.php了。
我们这里的模板建议规则,和page.tpl.php的保持一致,这是因为,breadcrumb.tpl.php也是基于路径的,这和页面模板文件一样。在block.tpl.php的预处理函数中,模板建议是这样定义的:
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module . '__' . strtr($variables['block']->delta, '-', '_');
它是逐个定义模板建议规则的。
我们前面忘记介绍了一点,就是什么时候改用template_preprocess_breadcrumb了,以前我们都是用的breadcrumb2_preprocess_breadcrumb,在调试breadcrumb.tpl.php无法在当前主题下面覆写的时候,我就把它改过来了。
无论是使用breadcrumb2_preprocess_breadcrumb,还是使用template_preprocess_breadcrumb,在我们这里,作用都是一样的。为什么这里要把breadcrumb2这个前缀,改为template呢?
我是这样想的,在node.module文件里面,他们没有使用node_preprocess_node,而是使用的template_preprocess_node;同样,在block.module文件里面,用的是template_preprocess_block。Drupal核心是这样用的,所以我这里遵守了Drupal核心的习惯。
现在,我思考这个问题的时候,我又有了新的发现,如果我们这里面,使用breadcrumb2_preprocess_breadcrumb的话,如果存在这样的模块,比如abc,它也实现这个预处理函数abc_preprocess_breadcrumb,这个函数就会放到breadcrumb2_preprocess_breadcrumb前面执行。在breadcrumb2.module文件中,我们是为breadcrumb.tpl.php模板文件提供一些默认变量的,所以这里使用template打头,更方便其它模块的覆写。
我们这里介绍一下,预处理/处理函数的执行顺序。这里以breadcrumb.tpl.php为例:
(1) template_preprocess(&$variables, $hook): 这是系统默认的预处理函数。
(2)template_preprocess_breadcrumb(&$variables): 由breadcrumb2.moudle实现,为breadcrumb.tpl.php提供默认的变量。
(3)MODULE_preprocess(&$variables, $hook):调用所有模块上的 hook_preprocess()实现。
(4) MODULE_preprocess_breadcrumb(&$variables): 调用hook_preprocess_breadcrumb() ,这样允许其它模块可以修改breadcrumb.tpl.php的默认变量。
(5) ENGINE_engine_preprocess(&$variables, $hook): 允许主题引擎为所有的主题钩子设置变量。
(6)ENGINE_engine_preprocess_breadcrumb(&$variables): 允许主题引擎为breadcrumb设置变量。
(7)THEME_preprocess(&$variables, $hook): 允许主题为所有的主题钩子设置变量。
(8)THEME_preprocess_breadcrumb(&$variables): 允许主题为breadcrumb设置变量
(9)template_process(&$variables, $hook):为所有的主题钩子添加一些附加变量。
(10)template_process_breadcrumb(&$variables): 我们这里没有实现这个函数
(11)MODULE_process(&$variables, $hook):触发钩子hook_process()
(12)MODULE_process_breadcrumb(&$variables):触发钩子 hook_process_HOOK() ,允许第三方模块为breadcrumb添加附加变量。
(13)ENGINE_engine_process(&$variables, $hook): 允许主题引擎为所有主题钩子添加附加变量
(14) ENGINE_engine_process_breadcrumb(&$variables): 允许主题引擎为breadcrumb添加附加变量.
(15) THEME_process(&$variables, $hook): 允许主题为所有主题钩子添加附加变量
(16) THEME_process_breadcrumb(&$variables): 允许主题为breadcrumb添加附加变量
从这个执行顺序上面来看,由于breadcrumb2模块提供了breadcrumb.tpl.php,所以我们需要为breadcrumb.tpl.php提供默认变量。template_preprocess_breadcrumb在这里也就更合适一点。
我们这里列的预处理函数比较多,实际上系统会把要执行的预处理函数、处理函数,缓存到主题注册表里面,这样在执行的时候,就不用去查找有多少个预处理函数需要执行了。
如果,我们没有把theme_breadcrumb改造为模板文件的形式,此时的执行预处理函数,只有下面的四个函数,可被调用:
THEME_preprocess(&$variables, $hook): 允许主题为所有的主题钩子设置变量。
THEME_preprocess_breadcrumb(&$variables): 允许主题为breadcrumb设置变量
THEME_process(&$variables, $hook): 允许主题为所有主题钩子添加附加变量
THEME_process_breadcrumb(&$variables): 允许主题为breadcrumb添加附加变量
这样的好处就是,速度快,不需要调用太多的预处理函数。主题函数与模板文件相比,一个很大的优势,就是主题函数的速度快,大约快10倍左右。模板文件的优势时,方便不懂PHP的美工修改这个文件,开发时比较方便。
我们在阅读Drupal核心代码的时候,可以经常碰到这个函数drupal_static,它的作用是,能够将一个变量缓存起来,在同一HTTP请求期间,重复调用这个变量,不会计算第二次。就是说,如果有这么一个变量,它在一个HTTP请求内,可以被调用多次的话,我们可以只计算一次,然后使用drupal_static将它缓存起来。
我想为breadcrumb2_load_by_path函数,使用drupal_static。这是没有使用drupal_static的代码:
function breadcrumb2_load_by_path($path) {
$breadcrumbs = breadcrumb2_load_multiple(FALSE, array('path' => $path));
return reset($breadcrumbs);
}
这是使用后的代码:
function breadcrumb2_load_by_path($path) {
$cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$path])) {
$breadcrumbs = breadcrumb2_load_multiple(FALSE, array('path' => $path));
$cache[$path] = reset($breadcrumbs);
return $cache[$path];
}
return $cache[$path];
}
我对这个函数,开始的时候,最不理解的地方就是__FUNCTION__,后来才弄明白,这是一个PHP常量,上面的代码等价于:
$cache = &drupal_static('breadcrumb2_load_by_path', array());
类似的实现,可以参看Profile2模块里面的profile2_load_by_user,它的这个要复杂很多:
function profile2_load_by_user($account, $type_name = NULL) {
// Use a separate query to determine all profile ids per user and cache them.
// That way we can look up profiles by id and benefit from the static cache
// of the entity loader.
$cache = &drupal_static(__FUNCTION__, array());
$uid = is_object($account) ? $account->uid : $account;
if (!isset($cache[$uid])) {
if (empty($type_name)) {
$profiles = profile2_load_multiple(FALSE, array('uid' => $uid));
// Cache ids for further lookups.
$cache[$uid] = array();
foreach ($profiles as $pid => $profile) {
$cache[$uid][$profile->type] = $pid;
}
return $profiles ? array_combine(array_keys($cache[$uid]), $profiles) : array();
}
$cache[$uid] = db_select('profile', 'p')
->fields('p', array('type', 'pid'))
->condition('uid', $uid)
->execute()
->fetchAllKeyed();
}
if (isset($type_name)) {
return isset($cache[$uid][$type_name]) ? profile2_load($cache[$uid][$type_name]) : FALSE;
}
// Return an array containing profiles keyed by profile type.
return $cache[$uid] ? array_combine(array_keys($cache[$uid]), profile2_load_multiple($cache[$uid])) : $cache[$uid];
}
其实这里的逻辑很简单,检查静态变量里面,是否已经存在,如果存在,直接返回;如果不存在,则重新生成一遍。我们这里向函数传递了参数,如果没有传递参数,代码结构会更加简单:
function mymodule_function() {
$cache = &drupal_static(__FUNCTION__);
if (!isset($cache)) {
// 如果没有设置,就计算一遍,为$cache赋值。
}
return $ cache;
}
前面,我们讲了如何使用drupal_static,新的问题出来了,如果在一个HTTP内,如果我们更新的对应的面包屑对象,更新了之后,再去调用breadcrumb2_load_by_path函数,有可能返回的还是最初的面包屑对象。所以我们有必要在保存面包屑对象的时候,也修改一下这个静态缓存。
class Breadcrumb extends Entity {
….
public function save() {
if (empty($this->bid) && (!empty($this->path))) {
$existing_breadcrumb = breadcrumb2_load_by_path($this->path);
if(!empty($existing_breadcrumb)){
$this->bid = $existing_breadcrumb->bid;
$this->is_new = FALSE;
}
}
parent::save();
// Update the static cache from breadcrumb2_load_by_path().
$cache = &drupal_static('breadcrumb2_load_by_path', array());
if (isset($cache[$this->path])) {
$cache[$this->path] = $this;
}
}
}
这是对面包屑对象保存,改进后的代码,就是我们在保存一个面包屑对象后,对对应的静态缓存也做了更新。
有兴趣的读者,可以在breadcrumb2_load_by_path里面加上几句drupal_set_message这样的调试代码,观察一下,同一个HTTP请求内,IF语句里面的代码被调用了多少次。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在前面通过修改Drupal核心代码,实现了在主题层也能覆写breadcrumb.tpl.php这个模版文件。在实现这个功能以后,我就想到了,其实不修改核心代码,也能够在我们的模块中实现这个功能。就是说,我们把_theme_build_registry里面的基主题、主题引擎、主题的_theme_process_registry的调用,在breadcrumb2_theme_registry_alter里面,重新调用执行一遍就可以了。
开始的时候,我是这样写的代码:
function breadcrumb2_theme_registry_alter(&$theme_registry) {
if (isset($theme_registry['breadcrumb'])) {
$path = drupal_get_path('module', 'breadcrumb2');
//$path = path_to_theme();
$theme_registry['breadcrumb']['path'] = $path;
$theme_registry['breadcrumb']['theme path'] = $path;
$theme_registry['breadcrumb']['template'] = 'breadcrumb';
$theme_registry['breadcrumb']['function'] = NULL;
unset($theme_registry['breadcrumb']['function']);
}
$themes = list_themes();
foreach ($themes as $theme) {
_theme_process_registry($theme_registry, $theme->name, 'theme', $theme->name, dirname($theme->filename));
//print debug($theme);
}
}
我当时这样想的,把所有启用的主题,分别重新注册一遍,这样它们在注册的时候,就可以覆写我们的模版文件了。就是把所有主题的_theme_process_registry,放到后面,重新执行一遍。
但是很遗憾,还是不行。当时,我就有了一个疑问,什么疑问呢?就是说,在_theme_build_registry里面,这句话是这样写的:
// Finally, hooks provided by the theme itself.
_theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
这里面,没有把所有的主题都列出来,分别处理一下,而是处理了一个。我当时特别想知道,这个$theme是从哪里来的,我能不能在breadcrumb2_theme_registry_alter里面直接调用这个变量?很遗憾,在breadcrumb2_theme_registry_alter,是没有将这个变量传递过来的。这个时候,对代码,进行了分析。这里的$theme是从函数的参数里面传递过来的:
function _theme_build_registry($theme, $base_theme, $theme_engine)
都有哪些函数,调用了函数_theme_build_registry,我通过搜索查找,这个函数的调用,只出现在了_theme_load_registry函数里面:
function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) {
if ($complete) {
// Check the theme registry cache; if it exists, use it.
$cached = cache_get("theme_registry:$theme->name");
if (isset($cached->data)) {
$registry = $cached->data;
}
else {
// If not, build one and cache it.
$registry = _theme_build_registry($theme, $base_theme, $theme_engine);
// Only persist this registry if all modules are loaded. This assures a
// complete set of theme hooks.
if (module_load_all(NULL)) {
_theme_save_registry($theme, $registry);
}
}
return $registry;
}
else {
return new ThemeRegistry('theme_registry:runtime:' . $theme->name, 'cache');
}
}
而这个$theme,也来自于_theme_load_registry函数本身的参数传递。通过搜索查找,我在theme.inc文件里面,找到两个地方,包含这个函数:
function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') {
还有一处是:
function theme($hook, $variables = array()) {
// If called before all modules are loaded, we do not necessarily have a full
// theme registry to work with, and therefore cannot process the theme
// request properly. See also _theme_load_registry().
if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {
throw new Exception(t('theme() may not be called until all modules are loaded.'));
}
通过阅读_drupal_theme_initialize,我们知道这个函数最终调用了_theme_load_registry,而$theme也是从这个函数的参数里面传递过来的。
function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') {
global $theme_info, $base_theme_info, $theme_engine, $theme_path;
$theme_info = $theme;
$base_theme_info = $base_theme;
$theme_path = dirname($theme->filename);
……
if (isset($registry_callback)) {
_theme_registry_callback($registry_callback, array($theme, $base_theme, $theme_engine));
}
}
而_drupal_theme_initialize里面的$theme,则是从drupal_theme_initialize传递过来的 。drupal_theme_initialize函数本身没有参数,
function drupal_theme_initialize() {
global $theme, $user, $theme_key;
// If $theme is already set, assume the others are set, too, and do nothing
if (isset($theme)) {
return;
}
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
$themes = list_themes();
// Only select the user selected theme if it is available in the
// list of themes that can be accessed.
$theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'bartik');
// Allow modules to override the theme. Validation has already been performed
// inside menu_get_custom_theme(), so we do not need to check it again here.
$custom_theme = menu_get_custom_theme();
$theme = !empty($custom_theme) ? $custom_theme : $theme;
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
// Find all our ancestor themes and put them in an array.
$base_theme = array();
$ancestor = $theme;
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
$ancestor = $themes[$ancestor]->base_theme;
$base_theme[] = $themes[$ancestor];
}
_drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
// Themes can have alter functions, so reset the drupal_alter() cache.
drupal_static_reset('drupal_alter');
// Provide the page with information about the theme that's used, so that a
// later Ajax request can be rendered using the same theme.
// @see ajax_base_page_theme()
$setting['ajaxPageState'] = array(
'theme' => $theme_key,
'theme_token' => drupal_get_token($theme_key),
);
drupal_add_js($setting, 'setting');
}
这里面,我们的$theme来自于$themes[$theme],而global $theme返回的则是当前主题的机读名字。
而drupal_theme_initialize,主要是在theme_get_registry函数中被调用的;而theme_get_registry,则主要是在theme()里面调用:
function theme($hook, $variables = array()) {
// If called before all modules are loaded, we do not necessarily have a full
// theme registry to work with, and therefore cannot process the theme
// request properly. See also _theme_load_registry().
if (!module_load_all(NULL) && !defined('MAINTENANCE_MODE')) {
throw new Exception(t('theme() may not be called until all modules are loaded.'));
}
$hooks = theme_get_registry(FALSE);
….
我们画个流程图:
弄明白这个结构,不容易啊。但是,我还没有完全不明白,一个Drupal站点里面,包含多个Drupal主题,每个主题的注册表是怎么注册的呢?
我们都知道,点击性能页面的清除缓存按钮,就可以清除所有的缓存。那个时候,肯定也会清除主题注册表的缓存。我们平时,添加一个新模版文件的时候,总是要清除缓存才会起作用的。我们从这个路径入手,根据admin/config/development/performance,从system.module里面,找到system_menu这个钩子函数。我们找到对应菜单项的定义:
$items['admin/config/development/performance'] = array(
'title' => 'Performance',
'description' => 'Enable or disable page caching for anonymous users and set CSS and JS bandwidth optimization options.',
'page callback' => 'drupal_get_form',
'page arguments' => array('system_performance_settings'),
'access arguments' => array('administer site configuration'),
'file' => 'system.admin.inc',
'weight' => -20,
);
根据这个信息,在'system.admin.inc'文件中找到'system_performance_settings'。
function system_performance_settings() {
drupal_add_js(drupal_get_path('module', 'system') . '/system.js');
$form['clear_cache'] = array(
'#type' => 'fieldset',
'#title' => t('Clear cache'),
);
$form['clear_cache']['clear'] = array(
'#type' => 'submit',
'#value' => t('Clear all caches'),
'#submit' => array('system_clear_cache_submit'),
);
……
return system_settings_form($form);
}
我们找到了对应的提交处理函数system_clear_cache_submit,然后查看这个提交函数的源代码:
function system_clear_cache_submit($form, &$form_state) {
drupal_flush_all_caches();
drupal_set_message(t('Caches cleared.'));
}
代码很简单,主要是drupal_flush_all_caches函数的调用。我们在当前页面搜索这个函数,没有找到它的定义。通过Google搜索一下,这个Drupal API函数。这个函数定义在includes/common.inc里面:
function drupal_flush_all_caches() {
// Change query-strings on css/js files to enforce reload for all users.
_drupal_flush_css_js();
registry_rebuild();
drupal_clear_css_cache();
drupal_clear_js_cache();
// Rebuild the theme data. Note that the module data is rebuilt above, as
// part of registry_rebuild().
system_rebuild_theme_data();
drupal_theme_rebuild();
entity_info_cache_clear();
node_types_rebuild();
// node_menu() defines menu items based on node types so it needs to come
// after node types are rebuilt.
menu_rebuild();
// Synchronize to catch any actions that were added or removed.
actions_synchronize();
// Don't clear cache_form - in-progress form submissions may break.
// Ordered so clearing the page cache will always be the last action.
$core = array('cache', 'cache_path', 'cache_filter', 'cache_bootstrap', 'cache_page');
$cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
// Rebuild the bootstrap module list. We do this here so that developers
// can get new hook_boot() implementations registered without having to
// write a hook_update_N() function.
_system_update_bootstrap_status();
}
这段代码,建议大家认真的读两遍,这里涉及到CSS、JS、主题、Entity、菜单,以及Drupal核心缓存表中缓存的清除工作。而我们这里关心的是:
system_rebuild_theme_data();
drupal_theme_rebuild();
这里面的system_rebuild_theme_data,是负责构建主题的info文件里面的数据的,有兴趣的可以继续沿着这个方向阅读对应的源代码。我们这里略过这个函数。drupal_theme_rebuild是用来重新构建Drupal的主题注册表的。我们来看这个函数,这个函数位于includes/theme.inc中:
function drupal_theme_rebuild() {
drupal_static_reset('theme_get_registry');
cache_clear_all('theme_registry', 'cache', TRUE);
}
我们看到了什么?这里清除了theme_get_registry以及theme_registry的所有缓存数据。但是这里面并没有重建这些数据。这个时候,我开始快明白了。
这个时候,我又做了一个这样的试验,就是在breadcrumb2_theme_registry_alter里面加上这样的调试信息
function breadcrumb2_theme_registry_alter(&$theme_registry) {
print debug($theme_registry['page']);
….
}
然后清除缓存。我得到了这样的调试信息:
Debug:
array (
'template' => 'page',
'path' => 'themes/seven',
'type' => 'theme_engine',
'theme path' => 'themes/seven',
'render element' => 'page',
'preprocess functions' =>
array (
0 => 'template_preprocess',
1 => 'template_preprocess_page',
2 => 'contextual_preprocess',
3 => 'overlay_preprocess_page',
4 => 'shortcut_preprocess_page',
5 => 'seven_preprocess_page',
),
'process functions' =>
array (
0 => 'template_process',
1 => 'template_process_page',
2 => 'rdf_process',
),
)
接着,我访问首页,我看到了新的调试信息:
array (
'template' => 'page',
'path' => 'themes/bartik/templates',
'type' => 'theme_engine',
'theme path' => 'themes/bartik',
'render element' => 'page',
'preprocess functions' =>
array (
0 => 'template_preprocess',
1 => 'template_preprocess_page',
2 => 'contextual_preprocess',
3 => 'overlay_preprocess_page',
4 => 'shortcut_preprocess_page',
),
'process functions' =>
array (
0 => 'template_process',
1 => 'template_process_page',
2 => 'rdf_process',
3 => 'bartik_process_page',
),
)
这个时候,我明白了一个道理,Drupal主题的注册表缓存数据,是在第一次需要的时候,才生成这些数据的。我们这里用到了Seven和bartik两个主题,对于其它Drupal主题,比如Garland,假如我们启用了它,但是没有页面使用这个主题的话,就不会为它缓存主题注册表数据。而不是我刚开始猜测的,为所有启用的主题,在清除缓存的时候,就重新生成一遍主题注册表里面的数据。
这一点和ImageCache(Image style),Boost模块的机制是一样的。归纳一下,就是说,当需要这个东西的时候,首先检查缓存,看是否存在,如果存在就使用缓存数据,如果不存在,系统就会重新生成一遍,并把数据缓存起来。在需要的时候,才会去缓存。不会去缓存用不到的数据。
至此,我通过对Drupal核心代码的研读,终于明白了主题机制背后的原理。这是改进后的代码:
function breadcrumb2_theme_registry_alter(&$theme_registry) {
print debug($theme_registry['page']);
if (isset($theme_registry['breadcrumb'])) {
$path = drupal_get_path('module', 'breadcrumb2');
//$path = path_to_theme();
$theme_registry['breadcrumb']['path'] = $path;
$theme_registry['breadcrumb']['theme path'] = $path;
$theme_registry['breadcrumb']['template'] = 'breadcrumb';
$theme_registry['breadcrumb']['function'] = NULL;
unset($theme_registry['breadcrumb']['function']);
}
global $theme;
$themes = list_themes();
$theme_obj = $themes[$theme];
//print debug($themes[$theme]);
_theme_process_registry($theme_registry, $theme_obj->name, 'theme', $theme_obj->name, dirname($theme_obj->filename));
}
清除缓存,测试,仍然没有起作用。我们最后一次修改核心代码:
function _theme_build_registry($theme, $base_theme, $theme_engine) {
$cache = array();
// First, process the theme hooks advertised by modules. This will
// serve as the basic registry. Since the list of enabled modules is the same
// regardless of the theme used, this is cached in its own entry to save
// building it for every theme.
if ($cached = cache_get('theme_registry:build:modules')) {
$cache = $cached->data;
}
else {
foreach (module_implements('theme') as $module) {
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
// Only cache this registry if all modules are loaded.
if (module_load_all(NULL)) {
cache_set('theme_registry:build:modules', $cache);
}
}
// Let modules alter the registry.
drupal_alter('theme_registry', $cache);
print debug($cache['breadcrumb']);
// Process each base theme.
foreach ($base_theme as $base) {
// If the base theme uses a theme engine, process its hooks.
$base_path = dirname($base->filename);
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
}
_theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
}
print debug($cache['breadcrumb']);
// And then the same thing, but for the theme.
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
}
print debug($cache['breadcrumb']);
// Finally, hooks provided by the theme itself.
_theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
print debug($cache['breadcrumb']);
// Let modules alter the registry.
//drupal_alter('theme_registry', $cache);
// Optimize the registry to not have empty arrays for functions.
foreach ($cache as $hook => $info) {
foreach (array('preprocess functions', 'process functions') as $phase) {
if (empty($info[$phase])) {
unset($cache[$hook][$phase]);
}
}
}
return $cache;
}
这里面,我加入了四个print debug($cache['breadcrumb']);,我想看看是在哪个阶段覆写的。清除缓存,然后再访问首页,我们看到这样的调试信息:
array (
'variables' =>
array (
'breadcrumb' => NULL,
),
'type' => 'module',
'theme path' => 'sites/all/modules/breadcrumb2',
'preprocess functions' =>
array (
0 => 'template_preprocess_breadcrumb',
),
'process functions' =>
array (
),
'path' => 'sites/all/modules/breadcrumb2',
'template' => 'breadcrumb',
)in _theme_build_registry() (line 695 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc).
Debug: array (
'variables' =>
array (
'breadcrumb' => NULL,
),
'type' => 'module',
'theme path' => 'sites/all/modules/breadcrumb2',
'preprocess functions' =>
array (
0 => 'template_preprocess_breadcrumb',
),
'process functions' =>
array (
),
'path' => 'sites/all/modules/breadcrumb2',
'template' => 'breadcrumb',
)in _theme_build_registry() (line 705 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc).
Debug: array (
'template' => 'breadcrumb',
'path' => 'themes/bartik/templates',
'type' => 'theme_engine',
'theme path' => 'themes/bartik',
'variables' =>
array (
'breadcrumb' => NULL,
),
'preprocess functions' =>
array (
0 => 'template_preprocess_breadcrumb',
),
'process functions' =>
array (
),
)in _theme_build_registry() (line 710 of D:\xampp\htdocs\breadcrumb2\includes\theme.inc).
Debug: array (
'template' => 'breadcrumb',
'path' => 'themes/bartik/templates',
'type' => 'theme_engine',
'theme path' => 'themes/bartik',
'variables' =>
array (
'breadcrumb' => NULL,
),
'preprocess functions' =>
array (
0 => 'template_preprocess_breadcrumb',
),
'process functions' =>
array (
),
)
从调试信息里面可以看出,是在第3个print debug前面的一个阶段,完成了覆写工作。所以我们的代码应该这样写:
function breadcrumb2_theme_registry_alter(&$theme_registry) {
//print debug($theme_registry['page']);
if (isset($theme_registry['breadcrumb'])) {
$path = drupal_get_path('module', 'breadcrumb2');
//$path = path_to_theme();
$theme_registry['breadcrumb']['path'] = $path;
$theme_registry['breadcrumb']['theme path'] = $path;
$theme_registry['breadcrumb']['template'] = 'breadcrumb';
$theme_registry['breadcrumb']['function'] = NULL;
unset($theme_registry['breadcrumb']['function']);
}
global $theme;
$themes = list_themes();
$theme_obj = $themes[$theme];
//print debug($themes[$theme]);
_theme_process_registry($theme_registry, 'phptemplate', 'theme_engine', $theme_obj->name, dirname($theme_obj->filename));
//_theme_process_registry($theme_registry, $theme_obj->name, 'theme', $theme_obj->name, dirname($theme_obj->filename));
}
我们把主题引擎的搬了过来。由于Drupal7下面,只有一个phptemplate主题引擎,所以这里直接写死在里面了。然后我们把theme.inc文件里面,修改过的代码,改回原状。恢复到Drupal核心最初的样子即可。
清除缓存,测试,成功了。其实我们这里面,只加了四行代码,但是为了加上这四行代码,我们对Drupal核心的主题机制,做了非常认真的分析,彻底搞明白了背后的基本原理。在实际的开发中,我从来不鼓励别人修改Drupal核心的代码,但是在练习当中,在学习实践当中,我总是随心所欲的在Drupal核心代码里面加上print debug,以追踪Drupal核心的执行流程。我这里介绍的是我的土办法,如果你有更好的工具,也可以使用你集成环境里面的代码逐行调试功能。
此外,我们的Think in Drupal第4集也就写到这里了。加上以前所写的Field validation的历程,Drupal核心模板文件里面变量的含义说明,还有Drupal开发最佳实践什么的。
本来还想写JavaScript、单元测试、安装包profile的开发,考虑到Javascript本身的变化不大,而且我们阅读了contextula模块的代码,里面有相关的JS可供参考;单元测试、安装包profile的开发在中国,没有公司用的,所以就将这些知识点的介绍推后了。下面就是把所有的这段时间编写的,合并到一起,打印出来,然后,自己认真的读一遍,修正里面的文字错误,修正完以后的版本,自己拿去打印店打印,开始taobao上面出售了。就这样的流程。
在2012年12月31号的17点24分,完成了Think in Drupal的写作。就是说,当别人放假,出去玩的时候,我们还在整理着这样的资料。
我们其实在前面已经讲过了,如何新增一个变量。不过我们在Drupal核心的模板文件里面,经常看到这样的代码:
<div class="<?php print $classes; ?>">
而在我们现在的模板文件中,则是这样写的:
<div class="breadcrumb-wrapper contextual-links-region">
这里的class是写死在里面的,我们也想把它改造为变量的形式,好的,我们修改预处理函数里面的代码,这是修改后的样子:
function template_preprocess_breadcrumb(&$variables) {
$variables['classes_array'][] = 'breadcrumb-wrapper';
// Populate the breadcrumb classes.
if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb', '-')) {
foreach ($suggestions as $suggestion) {
if ($suggestion != 'breadcrumb-front') {
// Add current suggestion to breadcrumb classes to make it possible to theme
// the breadcrumb depending on the current page type (e.g. node, admin, user,
// etc.) as well as more specific data like node-12 or node-edit.
$variables['classes_array'][] = drupal_html_class($suggestion);
}
}
}
$current_path = current_path();
$breadcrumb2 = breadcrumb2_load_by_path($current_path);
if(!empty($breadcrumb2)){
$breadcrumbs = array();
// Only keep the first one.
if (!empty($variables['breadcrumb'][0])) {
$breadcrumbs[] = $variables['breadcrumb'][0];
}
$wrapper = entity_metadata_wrapper('breadcrumb2', $breadcrumb2);
$breadcrumb_links = $wrapper->link->value();
foreach($breadcrumb_links as $breadcrumb_link){
$breadcrumbs[]= l($breadcrumb_link['title'], $breadcrumb_link['url']);
}
$variables['breadcrumb'] = $breadcrumbs;
}
if (user_access('administer breadcrumbs') && user_access('access contextual links')) {
$contextual_links_element = array(
'#contextual_links' => array(
'breadcrumb2' => array('breadcrumb', array('0')),
)
);
$contextual_links = array(
'#type' => 'contextual_links',
'#contextual_links' => $contextual_links_element['#contextual_links'],
'#element' => $contextual_links_element,
);
$variables['contextual_links'] = drupal_render($contextual_links);
$variables['classes_array'][] = 'contextual-links-region';
}
// Populate the breadcrumb template suggestions.
if ($suggestions = theme_get_suggestions(arg(), 'breadcrumb')) {
$variables['theme_hook_suggestions'] = $suggestions;
}
// Flatten out classes.
$variables['classes'] = implode(' ', $variables['classes_array']);
}
开始的时候,我没有添加$variables['classes']相关的代码,我是这样想的,系统会调用template_process函数,而在这个函数中,会增加附加的变量classes,不过我想错了。默认处理函数的代码很简单:
function template_process(&$variables, $hook) {
// Flatten out classes.
$variables['classes'] = implode(' ', $variables['classes_array']);
// Flatten out attributes, title_attributes, and content_attributes.
// Because this function can be called very often, and often with empty
// attributes, optimize performance by only calling drupal_attributes() if
// necessary.
$variables['attributes'] = $variables['attributes_array'] ? drupal_attributes($variables['attributes_array']) : '';
$variables['title_attributes'] = $variables['title_attributes_array'] ? drupal_attributes($variables['title_attributes_array']) : '';
$variables['content_attributes'] = $variables['content_attributes_array'] ? drupal_attributes($variables['content_attributes_array']) : '';
}
这里面为我们提供了变量$variables['classes']。但是实际上对于breadcrumb.tpl.php,却没有调用这个处理函数。所以我将这里的代码直接复制到了预处理函数中了。
注意,这里面$variables['classes_array']是一个数组,$variables['classes']是一个字符串。为了生成classes_array这个数组,我们还使用了drupal_html_class这个函数,这些都是从Drupal核心模板的预处理函数中复制过来的。
这是现在的模板文件:
<?php if (!empty($breadcrumb)): ?>
<div class="<?php print $classes; ?>">
<h2 class="element-invisible"> <?php print t('You are here'); ?> </h2>
<?php if (!empty($contextual_links)): ?>
<div style='height:10px'> <?php print $contextual_links; ?> </div>
<?php endif; ?>
<?php //print drupal_render($contextual_link); ?>
<div class="breadcrumb"> <?php print implode(' » ', $breadcrumb); ?> </div>
</div>
<?php endif; ?>