第4章 Drupal 菜单系统

老葛的Drupal培训班 http://zhupou.cn

Dru    Drupal的菜单系统很复杂,但是也很强大。术语“菜单系统”可能有点用词不当了。下面的理解可能会更恰当一些,那就是将菜单系统看作一个拥有3种主要功能的系统:1、回调映射,2、访问控制,3、菜单定制。菜单系统的基本代码位于includes/menu.inc中,而可选代码则位于modules/menu,后者可用来启用菜单的一些特性比如自定义菜单等等.
                在本章中,我们将探索一下什么是回调映射以及它是如何工作的,看一下如何通过访问控制来保护菜单项,学习如何使用菜单通配符,并逐条列出了各种内置的菜单项类型。在本章的最后,给出了如何覆写,添加,和删除已有的菜单项,这样你就可以随心所欲的定制Drupal了。
 

Drupal版本:

回调映射

当一个Web浏览器向Drupal发送一个请求时,它向Drupal传递了一个URL。通过这一信息,Drupal必须指出要运行哪段代码以及如何处理这一请求。这也就是通常所说的路由或者分发。Drupal截掉URL的基部分并使用后面的部分,后者被称之为Drupal路径。举例来说,如果URL是http://example.com/?q=node/3,则Drupal路径就为node/3。如果你使用了Drupal的简洁URL特性,那么在浏览器中的URL就是http://example.com/node/3,但是在你的web服务器中,在Drupal收到这个URL以前,web服务器已将其重写为http://example.com/?q=node/3;所以,对于Drupal来说,这两者是一样的。在前面的例子中,不管是否启用了简洁URL,其Drupal路径都是node/3。此方面的更多详细,可参看第一章中的“Web服务器的角色”。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

将URL映射为函数

通常采用的方式如下所示:Drupal请求所有启用的模块来提供一个菜单项数组,每个菜单项都包含了一个数组,其中以路径为键,里面还包含了路径的一些相关信息。一个模块必须提供的一段信息就是页面回调page callback)。在这里,回调就是一个PHP函数的名称,当一个浏览器请求一个特定的路径时就会调用它。当一个请求到达时,Drupal将执行以下步骤:

    1.建立Drupal路径。如果路径是一个真实路径的别名,Drupal将找出真实路径并使用它来代替别名。比如,如果管理员使用别名http://example.com/?q=about 来代替http://example.com/?q=node/3(比如,使用路径模块),那么Drupal将会使用node/3作为内部路径。
    2.Drupal使用menu_router表来追踪路径与回调函数之间的映射,使用menu_links表来追踪菜单项链接。首先会检查是否需要重新构建menu_router和menu_links表,不过一般在Drupal安装或者更新以后,就很少再会重新构建了。
    3.计算出menu_router表中的哪个条目对应于Drupal路径,并构建出一个路由项,来描述即将被调用的回调。
    4. 加载需要传递给回调的对象。
    5. 检查用户是否有权访问该回调。如果没有的话,返回一个“拒绝访问”消息
    6. 根据当前语言,将菜单项的标题和描述本地化。
    7. 加载需要的.inc文件
    8. 调用回调并返回结果,index.php将调用theme_page(),从而为浏览器返回一个网页。
 
该流程的图示可参看图4-1和4-2。
4-1.菜单分发流程的概览
 
4-2. 菜单路由器和链接的构建流程概览
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

创建一个菜单项

老葛的Drupal培训班 Think in Drupal

通过在你的模块中使用菜单钩子来钩住这一流程。这样你就可以定义包含在menu_router表中的菜单项。让我们构建一个名为menufun.module的示例模块,通过它来学习菜单系统。我们将Drupal路径menufun映射到PHP函数menufun _hello()上。首先,我们需要一个名为menufun.info的文件,位于sites/all/modules/custom/menufun/menufun.info:

 
; $Id$
name = Menu Fun
description = Learning about the menu system.
package = Pro Drupal Development
core = 6.x
 
    接着我们创建sites/all/modules/custom/menufun/menufun.module文件,它包含了我们的hook_menu()实现,以及我们想要运行的函数:
<?php
// $Id$
/**
* @file
* Use this module to learn about Drupal's menu system.
*/
/**
* Implementation of hook_menu().
*/
function menufun_menu() {
    $items['menufun'] = array(
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
/**
* Page callback.
*/
function menufun_hello() {
    return t('Hello!');
}
 
Enabling the module at Administer ä Site building äModules causes the menu item to
be inserted into the router table, so Drupal will now find and run our function when we go to http://example.com/?q=menufun, as shown in Figure 4-3.
 
    在“管理➤站点构建 ➤模块”中,启用该模块,这样就会将该菜单项插入到menu_router表中,这样,当我们访问http://example.com/?q=menufun时,Drupal就可以找到菜单项并运行我们的函数了,如图4-3所示。
 
    这里需要注意的要点是,我们定义了一个路径,并将其映射到了一个函数上。该路径是一个Drupal路径。我们使用该路径作为$items数组的键。你还会注意到这个路径的名字和模块的名字是一样的。这里主要是用来保证有一个干净的URL命名空间。实际上,你可以在这里定义各种路径。
   
4-3.菜单项使得Drupal能够找到和运行menufun_hello()函数。
 

Drupal版本:

定义一个标题

前面所写的hook_menu()实现,还是非常简单的。让我们在里面添加一些键,这样就和我们通常所用的差不多了。

 
function menufun_menu() {
    $items['menufun'] = array(
       'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
 
    我们为我们的菜单项给出了一个标题,当在浏览器中显示该页面时,它会自动用作页面标题(如果你想在后面的代码执行时覆写页面标题的话,你可以使用drupal_set_title())。保存了这些改动以后,刷新你的浏览器,却并没有显示出来我们定义的标题。为什么呢?这是因为Drupal将所有的菜单项存储在了menu_router表中,尽管这里我们的代码改动了,但是数据库还没有变。我们需要告诉Drupal来重新构建menu_router表。这里有两种方式。最简单的就是安装开发者模块(http://drupal.org/project/devel),并在“管理➤站点构建 ➤区块”中启用devel区块。devel区块中包含了一个名为重构菜单的选项。点击它将会重构menu_router表。如果你没有安装开发者模块的话,直接访问“管理➤站点构建 ➤模块”,也能实现同样的效果。作为显示该页面的准备工作的一部分,Drupal重构了菜单表。从现在起,我假定大家知道知道了每次代码修改后都需要重构菜单,以后就不再对此进行单独说明了。
 
    重构后,我们的页面如图4-4所示。
4-4.菜单项的标题显示在了页面和浏览器标题栏中。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

页面回调参数(1)

有时,你可能希望向映射到该路径上的函数提供更多的信息。首先,路径中的其它部分将会自动传递过来。让我们修改一下我们的函数:

 
function menufun_hello($first_name = '', $last_name = '') {
    return t('Hello @first_name @last_name',
        array('@first_name' => $first_name, '@last_name' => $last_name));
}
 
    现在,如果我们访问http://example.com/?q=menufun/John/Doe,我们将得到如图4-5所示的输出。
 
4-5.路径的其余部分传递给了回调函数。
 
    注意,这里的URL中的其它的各个部分是如何作为参数传递给我们的回调函数的。
 
    通过向$items数组中添加可选的'page arguments'(页面参数)键,你还可以在菜单钩子中定义页面回调参数。定义页面参数非常有用,这样你就可以从不同的菜单项中调用同一个回调函数了,并通过页面参数为回调提供隐藏的上下文。让我们在我们的菜单项中定义一些页面参数:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
       'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
        'type' => MENU_CALLBACK,
    );
 
    return $items;
}
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

页面回调参数(2)

老葛的Drupal培训班 Think in Drupal

你定义在页面参数中的回调参数将会首先传递给回调函数(也就是说,在传递给回调函数的参数列表中,它放在最前面),其次才是从路径中生成的参数。来自URL的参数仍然可用;为了访问它们,你可以修改回调函数的签名,从而添加来自于URL的参数。所以,对于我们修改后的菜单项,并使用下面的函数签名,那么$first_name将是Jane(页面参数数组的第一项),$last_name将是Doe(页面参数数组的第二项),$a将是John(来自于URL),而$b将是Doe(来自于URL)。
 
function menufun_hello($first_name = '', $last_name = '', $a = '', $b = '') {...}
 
    让我们做一下测试,将Jane Doe放到页面参数中,把John Doe放在URL中,然后看看结果。访问http://example.com/?q=John/Doe,你将看到如图4-6所示的结果(如果你没有得到这些结果,那你一定忘了重构你的菜单了)。
4-6.向回调函数传递和显示参数
 
    在页面回调参数的数组中,键被忽略了,所以你不能使用键来映射函数参数;在这里,只有顺序才是有意义的。回调参数通常是变量,并常用在动态菜单项中。
 

Drupal版本:

放在其它文件中的页面回调

如果你没有特别指定的话,那么Drupal会假定你把页面回调放在了.module文件中。在Drupal6中,对于每个页面请求,为了尽可能的降低为其加载的代码总量,许多模块被拆分成了多个部分。如果回调函数不在当前的.module文件中的话,可以使用菜单项中的file键,来指定哪个文件包含了该函数。我们在第2章中编写注释模块的时候,就用到了file键。

 

    如果你定义了file键,那么Drupal将会在你的模块目录下查找该文件。如果你的页面回调是由其它模块提供的话,也就是说该文件不在你的模块目录中,那么你需要告诉Drupal在查找该文件时所用的文件路径。使用file path键,就可以轻松的实现这一点了。我们在第2章的“定义你自己的管理部分”就用到了它。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

向导航区块中添加一个链接

老葛的Drupal培训班 Think in Drupal

我们把我们的菜单项的类型声明为了MENU_CALLBACK。通过将该类型改为MENU_NORMAL_ITEM,这样就不再将路径简单的映射到一个回调函数上了,而是让Drupal把它作为一个菜单包含进来。
 
提示 因为MENU_NORMAL_ITEM是Drupal的默认菜单类型,所以在本节中的代码里,type键可被忽略。我将会在后面的例子中忽略它。
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
       'type' => MENU_NORMAL_ITEM,
    );
 
    return $items;
}
 
    菜单项现在显示在了导航区块中,如图4-7所示。
4-7.菜单项显示在了导航区块中
 
If we don’t like where it is placed, we can move it down by increasing its weight. Weight is another key in the menu item definition:
如果我们觉得它放的不是地方,通过增加它的重量,我们还可以将它往下移动。重量是菜单项定义中的另一个键:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'page arguments' => array('Jane', 'Doe'),
        'access callback' => TRUE,
       'weight' => 5,
    );
 
    return $items;
}
The effect of our weight increase is shown in Figure 4-8. Menu items can also be relocated without changing code by using the menu administration tools, located at Administer ä Site building äMenus (the menu module must be enabled for these tools to appear).
我们的重量增加后的效果,如图4-8所示。在管理界面“管理➤站点构建 ➤菜单”中(你需要启用菜单模块),使用菜单管理工具,不需要修改代码,也可以调整菜单项之间的相对顺序。
 
4-8.菜单项的重量越大,在导航区块中的位置就越往下。
 

Drupal版本:

菜单嵌套

老葛的Drupal培训班 Think in Drupal

到目前为止,我们仅仅定义了一个静态菜单项。让我们再添加一个与它相关的子项:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
        'weight' => -10,
    );
    $items['menufun/farewell'] = array(
       'title' => 'Farewell',
       'page callback' => 'menufun_goodbye',
       'access callback' => TRUE,
    );
 
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello() {
return t('Hello!');
}
/**
* Page callback.
*/
function menufun_goodbye() {
    return t('Goodbye!');
}
 
    Drupal将会注意到第2个菜单项(menufun/farewell)的路径是第一个菜单项路径(menufun)的孩子。因此,在显示菜单时(转化为HTML),Drupal将会缩进第2个菜单项,如图4-9所示。它还在页面的顶部正确的设置了面包屑,以指示嵌套关系。当然,根据设计者的要求,主题可将菜单或面包屑显示成所要的样式。
 
4-9.嵌套菜单
 

Drupal版本:

访问控制

到目前为止,在前面的所有例子中,我们都简单的将菜单项的access callback键设置为了TRUE,这意味着所有的用户都可以访问我们的菜单。一般情况下,通过在模块中使用hook_perm()来定义权限,并使用一个函数来测试这些权限,从而实现对菜单的访问控制。这里所用的函数的名字定义在菜单项的access callback键中,它一般为user_access。让我们定义一个名为receive greeting的权限;如果用户没有哪个角色具有该权限的话,当他/她访问页面http://example.com/?q=menufun时,将会收到一个“拒绝访问”消息。

 
/**
* Implementation of hook_perm().
*/
function menufun_perm() {
    return array('receive greeting');
}
 
/**
* Implementation of hook_menu().
*/
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
        'page callback' => 'menufun_hello',
       'access callback' => 'user_access',
       'access arguments' => array('receive greeting'),
        'weight' => -10,
    );
    $items['menufun/farewell'] = array(
        'title' => 'Farewell',
        'page callback' => 'menufun_goodbye',
    );
 
    return $items;
}
 
 
    在前面的代码中,是根据user_access('receive greeting')的返回结果来判定是否允许访问的。这样,菜单系统就相当于一个门卫,它会基于用户的角色来判定哪些路径可以访问,哪些路径不可以访问。
 
提示 user_access()函数是默认的访问回调。如果你没有定义访问回调的话,那么访问参数将被菜单系统传递给user_access()。
 
    子菜单一般不会继承父菜单项的访问回调和访问参数。所以必须为每个菜单项定义access arguments键。如果访问回调不是user_access的话,那么还需要定义access callback键。不过也有例外,那就是类型为MENU_DEFAULT_LOCAL_TASK的菜单项,它可以继承父菜单项的访问回调和访问参数,不过为了清晰起见,对于这些默认的本地任务菜单项,最好能够为其明确的定义这些键。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

标题的本地化和定制

Drupal支持多语言,它使用t()函数来翻译字符串。所以你可能会想,菜单项中的title键应该是这样定义的:

'title' => t('Greeting') // No! don't use t() in menu item titles or descriptions.
 
    然而,你想错了。菜单字符串是以原始字符串的形式存储在menu_router表中的,而菜单项的翻译则被推迟到了运行时进行。真实情况是,Drupal有一个默认的翻译函数(t()函数),它被指定用来翻译菜单标题。你将在后面看到,如何将默认翻译函数修改为你选择的函数,以及如何向该函数传递参数。负责翻译的函数被称为title callback(标题回调),而传递过来的参数则被称为title arguments(标题参数)。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

定义标题回调

如果没有在菜单项中定义标题回调的话,Drupal将默认使用t()函数。我们也可以明确地给出这个回调函数的名字,在title callback键中声明它就是了:

 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
       'title callback' => 't',
        'description' => 'A salutation.',
        'page callback' => 'menufun_hello',
        'access arguments' => array('receive greeting'),
    );
}
 
注意 不管title callback键的值如何,description键总是使用t()来翻译的。这里没有描述回调键。
 
    嗯。如果我们为标题回调声明自己的函数,那会怎么样呢?那就让我们看看吧:
 
function menufun_menu() {
    $items['menufun'] = array(
        'title' => 'Greeting',
       'title callback' => 'menufun_title',
        'description' => 'A salutation.',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
 
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello() {
    return t('Hello!');
}
/**
* Title callback.
*/
function menufun_title() {
    $now = format_date(time());
    return t('It is now @time', array('@time' => $now));
}
 
    如图4-10所示,通过使用一个自定义标题回调,就可以实现在运行时设置菜单项标题了。但是,如果想让菜单项标题和页面标题不一样时,那该怎么办呢?简单。我们可以使用drupal_set_title()来设置页面标题:
 
function menufun_title() {
    drupal_set_title(t('The page title'));
    $now = format_date(time());
    return t('It is now @time', array('@time' => $now));
}
 
4-10.标题回调设置了菜单项的标题
    这样就将页面和菜单项的标题分离了开来,如图4-11所示。
4-11. 将菜单项和页面的标题分离开来
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

标题参数

Drupal的翻译函数,可以接受一个字符串和一个用来替换的数组(更多关于t()函数的详细,参看第18章),例如:

t($string, $keyed_array);
t('It is now @time', array('@time' => $now));
 
    那么,如果菜单项的title键就是传递给t()的字符串,那么用来替换的数组放在哪里呢?这个问题问得好。title arguments键就是负责这件事的:
function menufun_menu() {
    $items['menufun'] = array(
       'title' => 'Greeting for Dr. @name',
       'title callback' => 't',
       'title arguments' => array('@name' => 'Foo'),
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
 
    return $items;
}
 
    在运行时,翻译函数运行了,占位符也被填充了,如图4-12所示。
4-12.标题参数被传递给了标题回调函数。
 
    不过这种替换也有一个缺点。因为定义在菜单钩子中的菜单项是在菜单构建流程期间保存在数据库中的,所以title arguments中的代码是在菜单构建时执行的,而不是在运行时。如果你想在运行时修改你的菜单的话,最好使用title callback键;定义在这里的函数将在运行时运行。
 
警告 title arguments键中的值必须是字符串。整数将被清除掉;因此,'title arguments' => array('@name' => 3)将不起作用,而'title arguments' => array('@name' => '3')则能正常工作。这是因为整数具有特殊含义,你将在接下来看到。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

菜单项中的通配符

到目前为止,我们在菜单项中所用的都是普通的Drupal路径名字,比如menufun 和menufun/farewell。但是Drupal还经常使用这样的路径,比如user/4/track或node/15/edit,在这些路径中,有一部分是动态的。现在,让我们来看看动态路径是如何工作的。

 
基本通配符
    %字符在Drupal菜单项中是一个特殊的字符。它意味着“从这到下一个/字符之间的字符串”。下面是一个使用了通配符的菜单项:
 
function menufun_menu() {
    $items['menufun/%'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'access callback' => TRUE,
    );
    return $items;
}
 
    这个菜单项适用于的Drupal路径可以有menufun/hi, menufun/foo/bar, menufun/
123, 和menufun/file.html。但是它不适用于路径menufun;对于后者,因为它只包含了一个部分,而menufun/%只匹配具有两部分的字符串,所以你需要为其单独创建一个菜单项。注意,尽管%通常是用来指定一个数字的(比如,user/%/edit用于user/2375/edit),但是它能匹配该位置上的任何文本。
 
注意 在路经中带有通配符的菜单项,即便是将菜单项的类型设置为MENU_NORMAL_ITEM,它也不会显示在导航菜单中。原因很明显:由于路径中包含了一个通配符,所以Drupal不知道如何为该路径构建URL。这是一般情况下的规律,也有例外的情况,因为你可以告诉Drupal使用什么URL,更多详细,可参看本章后面的“使用to_arg()函数为通配符构建路径”。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

通配符和页面回调参数

老葛的Drupal培训班 Think in Drupal

菜单路径中的通配符,不影响将URL中的额外部分作为参数传递给页面回调,这是因为通配符只匹配到下一个/字符。继续使用我们的menufun/%路径作为例子,对于URL http://example.com/?q=menufun/foo/Fred,通配符所匹配的字符串就是foo,而对于路径中的最后一部分(Fred),它将作为一个参数传递给页面回调。

Drupal版本:

使用通配符的值

老葛的Drupal培训班 Think in Drupal

为了使用路径中匹配的那部分,可以在page arguments键中指定该路径部分的位置:
function menufun_menu() {
    $items['menufun/%/bar/baz'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'page arguments' => array(1), // The matched wildcard.
        'access callback' => TRUE,
    );
    return $items;
}
 
/**
* Page callback.
*/
function menufun_hello($a = NULL, $b = NULL) {
    return t('Hello. $a is @a and $b is @b', array('@a' => $a, '@b' => $b));
}
 
我们的页面回调函数menufun_hello()所收到的参数,如图4-13所示。
 
4-13.第一个参数来自于匹配的通配符,第2个参数来自于URL的最后部分。
 
    第一个参数,$a,是通过页面回调传递过来的。用于页面回调的条目array(1)的意思是,“不管路径中的部分1是什么,请将它传递过来”。我们是从0开始算起的,所以部分0就是'menufun',部分1就是通配符所匹配的任何东西,部分2就是'bar',依次类推。第2个参数,$b,也被传递了过来,它的传递原理我们在前面已经学过了,那就是Drupal路径后面的一部分将会作为参数传递给回调函数(参看本章前面的“页面回调参数”一节)。
 

Drupal版本:

通配符和参数替换

在实际中,一个Drupal路径的一部分通常是用来查看或者修改一个对象的,比如一个节点对象或者一个用户对象。例如,路径node/%/edit用来编辑一个节点,而路径user/%则用来根据用户ID来查看用户的相关信息。让我们看一下后者的菜单项,你可以在modules/user/user.module中的hook_menu()实现中找到它。这个路径匹配的URL应该看起来是这样的http://example.com/?q=user/2375。在你的Drupal站点上点击查看“我的帐号”页面,就能看到这样的URL了。

 
 
$items['user/%user_uid_optional'] = array(
    'title' => 'My account',
    'title callback' => 'user_page_title',
    'title arguments' => array(1),
    'page callback' => 'user_view',
    'page arguments' => array(1),
    'access callback' => 'user_view_access',
    'access arguments' => array(1),
    'file' => 'user_pages.inc',
);
 
 
暂停一下!路经user/%user_uid_optional是怎么一回事呢?我们在这里详细的解释一下:
1.使用/字符将路径切分成各个部分。
2.在第2部分中,匹配从%到下一个可能的/字符之间的字符串。在这里,该字符串就是user_uid_optional。
3. 向该字符串上追加_load,来生成一个函数的名字。在这里,该函数的名字就是user_uid_optional_load。
4. 调用该函数,并将Drupal路径中通配符的值作为参数传递给它。所以,如果URL为http://example.com/?q=user/2375, Drupal路径为user/2375,而通配符匹配的第2部分就是2375,那么调用的就是user_uid_optional_load('2375')。
5. 使用这个调用所返回的结果来替换通配符。这里的标题参数为array(1),在标题回调被调用时,我们没有传递Drupal路径中的部分1(2375),而是传递了user_uid_optional_load('2375')返回的结果,也就是一个用户对象。我们可以把它看作,Drupal路径中的一部分被它所表示的对象替换了。
6. 注意,页面回调和访问回调也将使用替换对象。所以,在前面的菜单项中,user_view_access()用于访问控制,user_view()则用于生成页面内容,对于这两者,都会传递进来用户2375的用户对象。
 
提示 对于Drupal路径,比如node/%node/edit,如果你将%node看作是一个通配符%,并在其右边加上了一个注释的话(这里为node),那么会更容易的理解对象替换。换句话说,node/%node/edit实际上就是node/%/edit,外加了一个隐藏指令:为通配符匹配的内容运行node_load()。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

向加载函数传递额外的参数

如果需要向加载函数传递额外的参数,那么可以使用load arguments键来定义它们。下面是来自节点模块的例子:一个用来查看节点修订本的菜单项。在这里需要向加载函数,也就是node_load(),传递节点ID和修订本的ID。

 
$items['node/%node/revisions/%/view'] = array(
    'title' => 'Revisions',
    'load arguments' => array(3),
    'page callback' => 'node_show',
    'page arguments' => array(1, NULL, TRUE),
    'type' => MENU_CALLBACK,
);
 
    菜单项为load arguments键指定了array(3)。这意味着,除了节点ID通配符的值会自动传递给加载函数以外,还会向加载函数传递一个额外的参数。因为array(3)里面有个元素,也就是整数3;我们在“使用通配符的值”一节中已经讲过,这意味着将会使用路径中的部分3。位置和路径参数的示例URL http://example.com/?q=node/56/revisions/4/view,如表4-2所示。
 
4-2.当查看页面http://example.com/?q=node/56/revisions/4/view时,Drupal路径node/%node/revisions/%/view的位置和参数
 
位置   参数       来自URL的值
0       node        node
1       %node       56
2       revisions  revisions
3       %           4
4       view        view
 
    因此,定义了load arguments键,这就意味着将会调用node_load('56', '4'),而不是node_load('56')了。
 
    当页面回调运行时,加载函数会将'56'替换为加载了的节点对象,所以页面回调将会是node_show($node, NULL, TRUE)。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

特殊的,预定义的加载参数:%map和%index

老葛的Drupal培训班 Think in Drupal

有两个特殊的加载参数。%map令牌将当前Drupal路径作为数组进行传递。在前面的例子中,如果%map作为一个加载参数传递过来的话,那么其值就为array('node', '56', 'revisions', '4', 'view')。如果加载函数的参数是通过引用传递的话,那么加载函数可以修改%map中对应的值。例如,在modules/user/user.module中,user_category_load($uid, &$map, $index)就是这样用来处理类别名字中字符“/”的。
 
    %index令牌在加载函数中指的是通配符的位置。对于前面的例子,由于通配符的位置为1,如表4-2所示,所以该令牌的值就为1。

Drupal版本:

使用to_arg()函数为通配符构建路径

老葛的Drupal培训班 Think in Drupal

还记不记得我曾经说过,对于包含通配符的Drupal路径,Drupal无法为其创建一个有效的链接,比如user/%(毕竟,Drupal怎么会知道如何替换%呢)?不过这一点并非完全正确。我们可以定义一个帮助函数,来为通配符生成一个替身,这样,在Drupal构建链接时就可以使用替身了。在菜单项“我的帐号”中,使用以下步骤来生成“我的帐号”链接的路径:
    1. Drupal路径最初为user/%user_uid_optional。
    2. 当构建链接时,Drupal会查找一个名为user_uid_optional_to_arg()的函数。如果没有定义该函数的话,那么Drupal就不知道如何构建路径,因此也就无法显示链接了。
    3. 如果找到了该函数,那么Drupal将会使用该函数返回的结果来替代链接中的通配符。user_uid_optional_to_arg()函数返回了当前用户的用户ID,假定你就是用户4,那么Drupal就会将“我的帐号”链接到http://example.com/?q=user/4
 
    to_arg()函数不是特定于一个给定路径的。换句话说,对于任何页面,在构建链接期间都会运行to_arg()函数,而不仅仅是对于匹配Drupal路径的一个特定页面。“我的帐号”链接显示在所有的页面,而不仅仅是页面http://example.com/?q=user/3

 

Drupal版本:

通配符和to_arg()函数的特殊情况

老葛的Drupal培训班 Think in Drupal

Drupal在为一个菜单项构建链接时,它要查找的to_arg()函数,是基于Drupal路径中通配符后面的字符串生成的。它可以是任意的字符串,例如:
/**
* Implementation of hook_menu().
*/
function_menufun_menu() {
    $items['menufun/%a_zoo_animal'] = array(
        'title' => 'Hi',
        'page callback' => 'menufun_hello',
        'page arguments' => array(1),
        'access callback' => TRUE,
        'type' => MENU_NORMAL_ITEM,
        'weight' => -10
    );
    return $items;
}
 
function a_zoo_animal_to_arg($arg) {
    // $arg is '%' since it is a wildcard
    // Let's replace it with a zoo animal.
    return 'tiger';
}
 
    这样,链接“Hi”就会出现在导航区块中了。该链接的URL为http://example.com/?q=menufun/tiger。通常,你不会像这个简单的例子中这样,使用一个静态字符串来替换通配符,而是使用动态的东西,比如当前用户的uid或者当前节点的nid。

Drupal版本:

改变其它模块的菜单项

老葛的Drupal培训班 Think in Drupal

Drupal重构menu_router表和更新menu_link表时(比如,当一个新模块被启用时),通过实现hook_menu_alter(),模块就可以修改任意的菜单项。例如,“登出”菜单项通过调用user_logout()将当前用户登出,它将销毁用户的会话并将用户重定向到站点的首页。由于user_logout()函数位于modules/user/user.pages.inc,所以该Drupal路径的菜单项中定义了file键。所以,通常情况下,当一个用户点击了导航区块中的“登出”链接,Drupal会加载文件modules/user/user.pages.inc并运行user_logout()函数。
/**
* Implementation of hook_menu_alter().
*
* @param array $items
* Menu items keyed by path.
*/
function menufun_menu_alter(&$items) {
    // Replace the page callback to 'user_logout' with a call to
    // our own page callback.
    $items['logout']['page callback'] = 'menufun_user_logout';
    // Drupal no longer has to load the user.pages.inc file
    // since it will be calling our menufun_user_logout(), which
    // is in our module -- and that's already in scope.
    unset($items['logout']['file']);
}
 
/**
* Menu callback; logs the current user out, and redirects to drupal.org.
* This is a modified version of user_logout().
*/
function menufun_user_logout() {
    global $user;
 
    watchdog('menufun', 'Session closed for %name.', array('%name' => $user->name));
 
    // Destroy the current session:
    session_destroy();
    // Run the 'logout' operation of the user hook so modules can respond
    // to the logout if they want to.
    module_invoke_all('user', 'logout', NULL, $user);
 
    // Load the anonymous user so the global $user object will be correct
    // on any hook_exit() implementations.
    $user = drupal_anonymous_user();
 
    drupal_goto('http://drupal.org/');
}
    在我们的hook_menu_alter()实现运行以前,logout路径的菜单项应该是这样的:
array(
    'access callback' => 'user_is_logged_in',
    'file' => 'user.pages.inc',
    'module' => 'user',
    'page callback' => 'user_logout',
    'title' => 'Log out',
    'weight' => 10,
)
    当我们修改了它以后,就变成了这样:
array(
    'access callback' => 'user_is_logged_in',
    'module' => 'user',
    'page callback' => 'menufun_user_logout',
    'title' => 'Log out',
    'weight' => 10,
)

Drupal版本:

改变其它模块的菜单链接

模块通过实现hook_menu_link_alter(),就可以在Drupal将一个菜单项保存到menu_link表时,来修改该链接了。下面是如何将“登出”菜单项的标题改为“Sign off”(“结束”)的。

 
/**
* Implementation of hook_link_alter().
*
* @param $item
* Associative array defining a menu link as passed into menu_link_save().
* @param $menu
* Associative array containing the menu router returned from
* menu_router_build().
*/
function menufun_menu_link_alter(&$item, $menu) {
    if ($item['link_path'] == 'logout') {
        $item['link_title'] = 'Sign off';
    }
}
 
    这个钩子可以用来修改一个链接的标题和重量。如果你需要修改一个菜单项的其它属性的话,比如访问回调,那么需要使用hook_menu_alter()。
 
注意hook_menu_link_alter()中对菜单项所做的修改,是无法在用户界面“管理➤站点构建 ➤菜单”中再进行覆写的。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

菜单项的种类

当你在菜单钩子中添加一个菜单项时,经常用到的一个键就是type。如果你没有定义类型,那么将会使用默认类型MENU_NORMAL_ITEM。Drupal将根据你指定的类型来对菜单项进行不同的处理。每一个菜单项类型都有一系列的标记或者属性组成。表4-2列出了菜单项类型的标记。

 
4-2. 菜单项类型标记
二进制        十六进制   十进制        常量
000000000001    0x0001      1               MENU_IS_ROOT
000000000010    0x0002      2               MENU_VISIBLE_IN_TREE
000000000100    0x0004      4               MENU_VISIBLE_IN_BREADCRUMB
000000001000    0x0008      8               MENU_LINKS_TO_PARENT
000000100000    0x0020      32              MENU_MODIFIED_BY_ADMIN
000001000000    0x0040      64              MENU_CREATED_BY_ADMIN
000010000000    0x0080      128             MENU_IS_LOCAL_TASK
 
    例如,常量MENU_NORMAL_ITEM拥有标记MENU_VISIBLE_IN_TREE和MENU_VISIBLE_IN_BREADCRUMB,如表4-3所示。看一下不同的标记在单个常量中是如何表示的,你看出来了吗?
 
4-3.菜单项类型MENU_NORMAL_ITEM的标记
二进制            常量
000000000010        MENU_VISIBLE_IN_TREE
000000000100        MENU_VISIBLE_IN_BREADCRUMB
000000000110        MENU_NORMAL_ITEM
 
    因此,MENU_NORMAL_ITEM拥有下列标记:0000000001104-4展示了可用的菜单项类型和它们所表示的标记
 
 
4-4. 菜单项类型表示的标记
菜单标记              菜单类型常量
                      MENU_                 MENU_            MENU_            MENU_
                            NORMAL_    MENU_      SUGGESTED_             LOCAL_           DEFAULT_
                            ITEM       CALLBACK  ITEM*            TASK             LOCAL_TASK
MENU_IS_ROOT
MENU_VISIBLE_IN_TREE        X
MENU_VISIBLE_IN_BREADCRUMB  X          X          X
MENU_LINKS_TO_PARENT                                                                X
MENU_MODIFIED_BY_ADMIN
MENU_CREATED_BY_ADMIN
MENU_IS_LOCAL_TASK                                                 X                X
*这个常量是由位逻辑运算符OR和0x0010创建的
 
    当你定义自己的菜单项类型时,应该使用哪一个常量呢?查看表4-4,在里面看一下你想启用哪些标记,然后使用包含这些标记的常量。对于每个常量的详细描述,参看includes/menu.inc里面的注释。最常用的为MENU_CALLBACK, MENU_LOCAL_TASK, 和 MENU_DEFAULT_LOCAL_TASK。更多详细,请仔细阅读里面的注释。
 
老葛的Drupal培训班 Think in Drupal

Drupal版本:

常见任务

老葛的Drupal培训班 Think in Drupal

在这一部分,我们给出了一些常见问题的典型解决办法,这些问题都是程序员使用菜单时会经常遇到的。

 
分配回调而无须向菜单添加一个链接
    通常,你可能想将一个URL映射到一个函数上,而不需要创建一个可见的菜单项。例如,你可能在web表单中有一个JavaScript函数,它需要从Drupal中得到一列状态,所以你需要将这个URL映射到一个PHP函数上,而不需要将它放到导航区块中。你可以通过将你的菜单项的类型指定为MENU_CALLBACK来实现这一点,就像本章中的第一个例子那样。

Drupal版本:

将菜单项显示为标签

Drupal的公认晦涩的菜单行话来说,一个显示为标签的回调被认为是一个本地任务,它的菜单类型为MENU_LOCAL_TASK或者MENU_DEFAULT_LOCAL_TASK.本地任务的标题应该是一个简短的动词,比如“添加”或者“列出”。它通常作用在一些对象上,比如节点,或者用户。你可以把一个本地任务想象为一个关于菜单项的语义声明,通常显示为一个标签(tab)----这和<strong>标签类似,后者也是一个语义声明,通常用来显示加粗的文本。

 
    为了显示标签,本地任务必须有一个父菜单项。一个常用的实践是将一个回调指定到一个根路径上,比如milkshake,然后将本地任务指定到扩展了该路径的子路径上,比如milkshake/prepare,milkshake/drink,等等。Drupal内建的主题支持两级标签本地任务。(底层系统可以支持多级的标签,但是为了显示更多的层级,你需要让你的主题为此提供支持。)
 
    标签的显示顺序是由菜单项标题的字母顺序决定的。如果这种顺序不是你想要的,那么你可以为你的菜单项添加一个weight键,然后它们将按照重量进行排序。
 
    下面的例子所示的代码将会生成了两个主标签和位于默认本地任务下面的两个次标签。创建sites/all/modules/custom/milkshake/milkshake.info文件,如下所示:
 
; $Id$
name = Milkshake
description = Demonstrates menu local tasks.
package = Pro Drupal Development
core = 6.x
 
    接着,创建sites/all/modules/custom/milkshake/milkshake.module文件:
<?php
// $Id$
 
/**
* @file
* Use this module to learn about Drupal's menu system,
* specifically how local tasks work.
*/
 
/**
* Implementation of hook_perm().
*/
function milkshake_perm() {
    return array('list flavors', 'add flavor');
}
 
/**
* Implementation of hook_menu().
*/
function milkshake_menu() {
    $items['milkshake'] = array(
        'title' => 'Milkshake flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_overview',
        'type' => MENU_NORMAL_ITEM,
    );
    $items['milkshake/list'] = array(
        'title' => 'List flavors',
        'access arguments' => array('list flavors'),
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => 0,
    );
    $items['milkshake/add'] = array(
        'title' => 'Add flavor',
        'access arguments' => array('add flavor'),
        'page callback' => 'milkshake_add',
        'type' => MENU_LOCAL_TASK,
        'weight' => 1,
    );
    $items['milkshake/list/fruity'] = array(
        'title' => 'Fruity flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_list',
        'page arguments' => array(2), // Pass 'fruity'.
        'type' => MENU_LOCAL_TASK,
    );
    $items['milkshake/list/candy'] = array(
        'title' => 'Candy flavors',
        'access arguments' => array('list flavors'),
        'page callback' => 'milkshake_list',
        'page arguments' => array(2), // Pass 'candy'.
        'type' => MENU_LOCAL_TASK,
    );
    return $items;
}
 
function milkshake_overview() {
    $output = t('The following flavors are available...');
    // ... more code here
    return $output;
}
 
function milkshake_add() {
    return t('A handy form to add flavors might go here...');
}
 
function milkshake_list($type) {
    return t('List @type flavors', array('@type' => $type));
}
    图 4-14给出了在Bluemarine主题下的效果图
4-14.本地任务和标签化菜单
 
    注意,页面的标题来自于父回调,而不是来自于默认本地任务。如果你想使用一个不同的标题,那么可以使用drupal_set_title()来设置它。
 
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

隐藏已有菜单项

老葛的Drupal培训班 Think in Drupal

如何隐藏已有的菜单项?只需要修改它们的链接项的hidden属性就可以了。假定由于一些原因,你想要删除“创建内容”这个菜单项。那么可以使用我们熟悉的hook_menu_link_alter():
/**
* Implementation of hook_menu_link_alter().
*/
function menufun_menu_link_alter(&$item, $menu) {
    // Hide the Create content link.
    if ($item['link_path'] == 'node/add') {
        $item['hidden'] = 1;
    }
}
 

Drupal版本:

使用menu.module

启用Drupal的菜单模块,就可以为管理员提供一个方便的用户界面,来定制已有的菜单,比如导航菜单或者一级/二级链接菜单,或者来添加新菜单。当位于includes/menu.inc中的menu_rebuild()函数运行时,表示菜单树的数据结构将被存储在数据库中。当你启用模块时,当你禁用模块时,或者当其它的一些能够影响菜单树结构的事情发生时,都会运行menu_rebuild()函数。数据将被保存到数据库的menu_router表中,而关于链接的信息则被保存到了menu_links表中。

 
    在为一个页面构建链接的流程期间,Drupal首先会基于从模块的菜单钩子实现中获取的路径信息和从menu_router表中获取的路径信息,来构建菜单树;接着,它会使用来自于数据库的菜单信息对前面的信息进行覆盖。这样一来,你就可以使用menu.module来修改菜单树的父亲、路径、标题、和描述了----实际上你并没有真正的修改底层的菜单树,而是创建了一些数据来覆盖在菜单树的上面。
 
注意 菜单树类型,比如MENU_CALLBACK 或者DEFAULT_LOCAL_TASK,在数据库中是以数字的形式存储的。
 
    menu.module还在节点表单上加了一部分,用来将当前文章添加为一个菜单项。
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

常见错误

现在你刚刚在你的模块中实现了菜单钩子,但是你的回调没有响应,你的菜单没有显示,或者就是不能正常工作,也就是出了问题。那么对于下面这些地方,你需要好好的检查一下:

• 你的access callback键所指的函数,返回的是否为FALSE?
• 在你菜单钩子的结尾处,你是不是忘记添加return $items;一行了?
• 你是不是不小心将access arguments或者page arguments的值设置为了一个字符串,而不是一个数组?
• 你是不是忘记了清空你的菜单缓存并重建菜单了?
• 如果你想通过将菜单类型指定为MENU_LOCAL_TASK从而将其显示为标签,那么你有没有指定一个带有回调函数的父菜单项?
• 如果你使用的是本地任务,那么在一个页面上的标签是不是少于两个阿(为了显示,必须多于等于两个)?
老葛的Drupal培训班 Think in Drupal
 

Drupal版本:

总结

老葛的Drupal培训班 Think in Drupal

当读完这一章后,你应该可以:

URL映射到函数上
理解访问控制的工作原理
理解如何在路径中使用通配符
创建带有标签(本地任务)的页面,标签能映射到函数上
通过代码来修改已有的菜单项
更多阅读,可参看menu.inc中的注释。当然,还可参看http://drupal.org/node/102338http://api.drupal.org/?q=api/group/menu/6
 

Drupal版本: