第3章 Drupal 菜单系统

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

菜单就是网站的导航链接,Drupal自带的菜单模块,允许我们添加、编辑、删除菜单和菜单项。这里的菜单项,就是通常所说的导航链接,通常菜单中的导航链接是有层级关系的。Drupal7自带了四个菜单,主菜单、管理菜单、导航菜单、用户菜单。Drupal的菜单系统,除了包含前面菜单模块所提供的功能以外,还蕴含着其它含义。为了更好的理解Drupal的菜单系统,我们可以从它提供的功能入手,菜单系统提供了三种功能:1、回调映射,2、访问控制,3、菜单定制。菜单系统的基本代码位于includes/menu.inc中,主要包含了前两点功能;而可选代码则位于modules/menu,这就是菜单模块提供的普通用户可见的菜单功能。

 

在本章中,我们将主要学习菜单系统中开发相关的知识,比如如何创建一个菜单项,如何通过访问控制来保护菜单项,学习如何使用菜单通配符,以及内置的各种菜单项类型。在本章的最后,给出了如何修改、添加、和删除已有的菜单项,这样你就可以灵活的控制Drupal菜单了。


Drupal版本:

10 页面回调参数

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

有时候,我们可能希望能够向回调函数提供更多的信息。首先,路径中的其它部分能够自动的作为参数传递过来。让我们修改一下我们的回调函数,增加两个参数:

 

/**

 * 菜单项menu_abc/sub的回调函数.

 */

function menu_abc_sub_callback_page($arg1 = '', $arg2 = ''){

  $render_array = array();

  $render_array['#markup'] = t('菜单ABC子页面内容 @arg1 @arg2', array('@arg1' =>

    $arg1, '@arg2' => $arg2));   

  return $render_array;

}

现在,如果我们访问http://localhost/thinkindrupal/menu_abc/sub/A/B/C,我们将得到如图3-11所示的输出。

 

 

图片1.png

 3-11.路径的其余部分传递给了回调函数。

 

我们在这里需要注意,URL中的其余部分是如何作为参数传递给我们的回调函数的,注意A作为第一参数,B作为第二个参数传递了过来,而C则没有传递过来。

 

还可以在菜单钩子中定义页面回调的参数,只需要使用'page arguments'(页面参数)键即可。定义页面参数有时非常有用,这样我们就可以定义一个回调函数,供不同的菜单项调用,而菜单项则可以使用页面参数来传递上下文。让我们在我们的菜单项中定义一下页面参数:

 

  $items['menu_abc/sub'] = array(

    'title' => '菜单ABC子项',

    'description' => '菜单ABC的子项.',

    'page callback' => 'menu_abc_sub_callback_page',

'page arguments' => array('B', 'C'),

'file' => 'menu_abc.pages.inc',

    'access callback' => TRUE,

'weight' => 10,

'menu_name' => 'main-menu',

  );

我们在菜单钩子中定义的回调参数,将会首先传递给回调函数(也就是说,在传递给回调函数的参数列表中,它放在最前面),其次才是从路径中获取的参数。来自URL的参数仍然可用。让我们做一下测试,仍然访问http://localhost/thinkindrupal/menu_abc/sub/A/B/C,我们将看到如图3-12所示的结果(如果你没有得到这一结果,那你清空一下缓存)。

图片2.png 

 3-12.向回调函数中传递和显示参数


    此时,我们看到起作用的是菜单钩子中定义的回调参数,通过URL传递过来的参数则没有起任何作用。原因是我们这里只有两个参数,如果再添加两个参数的话:

 

/**

 * 菜单项menu_abc/sub的回调函数.

 */

function menu_abc_sub_callback_page($arg1 = '', $arg2 = '', $arg3 = '', $arg4 = ''){

  $render_array = array();

  $render_array['#markup'] = t('菜单ABC子页面内容 @arg1 @arg2 @arg3 @arg4',  array('@arg1' => $arg1, '@arg2' => $arg2, '@arg3' => $arg3, '@arg4' => $arg4));

  return $render_array;

}

    此时,我们再次访问页面menu_abc/sub/A/B/C,注意页面内容的变化:

图片3.png 

      3-13.页面参数和URL参数

 

 

在页面回调参数的数组中,键被忽略了,只有值才有意义,所以你不能使用键来对应回调函数中的参数;在这里,是按照顺序走。回调参数通常是变量,并常用在动态菜单项中。


Drupal版本:

11 将菜单项显示为标签

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

有时候,Drupal的术语是异常晦涩难懂的,同样的名字,在Drupal中有着不同的含义,拿标签tabs来说,在Drupal中,我们把它叫做本地任务,对应的菜单类型为MENU_LOCAL_TASK或者MENU_DEFAULT_LOCAL_TASK本地任务的标题通常是一个简短的动词,比如“添加”或者“列出”。它通常作用在一些对象上,比如节点,或者用户。我们可以把一个本地任务理解为一个关于菜单项的语义声明,通常显示为一个标签(tab)----这和<strong>标签类似,后者也是一个语义声明,通常用来显示加粗的文本。

 

为了显示标签,本地任务必须有一个父菜单项。一个常用的做法是将一个回调指定到一个根路径上,比如my,然后将本地任务指定到扩展了该路径的子路径上,比如my/ordersmy/commentsmy/favorites等等。Drupal内置的主题仅支持两级本地任务。(底层系统可以支持多级的本地任务,但是为了显示更多的层级,你需要让你的主题为此提供支持。)

 

本地任务的显示顺序是由菜单项标题的字母顺序决定的。如果这种顺序不是你想要的,那么你可以为你的菜单项添加一个weight键,然后它们将按照重量进行排序。

 

下面的例子中,将会生成了四个主标签。这是一个实际模块代码改造而来的实例,用来解决Drupal用户个人主页的调整。我们将模块名字命名为my,然后分别创建3个文件,my.info、my.module、my.pages.inc,之后向my.info文件中添加以下内容: 

 

name = 用户主页

description = 使用tabs作为用户主页导航

core = 7.x

接着,向my.module文件中添加以下代码

<?php

 

/**

 * @file

 * 演示Drupal中菜单API本地任务的基本用法,

 */

 

/**

 * 实现 hook_menu().

 */

function my_menu() {

 

  $items['my'] = array(

    'title' => '我的主页',

    'page callback' => 'my_home_page',

    'file' => 'my.pages.inc',

    'access callback' => 'my_access_callback',

  );


$items['my/home'] = array(

    'title' => '我的主页',

    'page callback' => 'my_home_page',

    'file' => 'my.pages.inc',

    'access callback' => 'my_access_callback',

    'type' => MENU_DEFAULT_LOCAL_TASK,

    'weight' => 1,  

  );


$items['my/orders'] = array(

    'title' => '我的订单',

    'page callback' => 'my_orders_page',

    'file' => 'my.pages.inc',

    'access callback' => 'my_access_callback',

    'type' => MENU_LOCAL_TASK,

    'weight' => 2,

  );


$items['my/comments'] = array(

    'title' => '我的评论',

    'page callback' => 'my_comments_page',

    'file' => 'my.pages.inc',

    'access callback' => 'my_access_callback',

    'type' => MENU_LOCAL_TASK,

    'weight' => 3,  

  );


$items['my/favorites'] = array(

    'title' => '我的收藏',

    'page callback' => 'my_favorites_page',

    'file' => 'my.pages.inc',

    'access callback' => 'my_access_callback',

    'type' => MENU_LOCAL_TASK,

    'weight' => 4,

  );

  return $items;

}

 

/**

 * 页面回调.

 */

function my_access_callback(){

  global $user;

  $flag = FALSE;

  //只有注册用户才能访问自己的主页

  if($user->uid>0){

    $flag = TRUE;

  }

  return $flag;

}

 

    最后向my.pages.inc中添加对应的回调函数,注意回调函数中有注释,

 

<?php

 

/**

 * @file

 * my的各种回调函数,

 */

 

/**

 * 菜单项my的回调函数.

 */

function my_home_page(){

  global $user;

  $render_array = array();

  $render_array['#markup'] = t('我的主页页面内容');

  //逻辑代码,比如

  // $render_array['#markup'] .= views_embed_view('my_home', 'block', $user->uid);

  return $render_array;

}

 

/**

 * 菜单项my/orders的回调函数.

 */

function my_orders_page(){

  global $user;

  $render_array = array();

  $render_array['#markup'] = t('我的订单页面内容');

  //逻辑代码,比如

  // $render_array['#markup'] .= views_embed_view('my_orders', 'block', $user->uid);

  return $render_array;

}

 

/**

 * 菜单项my/comments的回调函数.

 */

function my_comments_page(){

  global $user;

  $render_array = array();

  $render_array['#markup'] = t('我的评论页面内容');

  //逻辑代码,比如

  // $render_array['#markup'] .= views_embed_view('my_comments', 'block', $user->uid);

  return $render_array;

}

 

/**

 * 菜单项my/favorites的回调函数.

 */

function my_favorites_page(){

  global $user;

  $render_array = array();

  $render_array['#markup'] = t('我的收藏页面内容');

  //逻辑代码,比如

  // $render_array['#markup'] .= views_embed_view('my_favorites', 'block', $user->uid);

  return $render_array;

}

 

    我们启用这个模块,访问http://localhost/thinkindrupal/my,看到如图4-14所示的效果。

图片1.png 

                 图 3-14.采用了本地任务的个人主页

 

注意,页面的标题来自于父回调函数,而不是来自于默认的本地任务。如果你想使用一个不同的标题,那么可以使用drupal_set_title()来单独设置它。


Drupal版本:

12 修改其它模块定义的菜单项

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

当Drupal重构menu_router表和更新menu_link表时(比如,当一个新模块被启用时),通过实现hook_menu_alter(),模块就可以修改任意的菜单项。例如,“我的帐户”菜单项通过调用user_page(),如果当前用户是登录用户,那么就显示用户的个人资料;如果是匿名用户,则显示登录表单。由于user_page ()函数位于modules/user/user.pages.inc中,所以该Drupal路径的菜单项中定义了file键。这样,通常情况下,当一个用户点击了用户菜单中的“我的帐户”链接,Drupal会加载文件modules/user/user.pages.inc并运行user_page ()函数。

 

    由于我们在my模块中,定义了自己的用户中心路径,我们希望当用户访问路径user的时候,能够重定向到我们新定义的个人主页。此时我可以通过hook_menu_alter钩子来修改Drupal系统自带的菜单项。在my.module文件中,添加以下内容:

 

 

/**

 * 实现 hook_menu_alter().

 */

function my_menu_alter(&$items){

  $items['user']['page callback'] = 'my_user_page';

  $items['user']['file'] = 'my.pages.inc';

  $items['user']['file path'] = drupal_get_path('module', 'my');

}

 

    向my.pages.inc中添加对应的回调函数:

/**

 * 菜单项user的回调函数.

 */

function my_user_page(){

  global $user;

  if($user->uid > 0){

    drupal_goto('my');

  }else{

    //drupal_goto('user/login');

    return drupal_get_form('user_login');

  }

}

 

在我们的hook_menu_alter()钩子函数运行以前,user路径的菜单项应该是这样的:

array(

    'title' => 'User account',

    'title callback' => 'user_menu_title',

    'page callback' => 'user_page',

    'access callback' => TRUE,

    'file' => 'user.pages.inc',

    'weight' => -10,

    'menu_name' => 'user-menu',

  );

当我们修改了它以后,就变成了这样:

array(

    'title' => 'User account',

    'title callback' => 'user_menu_title',

    'page callback' => 'my_user_page',

    'access callback' => TRUE,

    'file' => 'my.pages.inc',

    'file path' => drupal_get_path('module', 'my'),

    'weight' => -10,

    'menu_name' => 'user-menu',

  );

   清除缓存,当我们再次访问“我的帐户”链接时,系统就会重定向到我们新定义的页面。

 

    注意这里面,由于我们将my_user_page定义在了my.pages.inc中,所以此时我们还需要明确的定义$items['user']['file path'],否则系统就会默认的在modules/user目录下查找这个文件,并显示无法打开相应文件的错误信息。

图片1.png 

    3-15.注释掉 'file path'的定义信息,就会报错。

 


Drupal版本:

13 改变其它模块的菜单链接

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

如果我们前面的修改,还不能令客户满意,而此时又需要进一步的隐藏系统自带的用户菜单。那么有多种方法,我们这里介绍通过实现hook_menu_link_alter()钩子函数,在Drupal将一个菜单项保存到menu_link表时,修改对应链接。下面是如何将“我的帐户”、“登出”菜单项隐藏的。

 

/**

 * 实现 hook_menu_link_alter().

 */

function my_menu_link_alter(&$item){

  if($item['link_path'] == 'user'){

    $item['hidden'] = 1;

}

if($item['link_path'] == 'user/logout'){

    $item['hidden'] = 1;

}

}

这个钩子可以用来修改一个链接的多个属性,比如标题、重量、是否隐藏。如果你需要修改一个菜单项的其它属性的话,比如访问回调,那么需要使用hook_menu_alter()

图片1.png 

        3-16.用户菜单的链接已被禁用

 

    此时,在用户菜单的管理界面,我们选择启用,并保存设置,我们发现两个菜单项仍然是禁用的,也就是说hook_menu_link_alter()中对菜单项所做的修改,是无法在用户界面中再进行覆写的。


Drupal版本:

14 菜单项中的通配符

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

 

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

 

    我们为此单独创建一个模块menu_wildcard,首先创建menu_wildcard.info, menu_wildcard.module, menu_wildcard.pages.inc文件。然后向menu_wildcard.info中添加以下内容:

 

name = 菜单通配符

description = 用来学习菜单通配符的实例模块

core = 7.x

 

    接着向menu_wildcard.module文件添加以下代码:

<?php

 

/**

 * @file

 * 演示Drupal中菜单通配符的用法,

 */

 

 

/**

 * 实现 hook_menu().

 */

function menu_wildcard_menu() {

 

  $items['wildcard/%'] = array(

    'title' => '简单的通配符',

    'description' => '一个简单的包含通配符的菜单项.',

    'page callback' => 'menu_wildcard_callback_page',

'page arguments' => array(1),

'file' => 'menu_wildcard.pages.inc',

    'access callback' => TRUE,

  );


  return $items;

}

 

    最后向menu_wildcard.pages.inc文件中添加对应的回调函数:

<?php

 

/**

 * @file

 * menu_wildcard的各种回调函数,

 */

 

 

/**

 * 菜单项wildcard/%的回调函数.

 */

function menu_wildcard_callback_page($arg1 = '', $arg2 = '', $arg3 = '', $arg4 = ''){

  $render_array = array();

  $render_array['#markup'] = t('菜单通配符示例页面内容');

  $render_array['#markup'] .=  '<div>'.t('参数1:@arg1', array('@arg1' => $arg1)).'</div>';

  $render_array['#markup'] .=  '<div>'.t('参数2:@arg2', array('@arg2' => $arg2)).'</div>';

  $render_array['#markup'] .=  '<div>'.t('参数3:@arg3', array('@arg3' => $arg3)).'</div>';

  $render_array['#markup'] .=  '<div>'.t('参数4:@arg4', array('@arg4' => $arg4)).'</div>';

  return $render_array;

}

 


Drupal版本:

15 基本通配符

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

%字符在Drupal菜单项中是一个特殊的字符。它意味着“从这到下一个/字符之间的字符串”。上面是一个使用了通配符的菜单项。这个菜单项适用的Drupal路径可以有wildcard /a, wildcard/a/b, wildcard/88。但是它对路径wildcard不起作用;对于后者,因为它只包含了一个部分,而wildcard /%只匹配至少具有两部分的字符串,所以你需要为其单独创建一个菜单项。注意,尽管%通常是用来指定一个数字的(比如,user/%/edit用于user/1/edit),但是它能匹配该位置上的任何文本。

 

注意 在路径中带有通配符的菜单项,即便是将菜单项的类型设置为MENU_NORMAL_ITEM,它也不会显示在导航菜单中。原因很明显:由于路径中包含了一个通配符,所以Drupal不知道如何为该路径构建URL。这是一般情况下的规律,也有例外的情况,更多详细,可参看本章后面的“使用to_arg()函数为通配符构建路径”。

 

    我们访问路径wildcard/123,就会得到这样的结果:

      图片1.png

                 3-17.带有通配符的菜单项


Drupal版本:

16 使用通配符的值

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

我们新增一个菜单项:

function menu_wildcard_menu() {

 

  $items['wildcard/%'] = array(

    'title' => '简单的通配符',

    'description' => '一个简单的包含通配符的菜单项.',

    'page callback' => 'menu_wildcard_callback_page',

'page arguments' => array(1),

'file' => 'menu_wildcard.pages.inc',

    'access callback' => TRUE,

  );

  $items['wildcard/%/b'] = array(

    'title' => '普通的通配符',

    'description' => '一个普通的包含通配符的菜单项.',

    'page callback' => 'menu_wildcard_callback_page',

'page arguments' => array(1),

'file' => 'menu_wildcard.pages.inc',

    'access callback' => TRUE,

  );


  return $items;

}

    清空缓存后,让我们再次访问路径wildcard/123/b/c/d,得到如图3-19所示的结果。

 

图片1.png 

 3-19.通过URL只传递了两个参数c,d

 

第一个参数,123,是通过页面回调传递过来的。array(1)的意思是,“不管路径中的部分1是什么,请将它传递过来”。我们是从0开始算起的,所以部分0就是'wildcard ',部分1就是通配符所匹配的任何东西,部分2就是'b',依次类推。此时,b是Drupal路径中的一部分了,不再通过URL传递。后面的c,d,也被传递了过来,它的传递原理我们在前面已经学过了,那就是Drupal路径后面的一部分将会作为参数传递给回调函数,注意图3-18和图3-19之间的区别。


Drupal版本:

17 通配符、占位符、参数替换

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

 向我们的菜单钩子中添加一个新的菜单项:

  $items['placeholder/%menu_wildcard_arg_optional'] = array(

    'title' => '占位符示例',

    'description' => '一个通配符用作占位符的菜单项.',

    'page callback' => 'menu_wildcard_callback_page',

'page arguments' => array(1),

'file' => 'menu_wildcard.pages.inc',

    'access callback' => TRUE,

  );

 

    并在module文件中添加一个新的函数:

function menu_wildcard_arg_optional_load($id){

  $mapped_value = "";

  $mappings = array(

  'a' => "美国",

'b' => "英国",

'c' => "中国",

);

  if (isset($mappings[$id])) {

    $mapped_value = $mappings[$id];

  }

  if(empty($mapped_value)){

$mapped_value = t('当前ID @id 没有对应的值',array('@id' => $id));

  }

  return $mapped_value;

}

 

     清空缓存,让我们访问路径placeholder/a/b/c/d,得到图3-20所示的结果。

图片1.png 

          图 3-20.页面参数被_load()函数替换了

 

    参数a被替换成了“美国”,神奇吧!路经placeholder/%menu_wildcard_arg_optional是怎么一回事呢?我们在这里详细的解释一下:

 

    1.使用/字符将路径切分成各个部分。

    2.在第2部分中,匹配从%到下一个可能的/字符之间的字符串。在这里,该字符串就是menu_wildcard_arg_optional

    3. 向该字符串上追加_load,来生成一个函数的名字。在这里,该函数的名字就是menu_wildcard_arg_optional_load

    4. 调用该函数,并将Drupal路径中通配符的值作为参数传递给它。所以,如果Drupal路径为placeholder/a/b/c/d,那么通配符匹配的第2部分就是a,那么调用的就是menu_wildcard_arg_optional_load ('a')

    5. 使用这个调用所返回的结果来替换通配符。这里的页面参数为array(1),在页面回调被调用时,我们没有传递Drupal路径中的部分1(a),而是传递了menu_wildcard_arg_optional_load ('a')返回的结果,也就是“美国”。我们可以把它看作,Drupal路径中的一部分被它对应的_load()函数替换了。

    6. 注意,标题回调和访问控制回调也可以使用这种替换方式。

 

    在Drupal中,这种占位符形式的参数替换是非常常见的,比如最核心的node/%node,user/%user,就采用了这种方式。为了更好的理解这种替换,以node/%node/edit为例,我们可以把它看作是node/%/edit,外加了一个隐藏指令:为通配符匹配的内容运行node_load()


Drupal版本:

18 通配符和页面回调参数

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

    菜单路径中的通配符,不影响将URL中的额外部分作为参数传递给页面回调,这是因为通配符只匹配到下一个/字符。继续使用我们的wildcard/%路径作为例子,对于URL  wildcard/123/b/c/d,通配符所匹配的字符串就是123,而对于路径中的其余部分(b/c/d),它们将分别作为参数传递给页面回调。

图片1.png

             3-18.通配符的值和URL中的参数部分都传递了过来


Drupal版本:

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

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

如果需要向加载函数传递额外的参数,那么可以使用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, TRUE),

    'access callback' => '_node_revision_access',

    'access arguments' => array(1),

  );

菜单项为load arguments键指定了array(3)。这意味着,除了节点ID通配符的值会自动传递给加载函数以外,还会向加载函数传递一个额外的参数。因为array(3)里面有个元素;我们在“使用通配符的值”一节中已经讲过,这意味着将会使用路径中的部分3。当路径node/12/revisions/29/view被访问时,由于这里定义了load arguments键,这就意味着将会调用node_load('12', '29'),而不是node_load('12')了。

 

当页面回调运行时,加载函数会将'12'替换为加载了的节点对象,所以页面回调将会是node_show($node, TRUE)

 


Drupal版本:

20 特殊的,预定义的加载参数

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

有两个特殊的加载参数。%map令牌将当前Drupal路径作为数组进行传递。在前面的例子中,如果%map作为一个加载参数传递过来的话,那么其值就为array('node', '12', 'revisions', '29', 'view')。如果加载函数的参数是通过引用传递的话,那么加载函数可以修改%map中对应的值。%index令牌在加载函数中指的是通配符的位置。对于前面的例子,由于通配符的位置为1,如表4-2所示,所以该令牌的值就为1。

    例如,在modules/user/user.module中,有这样的菜单项:

 $items['user/%user_category/edit/' . $category['name']] = array(

          'title callback' => 'check_plain',

          'title arguments' => array($category['title']),

          'page callback' => 'drupal_get_form',

          'page arguments' => array('user_profile_form', 1, 3),

          'access callback' => isset($category['access callback']) ? $category['access callback'] : 'user_edit_access',

          'access arguments' => isset($category['access arguments']) ? $category['access arguments'] : array(1),

          'type' => MENU_LOCAL_TASK,

          'weight' => $category['weight'],

          'load arguments' => array('%map', '%index'),

          'tab_parent' => 'user/%/edit',

          'file' => 'user.pages.inc',

        );

    它对应的加载函数就是user_category_load($uid, &$map, $index)。这个菜单项向加载函数传递了参数array('%map', '%index')。如果用户通过路径'user/32/edit/foo'来编辑类别为foo的帐户信息,此时将会调用user_category_load函数,它的第一个参数的值为32,第2个参数的值为('user', 32, 'edit', 'foo'),第3个参数的值为1(也就是通配符所在的索引位置,注意是从0算起的)。user_category_load就会根据这些信息,把foo类别下的对应信息提取出来。


Drupal版本:

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

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

还记不记得前面我们曾经说过,对于包含通配符的Drupal路径,Drupal无法为其创建一个有效的链接,比如node/%(毕竟,Drupal怎么会知道如何替换%呢)?不过这一点并非完全正确。我们可以定义一个帮助函数,来为通配符参数提供一个默认值,这样,在Drupal构建链接时,就有路径可用了。向我们的模块的module文件中追加以下函数:

 

/**

 * to_arg()函数的实现

 */

function menu_wildcard_arg_optional_to_arg($arg){

  return (empty($arg) || $arg == '%') ? 'a' : $arg;

}

 

清除缓存,这样,链接占位符示例就会出现在导航区块中了。该链接的Drupal路径为placeholder/a

    to_arg()函数,最初应用于“我的帐户”这一链接上,但在Drupal7的正式版本中,“我的帐户”对应路径已被修改为了user,不带任何通配符。而与uid相关的信息则是从global $user中提取出来的。随着此处弃用了to_arg()函数,Drupal核心中,好像再也找不到实际的例子了。而这种方式,在实际的站点开发中,很少被用到。

 


Drupal版本:

22 Hook_menu的键值属性

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

    "title":必须的。菜单项未翻译的标题。

    "title callback": 用来生成标题的函数。默认为t()。如果你只想输出原始的字符串,那么可以将这个键设置为FALSE。

    "title arguments":传递给标题回调函数的参数

 

    "description":菜单项未翻译的描述。

 

    "page callback": 当用户访问这个路径时,为了显示页面内容,所需要调用的回调函数。如果忽略这个键值,那么将会使用父菜单项的回调函数。

 

    "page arguments":传递给页面回调函数的参数。

 

    "delivery callback": 把页面回调函数返回的结果打包并传递给浏览器,所调用的函数。默认为drupal_deliver_html_page(),也可以从父菜单项继承过来。注意,即便是没有通过访问控制检查,Drupal也会调用这个函数,注意在自定义交付回调函数(delivery callback)时,要考虑这一点。

 

    "access callback":访问控制回调函数,如果用户具有访问这个菜单项的权限,那么返回TRUE,否则返回FALSE。这里可以使用布尔值来代替函数,当然也可以使用数字(会自动转换为布尔值)。默认为user_access(),或者是从父菜单项继承过来;只有MENU_DEFAULT_LOCAL_TASK类型的菜单项才可以从父菜单项中继承。为了使用user_access(),你需要指定需要检查的权限。

 

    "access arguments": 传递给访问控制回调函数的参数。如果访问控制回调函数是继承过来的,那么访问控制参数也可以继承过来,除非在子菜单项中覆写访问控制参数。

 

    "theme callback": 这个函数用来返回呈现该页面所用主题的机读名字。如果没有提供,它的值将会从父菜单项中继承过来。如果没有主题回调函数,或者该函数没有返回站点的当前主题,那么这个页面所用的主题将由hook_custom_theme()决定,或者采用默认主题。通常只在非常特别的情况下,使用这个键,那就是这些页面的功能与特定主题完全绑定在了一起,此时只有采用hook_menu_alter()才能覆写这些页面。如果想要实现通用的主题切换功能,应该使用hook_custom_theme()钩子函数。

 

    "theme arguments": 传递给主题回调函数的参数。

 

    "file":在调用页面回调函数前,所需要加载的文件;它允许将页面回调函数放在单独的文件中,该文件应该存放在相对于当前模块的目录,也可以使用"file path"键指定文件所在的目录。这个键只适用于页面回调函数。

 

    "file path": 页面回调函数所在文件的目录所在。默认为当前模块所在的路径。

 

    "load arguments": 一个参数数组,用在页面参数后面,传递给路径中的每个通配符对象加载器。

 

    "weight": 一个整数,用来决定菜单项的相对位置。越重的菜单项越靠后。默认为0。重量相同的菜单项按字母顺序排序。

 

    "menu_name": 默认为导航菜单,如果你不想把菜单项放到这里,那么可以设置一个自定义的菜单。

 

    "context": 定义标签可以出现在的上下文。默认情况下,所有标签只在页面上下文中显示为了本地任务。如果想把这些标签作为上下文链接显示在页面区域容器中,那么需要使用下面的上下文:

 

    •MENU_CONTEXT_PAGE:对于页面上下文,标签被显示为本地任务。

    •MENU_CONTEXT_INLINE:在页面上下文外面,标签被显示为上下文链接。

    上下文可以联合使用,如果想把标签既显示成为本地任务,也显示为上下文链接,那么可以这样定义:

 

<?php

      'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,

 ?>

 

    "tab_parent":对于本地任务菜单项,本地任务的父菜单项的路径。比如'admin/people/create'的默认父菜单项为'admin/people'

 

    "tab_root": 对于本地任务菜单项,最近的非标签菜单项的路径;默认与"tab_parent"相同。

 

    "position": 这个条目对应区块在系统管理页面的位置('left' 或者 'right')

 

    "type": 菜单项的类型,可用值有MENU_NORMAL_ITEMMENU_CALLBACKMENU_SUGGESTED_ITEMMENU_LOCAL_ACTIONMENU_LOCAL_TASKMENU_DEFAULT_LOCAL_TASK。 如果忽略了"type",那么会使用默认的MENU_NORMAL_ITEM

 

    "options":在根据这个菜单项生成链接时,传递给l()函数的选项数组。


Drupal版本:

23 相关的钩子函数

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

 hook_menu_link_insert($link) 

    这个钩子用于通知模块,菜单项已经创建。

 

hook_menu_link_update($link) 

    这个钩子用于通知模块,菜单项已经更新。

 

hook_menu_link_delete($link) 

    这个钩子用于通知模块,菜单项已被删除。


Drupal版本:

23 菜单项的类型

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

    •MENU_NORMAL_ITEM: 普通的菜单项,显示在菜单树中,管理员可以对其进行管理。

 

    •MENU_CALLBACK: 回调函数简单的映射到一个路径上,不显示在菜单树中,当路径被访问时,调用对应的回调函数。

    •MENU_SUGGESTED_ITEM: 模块可以“建议”管理员启用的菜单项。

 

    •MENU_LOCAL_ACTION:本地动作是用来描述作用于父菜单项的动作,比如添加一个用户或者添加一个区块,它们在你的主题中呈现为动作链接。

 

    •MENU_LOCAL_TASK: 本地任务通常显示为标签,用于描述数据的不同显示。

 

    •MENU_DEFAULT_LOCAL_TASK: 每组本地任务中,必须提供一个默认的任务,它与父菜单项显示相同的内容。

 


Drupal版本:

24 总结

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

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

使用hook_menu定义自己的菜单项

理解访问控制的工作原理

理解如何在路径中使用通配符

创建带有标签(本地任务)的页面

通过代码来修改已有的菜单项

 

 


Drupal版本:

1创建一个菜单项

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在前面一章,学习过hook_menu这个钩子函数,通过这个钩子函数,我们可以定义自己的菜单项(回调映射)。我们现在编写一个简单的实例模块menu_abc.module,通过这个模块来学习菜单API。首先让我们在sites/all/modules/custom目录下面创建一个文件夹menu_abc,然后创建两个空白文件,分别为menu_abc.info、 menu_abc.module,让我们向menu_abc.info文件中添加一下内容:


name = 菜单ABC

description = 用来学习菜单API的简单实例模块

core = 7.x

 

    记得将文件的格式保存为UTF-8的形式,在以后的模块中,就不再提醒这一点了。

 

接着打开sites/all/modules/custom/menu_abc/menu_abc.module文件,在里面添加hook_menu()的实现,以及对应的回调函数:

 

<?php

 

/**

 * @file

 * 演示Drupal中菜单API的基本用法,主要包括钩子hook_menu(),

 */

 

/**

 * 实现 hook_menu().

 */

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

    'access callback' => TRUE,

  );

 

   return $items;

}

 

/**

 * 菜单项menu_abc的回调函数.

 */

function menu_abc_callback_page(){

  $render_array = array();

  $render_array['#markup'] = t('菜单ABC页面内容');

  return $render_array;

}

 

在“管理模块”中,启用这个模块,这样Drupal就会将菜单项menu_abc保存到数据库menu_router表中,当我们访问http://localhost/thinkindrupal/menu_abc时,这里我启用了简洁URL,Drupal就可以找到这个菜单项并调用对应的回调函数了,如图3-1所示。

1.png

            图3-1  访问菜单项menu_abc后,Drupal显示的页面内容

 

    需要注意的要点是,这里我们定义了一个路径,并将其映射到了一个函数上。该路径是一个Drupal路径。我们使用它作为$items数组的键。你还会注意到这个路径的名字和模块的名字是一样的,这里主要是用来保证有一个干净的URL命名空间。实际上,你可以在hook_menu定义各种有效的路径。

 

    让我们来学习一下,这个菜单项数组里面所包含的每一项的含义。

    'title':是用来定义菜单项的标题的,这里定义为'菜单ABC',当在浏览器中显示该页面时,它会自动用作页面标题。如果我们需要在后面的回调函数中覆写页面标题,那么可以使用drupal_set_title()函数。此外,我们没有在这里使用t()函数。这是因为对于菜单项的title来说,系统会自动地调用t()。

   'description':是这个菜单项的简单描述,这里定义为'一个简单的菜单项.',当我们把鼠标移到右边导航区块的对应链接时,这一文本会显示出来,如图3-2所示。

 

                       2.png

                   图3-2 菜单项在导航区块中的显示

 

    'page callback': 定义了这个菜单项的页面回调函数,这里定义为' menu_abc_callback_page',当用户访问路径”menu_abc”时,Drupal就会执行函数menu_abc_callback_page。对于这个函数,我们需要注意的是,它返回的是一个数组结构,Drupal会自动地将这个数组结构呈现为页面内容。对于学习过Drupal6开发的用户来说,我们知道,通常在页面回调函数中,我们返回字符串;在Drupal7中,返回字符串也是可以的,但是最好返回数组结构的形式。上述代码等价于:

 

  $output = '';

  $output = t('菜单ABC页面内容');

  return $output;

 

    'access callback':定义了这个菜单项的访问控制回调函数,这里定义为TRUE,表示所有用户都可以绕过访问控制检查,也就是所有用户都可以正常的访问这个路径。

 

    尽管在前面的菜单项中没有定义,但实际却用到的是'type''menu_name' ' weight ''type'定义了这个菜单项的类型,这里我们使用了默认的MENU_NORMAL_ITEM,所以在这里的代码里,type键可被忽略。'menu_name'定义了这个菜单项所属的菜单,这里我们使用了默认的'navigation',这样我们这里定义的菜单项,就会显示在导航区块中了。' weight '定义了这个菜单项在菜单中的位置,由于这里我们没有定义它,所以使用了默认值0。


Drupal版本:

2调整菜单项的位置

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

如果我们觉得这个菜单项的放置位置不合适,那么可以使用' weight '来进行相应的调整,增加菜单项的重量,可以使它向下移动;减少菜单项的重量,可以使它向上移动。

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

    'access callback' => TRUE,

'weight' => 10,

  );

 

   return $items;

 

    我们调整了代码以后,刷新页面,并没有看到任何变化。为什么呢?这是因为Drupal将所有的菜单项存储在了menu_router表中,尽管这里我们的代码改动了,但是数据库还没有变。我们需要告诉Drupal重新构建menu_router表。此时我们需要导航到“管理 〉 配置 〉 开发 〉 性能”页面,也就是admin/config/development/performance,点击“清空所有缓存”按钮。这样我们就能看到菜单项位置的变化了,

 

                       1.png

                       图3-3 调整菜单项重量后的导航区块

 

 

    我们调整重量后的效果,如图3-3所示。我们也可以使用菜单模块提供的可视化操作界面,来调整菜单项之间的相对顺序,这样我们就不需要修改模块中的代码了。

 


Drupal版本:

3调整菜单项所属的菜单

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

如果我们不想把这个菜单项放置在导航区块中,而是向放置在主菜单里面,此时我们可以这样:

 

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

    'access callback' => TRUE,

'weight' => 10,

'menu_name' => 'main-menu',

  );

 

   return $items;

}

    清空缓存数据,得到如图所示的效果

           1.png

                   图3-4 把菜单项显示在主菜单中

 


Drupal版本:

4不在菜单中显示菜单项

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

大多数时候,我们可能想将一个URL映射到一个函数上,而不需要创建一个可见的菜单项。例如,你可能在web表单中有一个JavaScript函数,它需要从特定Drupal路径中得到相应的数据,此时可以将这个URL映射到一个回调函数上,而不需要将它放到菜单中。通过将菜单项的类型指定为MENU_CALLBACK,便可轻松实现这一点。

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

    'access callback' => TRUE,

'weight' => 10,

    'type => MENU_CALLBACK,

//'menu_name' => 'main-menu',

  );

 

   return $items;

}


Drupal版本:

5把页面回调放在inc文件中

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

如果我们没有特别指定,那么Drupal会假定我们把页面回调函数放在了module文件中。在Drupal7中,对于每个页面请求,系统通常都会加载所有的module文件,为了尽可能的降低所加载的module文件大小,我们可以把很多回调函数放置在inc文件中。可以使用菜单项中的file键,来指定哪个文件包含了它的回调函数,这样回调函数就不需要放在当前的module文件中了。我们在前面一章中,就曾提到过file键。

 

如果我们定义了file键,那么Drupal默认将会在当前模块目录下查找该文件。如果页面回调函数是由其它模块提供的,也就是说该文件不在当前模块目录中,那么我们需要告诉Drupal在查找该文件时所用的文件路径,这里使用file path键,就可以轻松的实现这一点了。我们在前面一章中,就曾用到过file path键。

 

   我们新建一个menu_abc.pages.inc文件,将我们的回调函数剪切过去,然后修改菜单项中的代码,添加file键。

 

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => TRUE,

'weight' => 10,

'menu_name' => 'main-menu',

  );

 

   return $items;

}

    保存好module文件和inc文件后,清空缓存数据,我们看到了相同的结果。如果我们修改inc文件中的返回内容,我们会看到相应的变化。此时回调函数已经放在inc文件中了。


Drupal版本:

6访问控制

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

到目前为止,在前面的例子中,我们简单的将菜单项的access callback键设置为了TRUE,这意味着所有的用户都可以访问我们的菜单项。一般情况下,通过在模块中使用hook_ permission ()来定义权限,并使用一个函数来检查这些权限,从而实现对菜单的访问控制。这里所用的函数定义在菜单项的access callback键中,一般使用user_access。让我们定义一个名为access abc的权限;如果用户所在角色不具有该权限,当他访问页面http://localhost/thinkindrupal/menu_abc时,就会看到一个“拒绝访问”提示。

 

/**

 * 实现 hook_permission().

 */

function menu_abc_permission() {

  $perms = array(

    'access abc' => array(

      'title' => t('访问菜单ABC示例页面'),

    ),

  );

 

  return $perms;

}

 

/**

 * 实现 hook_menu().

 */

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => 'user_access',

'access arguments' => array('access abc'),

'weight' => 10,

'menu_name' => 'main-menu',

  );

 

   return $items;

}

 

    在这里,我们首先实现了hook_ permission这个钩子函数,在这个钩子函数中,我们定义了“访问菜单ABC示例页面”的权限。注意这里权限的定义,也是采用的数组的形式。'access abc'是这个数组的键,'title'表示这个权限的名字。

1.png 

              图3-5  我们定义的权限,显示在了权限列表页

