第5章 Drupal主题系统探索

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们前面,已经为Breadcrumb2实现了上下文链接,在里面也提到一个问题,就是将breadcrumb.tpl.php复制到当前主题下面,不起作用的问题。还有一些其它的问题,以及可以改善的地方。


Drupal版本:

1 无法在当前主题下面覆写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;

}

这是用来构建主题的注册表的,大致的流程如下:

图片1.png

 

    而注册的具体操作,则是委托给了_theme_process_registry。走到这里的时候,我做了一个非常大胆的尝试,我修改了这里的流程,这是我修改后的流程:

图片2.png

        这是我修改后的代码:

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给覆写了。


Drupal版本:

2 添加模板建议

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

好吧,关于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, '-', '_');

   它是逐个定义模板建议规则的。


Drupal版本:

3 template_preprocess_breadcrumb?

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

    我们前面忘记介绍了一点,就是什么时候改用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_blockDrupal核心是这样用的,所以我这里遵守了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)这是系统默认的预处理函数。

2template_preprocess_breadcrumb(&$variables)breadcrumb2.moudle实现,为breadcrumb.tpl.php提供默认的变量。

3MODULE_preprocess(&$variables, $hook):调用所有模块上的 hook_preprocess()实现。

4) MODULE_preprocess_breadcrumb(&$variables)调用hook_preprocess_breadcrumb() ,这样允许其它模块可以修改breadcrumb.tpl.php的默认变量。

5) ENGINE_engine_preprocess(&$variables, $hook)允许主题引擎为所有的主题钩子设置变量。

6ENGINE_engine_preprocess_breadcrumb(&$variables)允许主题引擎为breadcrumb设置变量。

7THEME_preprocess(&$variables, $hook)允许主题为所有的主题钩子设置变量。

8THEME_preprocess_breadcrumb(&$variables)允许主题为breadcrumb设置变量

9template_process(&$variables, $hook):为所有的主题钩子添加一些附加变量。

10template_process_breadcrumb(&$variables)我们这里没有实现这个函数

11MODULE_process(&$variables, $hook):触发钩子hook_process() 

12MODULE_process_breadcrumb(&$variables):触发钩子 hook_process_HOOK() ,允许第三方模块为breadcrumb添加附加变量。

13ENGINE_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版本:

5 drupal_static

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们在阅读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版本:

6 在保存面包屑时,更新对应的静态缓存

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

前面,我们讲了如何使用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语句里面的代码被调用了多少次。


Drupal版本:

7 不修改Drupal核心代码

作者:老葛,北京亚艾元软件有限责任公司,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);

….

我们画个流程图:

图片1.png


                    

弄明白这个结构,不容易啊。但是,我还没有完全不明白,一个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();

}

这段代码,建议大家认真的读两遍,这里涉及到CSSJS、主题、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主题的注册表缓存数据,是在第一次需要的时候,才生成这些数据的。我们这里用到了Sevenbartik两个主题,对于其它Drupal主题,比如Garland,假如我们启用了它,但是没有页面使用这个主题的话,就不会为它缓存主题注册表数据。而不是我刚开始猜测的,为所有启用的主题,在清除缓存的时候,就重新生成一遍主题注册表里面的数据。

这一点和ImageCacheImage 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 (

  ),

)

    从调试信息里面可以看出,是在第3print 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 Drupal4集也就写到这里了。加上以前所写的Field validation的历程,Drupal核心模板文件里面变量的含义说明,还有Drupal开发最佳实践什么的。

本来还想写JavaScript、单元测试、安装包profile的开发,考虑到Javascript本身的变化不大,而且我们阅读了contextula模块的代码,里面有相关的JS可供参考;单元测试、安装包profile的开发在中国,没有公司用的,所以就将这些知识点的介绍推后了。下面就是把所有的这段时间编写的,合并到一起,打印出来,然后,自己认真的读一遍,修正里面的文字错误,修正完以后的版本,自己拿去打印店打印,开始taobao上面出售了。就这样的流程。

20121231号的1724分,完成了Think in Drupal的写作。就是说,当别人放假,出去玩的时候,我们还在整理着这样的资料。

 



Drupal版本:

4为模板新增变量$classes

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们其实在前面已经讲过了,如何新增一个变量。不过我们在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; ?>


Drupal版本: