作者:老葛,北京亚艾元软件有限责任公司,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的写作。就是说,当别人放假,出去玩的时候,我们还在整理着这样的资料。