在前面的代码中,根据函数user_access('access abc')的返回结果,来判定是否允许用户访问的。现在,菜单系统就相当于一个门卫,hook_permission就相当于各种出入证,当用户访问特定路径时,需要提供相应的出入证明,没有有效的身份证明,就会被负责任的门卫遣返回家。

   2.png

        图3-6  匿名用户访问该页面,得到的提示

    user_access()函数是默认的访问回调。如果你没有定义访问回调的话,那么访问参数将被菜单系统传递给user_access()

 


Drupal版本:

7标题的本地化和定制

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

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

    'title' =>t( '菜单ABC'),

 

然而,这样定义就错了。菜单字符串是以原始字符串的形式存储在menu_router表中的,而菜单项的翻译则被推迟到了运行时。Drupal会自动的调用t()函数,用来翻译菜单项的标题。这个t()函数,就是默认的标题回调函数(title callback)。我们接下来会看到,如何将默认的t()函数修改为自定义的函数,以及如何向标题回调函数传递参数。


Drupal版本:

8定义标题回调

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

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

 

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

'title callback' => 't',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => 'user_access',

'access arguments' => array('access abc'),

'weight' => 10,

'menu_name' => 'main-menu',

  );

 

   return $items;

}

注意 不管title callback键的值如何,description键总是使用t()函数来翻译的。描述在这里没有对应的回调函数键。

 

如果我们把标题回调改为自己的函数,那会是什么样子呢?让我们先看看吧:

 

/**

 * 实现 hook_menu().

 */

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

'title callback' => 'menu_abc_my_title',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => 'user_access',

'access arguments' => array('access abc'),

'weight' => 10,

'menu_name' => 'main-menu',

  );

 

   return $items;

}

 

/**

 * 页面回调.

 */

function menu_abc_my_title(){

  global $user;

  $title = $user->name.t('的主页');

  return $title;

}

如图3-7所示,通过使用一个自定义的标题回调,就可以实现,在运行时设置菜单项标题了。

1.png

                图 3-7.标题回调设定了菜单项的标题

 

 

    但是,如果菜单项的标题和页面标题不一样时,那该怎么办呢?这个实现起来也不难,我们可以使用drupal_set_title()来单独的设置页面标题:

 

function menu_abc_my_title(){

  global $user;

  drupal_set_title(t('菜单ABC标题'));

  $title = $user->name.t('的主页');

  return $title;

}

这样就将页面标题和菜单项的标题分离了开来,如图3-8所示。

2.png 

                 图 3-8将菜单项的标题和页面标题独立开来


Drupal版本:

9菜单嵌套

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

到目前为止,我们仅仅定义了一个静态菜单项。让我们再添加一个与它相关的子项:

 

function menu_abc_menu() {

 

  $items['menu_abc'] = array(

    'title' => '菜单ABC',

'title callback' => 'menu_abc_my_title',

    'description' => '一个简单的菜单项.',

    'page callback' => 'menu_abc_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => 'user_access',

'access arguments' => array('access abc'),

'weight' => 10,

'menu_name' => 'main-menu',

  );


  $items['menu_abc/sub'] = array(

    'title' => '菜单ABC子项',

    'description' => '菜单ABC的子项.',

    'page callback' => 'menu_abc_sub_callback_page',

'file' => 'menu_abc.pages.inc',

    'access callback' => TRUE,

'weight' => 10,

'menu_name' => 'main-menu',

  );


  return $items;

}

 

    然后向menu_abc.pages.inc文件中添加以下代码:

/**

 * 菜单项menu_abc/sub的回调函数.

 */

function menu_abc_sub_callback_page(){

  $render_array = array();

  $render_array['#markup'] = t('菜单ABC子页面内容');

  return $render_array;

}

Drupal将会把第2个菜单项(menu_abc/sub)看作是第一个菜单(menu_abc)的孩子。因此,我们导航到主菜单的管理界面,在显示菜单项时,Drupal将会缩进第2个菜单项,如图3-9所示。

 

图片1.png 

 3-9.嵌套菜单

    Drupal还在页面的正文上面,正确的设置了面包屑,用来表示页面之间的嵌套关系。当然,根据设计的要求,可在主题层将菜单或面包屑定义成所要的各种样式。

图片2.png

 3-10子菜单项页面和及其面包屑

 


Drupal版本:

21 关联

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

    为了关联另一个数据库表,我们可以使用方法join()innerJoin()leftJoin()、或rightJoin(),下面的代码是一个具体示例:

<?php
$table_alias $query->join('user''u''n.uid = u.uid AND u.uid = :uid', array(':uid' => 5));
?>

    上述指令,将会对"user"表使用INNER JOIN(默认的关联类型),这里"user"表的别名为"u"。关联的条件为" n.uid = u.uid AND u.uid = :uid",其中:uid的值为5。注意,这里预备语句(prepared statement)片断的具体用法。采用这种方式,在关联语句中添加变量,就可以避免潜在的SQL注入了。即便是对于查询语句片断,也不要直接在里面使用字面值或者变量,这和静态查询中,不能使用字面值或者变量,性质是一样的。innerJoin()leftJoin()rightJoin()对应于各自的关联类型,除此以外,它们的用法完全相同。

    关联方法的返回值是对应表的别名。如果指定了别名,那么就会使用这个别名,除非这个别名已被其它表使用。在这种情况下,系统将会为其分配一个不同的别名。

    注意,在表名的位置上,比如上例中的'user'位置,所有的关联方法都可以接收一个选择查询作为它们的第一个参数。例如:

<?php
$myselect db_select('mytable')
  ->fields('mytable')
  ->condition('myfield''myvalue');
$alias $query->join($myselect'myalias''n.nid = myalias.nid');
?>


Drupal版本: