Drupal的菜单系统复杂但是强大,术语“菜单系统”(menu system)有些取名不当,最好认为菜单系统有三个主要职责:回调映射、访问控制、菜单定制。菜单系统基本的代码在includes/menu.inc中,那些可选代码包含一些定制菜单时激活的特性则在modules/menu中。 在本章,我们将探索回调映射怎样工作,看看怎样生成带访问控制的菜单项、总结菜单项的不同内建类型。最后以练习如何去覆写、增加、删除存在的菜单项来结束本章,你就能尽可能无干扰地定制Drupal了。
回调映射
当一个web浏览器对Drupal发出一个请求,它给Drupal一个URL,从这个信息,Drupal可以算出什么代码要运行和怎样处理这个请求,这是通常都知道的路由和调度。Drupal修剪掉URL的基本部分,使用后面的部分,叫路径(path),例如,如果URL是http://example.com?q=node/3,Drupal路径是node/3。如果你是使用Drupal的cleanURL特性,在你的浏览器中,此URL就显示为http://example.com/node/3,但是你的web服务器在Drupal看到它之前悄悄重写URL到http://example.com?q=node/3,所以Drupal总是处理相同的Drupal路径,在前面的例子中,Drupal路径不论是否是CleanURL都是node/3,如何工作的更多细节请看第一章“The Web Server's Role”节。
映射URL到函数
通常的途径是这样:Drupal询问所有激活的模块去提供一个菜单项的数组,每个菜单项包含一个以一个路径为键及有关路径某些信息的数组,一个模块应提供的这一块信息是一个页面回调(page callback),在这个上下文中,一个回调简单点就是浏览器请求特定路径时应该调用的PHP函数的名字。一个请求到达时,Drupal将经历如下步骤: 1. 构建Drupal路径。如果Drupal路径是一个到真实路径的别名,Drupal找到真实路径并用它来替代。例如,如果管理员用http://example.com/?q=about代替http://example.com?q=node/3,Drupal使用node/3作为路径。 2. Drupal在数据库表menu_router中保持跟踪哪个路径映射到哪个回调,并在数据库表menu_likes中保持跟踪哪个菜单项是链接。有一个检查去看menu_router和menu_links表是否需要重建,在Drupal安装或更新后罕有发生。 3. 算出menu_router表中的哪一项符合这个Drupal路径并创建一个路径项描述要调用的回调。 4. 载入任何需要传递给回调的对象。 5. 检查用户是否有访问这个回调的权限,如果没有,返回一个“访问拒绝”消息。 6、对菜单标题和描述本地化。 7. 载入任何需要包含的文件。 8. 调用回调并返回结果,那个index.php传递到theme_page(),结果是最终页面。 这个流程如图4-1和4-2:
Figure 4-1. Overview of the menu dispatching process
Figure 4-2. Overview of the router and link building process
创建一个菜单项
为创建一个菜单项,我们使用hook_menu(),hook_menu()持有一个由准备附加到一个菜单的项目组成的数组,每一个项目自己就是一个键值对组成的数组,描述菜单项的属性,下表描述了菜单项数组各键的细节:
Key Value title 一个必须字段,菜单项未翻译title title callback 一个生成title的函数,默认是t(),因为这个原因,我们不把前面的title放在t()函数中,如果你不想翻译,就简单设置此项为FALSE。 description 菜单项的未翻译描述 page callback 当用户浏览此路径时调用的函数,用来显示web页面 page arguments 要传递给页面回调函数的参数的数组;整型值传递匹配的URL成分 access callback 一个函数,返回一个布尔值,判定用户是否有访问该菜单项的权限;默认为user_access(),除非从父菜单项继承一个值 access arguments 传递给访问回调函数的参数数组;整型值传递匹配的URL成分 file 回调访问之前将要包含的文件;它允许回调函数处于一个单独的文件中。文件应该相对于实现模块路径,除非用“file path”选项另外指定其他值 file path 指向包含file指定的文件所在文件夹的路径,默认为实现hook的模块路径 weight 一个整数值,决定项目在菜单中的相对位置;值高的项目在下面,默认为0 menu_name 可选,设置一个自定义菜单,如果你的项目不想放到导航菜单上 type 一个标志的位掩码,描述菜单项的特性,值可以使下面这些: MENU_NORMAL_ITEM 正常菜单项,可以在菜单树上展示,并能被管理员移动/隐藏 MENU_CALLBACK 回调简单地注册一个路径,URL访问时恰当的函数将被触发 MENU_SUGGESTED_ITEM 模块可以“启发”菜单项,管理员可以激活 MENU_LOCAL_TASK 本地任务,默认渲染成选项卡 MENU_DEFAULT_LOCAL_TASK 每一个本地任务集合都要提供个默认的任务,点击它是就像点击父项连接到相同路径
钩进处理过程的最好地方是在你的模块中使用菜单钩子,它允许你去定义菜单项,这些菜单项将被包含进路由表。让我们创建一个简单的叫menufun.module的模块,实验菜单系统,我们映射Drupal路径menufun到我们要写的名叫menufun_hello()的PHP函数,首先,我们需要在sites/all/modules/custom/menufun中增加menufun.info:
name = Menu Fun description = Learning about the menu system. package = Pro Drupal Development core = 7.x files[] = menufun.module
然后我们需要建立sites/all/modules/custom/menufun/menufun.module文件,它包含我们实现的菜单钩子和我们要执行的函数:
<?php /** * @file * Use this module to learn about Drupal's menu system. */ /** * Implementation of hook_menu() */ function menufun_menu() { $items['menufun'] = array( 'title' => 'Greeting', 'page callback' => 'menufun_hello', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; }
在前面的代码中,你可以看见我们用3个键值对数组作为项建立我的菜单($items['menufun']): “title”:必需,定义未翻译的菜单title “page callback”:用户访问菜单路径是要调用的函数 “access callback”:典型地包含一个返回布尔值的函数
/** * Page callback */ function menufun_hello() { return t('Hello!'); }
激活这个模块,菜单项将被插入路由表,那么当访问http://example.com?q=menufun时,Drupal就找到并允许我们的函数,展示如图4-3。 需要注意的重要的东西是我们是定义了一个路径并映射它到一个函数,此路径是Drupal路径。我们将路径定义为我们的$items数组的键值,我们使用的路径的名字与我们的模块名相同,这个实用技术保证了原始URL的命名空间,当然,你能定义任意路径。
Figure 4-3. The menu item has enabled Drupal to find and run the menufun_hello() function.
Page Callback参数
有时,你可能希望提供更多的信息到映射路径的页面回调函数,首先,路径任何附加的部分都是自动向前传递,让我们像下面一样改变我们的函数:
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-4的情形:
Figure 4-4. Parts of the path are passed along to the callback function.
注意每一个URL扩展成分是怎样作为参数传递给我们的回调函数的。 你还能在菜单钩子中通过给$items数组附加一个可选的page arguments键来定义页面回调参数,定义页面参数比较常用是因为它允许你在传递给回调函数的参数上增加更多控制。 例如,让我们更新我们的menufun模块,为我们的菜单项增加page arguments:
function menufun_menu() { $items['menufun'] = array( 'title' => 'Greeting', 'page callback' => 'menufun_hello', 'page arguments' => array('Jane', 'Doe'), 'access callback => TRUE, 'type' => NEMU_CALLBACK, ); return $times; }
而后Drupal有明确给出页面参数给出的指令,任何剩余的路径参数是传递给回调函数但没获得解释,作为额外的参数,使用PHP参数重写机制。从URL来的参数还是可用的,为访问它们,你应当重写你的回调函数声明来增加URL参数。你修改了菜单使下列函数声明$first_name结果是Jane(第一个参数)$last_name是Doe(第二个参数)。
function menufun_hello($first_name = '', $last_name = '') { ... }让我们测试在页面参数和URL中输入Jane Doe的情形,进入http://example.com/?q=menufun/John/Doe将展现如图4-5的情形(如果不是这样,你就是忘了重建菜单) Figure 4-5. Passing and displaying arguments to the callback function 如果你想使用URL传来的值,你要用下面的值更新页面回调函数:
function menufun_hello($first_name = '', $last_name = '') { return t('Hello @first_name @last_name from @from_first_name @from_last_name', array('@first_name' => $first_name, '@last_name => $last_name)); }更新你的版本,清空缓存,使用http://example.com/?menufun 访问来看看情况。
在另外的文件中页面回调
如果你没有指明另外情况,Drupal假设你的页面回调能在你的.module文件中找到,许多模块都是被分割成多个文件,以使在每个页面请求时按条件载入不常用的文件,菜单项file键(如'file => 'menu_geetings.inc')用来指明包含回调函数的文件名。 作为例子,我要更新menufun.module hook_menu()函数去包含里面有回调函数的文件的名字。下列代码增加了'file' => 'menufun_greetings'到菜单项数组,我还改变页面回调到menufun_greeting,只是演示,此回调不使用已经存在于menufun.module中的函数。/** * Implementation of hook_menu(). */ function menufun_menu() { $items['menufun'] = array( 'title' => 'Menu Fun', 'page callback' => 'menufun_greeting', 'file' => 'menufun_greeting.inc', 'page arguments' => array('Jane', 'Doe'), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; }下一步我们将在menufun目录建立名为menufun_greeting.inc,代码是:
<?php function menufun_greeting($first_name = '', $last_name = '', $from_first_name='', $from_last_name='') { return t('Hello @first_name @last_name from @from_first_name @from_last_name', array('@first_name' => $first_name, '@last_name' => $last_name, '@from_first_name' => $from_first_name, '@from_last_name' => $from_last_name)); }保存这两个文件,清除缓存,测试改变后的情况,你应该能得到相同的结果,只是执行时间上扩大了调用.module的时间。
在导航区块增加一个连接
在菜单例子中,我们声明菜单项的类型是MENU_CALLBACK,现在改成MENU_NORMAL_ITEM,我们指明我们不想简单地将路径映射到回调函数,我们想让Drupal去将它包含进一个菜单。function menufun_menu() { $items['menufun'] = array( 'title' => 'Menu Fun', 'page callback' => 'menufun_greeting', 'file' => 'menufun_greeting.inc', 'page arguments' => array('Jane', 'Doe'), 'access callback' => TRUE, 'type' => MENU_NORMAL_ITEM, ); return $items; }现在菜单项目应该展示到了导航区块上,如图4-6: Figure 4-6. The menu item appears in the navigation block. 如果你不想将它放到那个地方,你可以增加或减少它的weight值,weight是菜单定义项目中的另一个键。
function menufun_menu() { $items['menufun'] = array( 'title' => 'Menu Fun', 'page callback' => 'menufun_greeting', 'file' => 'menufun_greeting.inc', 'page arguments' => array('Jane', 'Doe'), 'access callback' => TRUE, 'weight' => -1, 'type' => MENU_NORMAL_ITEM, ); return $items; }效果如同图4-7,菜单项可以不用改变代码而使用管理工具重排位置,Structure->Menus(菜单模块应该激活)。 Figure 4-7. Heavier menu items sink down in the navigation block.
菜单嵌套
目前为止,我们只定义了一个单个静态的菜单项,让我们用下面代码增加第二个和其它的回调:function menufun_menu() { $items['menufun'] = array( 'title' => 'Menu Fun', 'page callback' => 'menufun_greeting', 'file' => 'menufun_greeting.inc', 'page arguments' => array('Jane', 'Doe'), 'access callback' => TRUE, 'type' => MENU_NORMAL_ITEM, 'weight' => '-1', ); $items['menufun/farewell'] = array( 'title' => 'Farewell', 'page callback' => 'menufun_farewell', 'file' => 'menufun_greeting.inc', 'access callback' => TRUE, 'type' => MENU_NORMAL_ITEM, ); return $items; }下一步,在menufun_greeting.inc文件中增加如下的页面回调函数menufun_farewell:
function menufun_farewell() { return t('Goodbye'); }更新模块后,别忘记清除缓存。 Drupal将通知第二个菜单项(menufun/farewell)路径是第一个菜单项(menufun)的子路径。因此,当渲染此菜单页面时,Drupal将指向第二个菜单,就像图4-8,它也明确地设置页面顶部的面包屑踪迹来指示菜单嵌套,当然,一个主题可以将菜单和面包屑渲染成设计者需要的样式。 Figure 4-8. Nested menu
访问控制
到目前为止,我们简单地将access callback键设置成TRUE,这意味着任何人都能访问我们的菜单,通常菜单访问是通过在模块内使用hook_permission()来定义权限来控制的,并使用一个函数来测试他们,函数名用于定义菜单项的access callback键,典型是user_access。让我们定义一个权限叫做 receive greeting,如果用户的规则不允许这个权限,那么他将在访问http://example.com/?q=menufun时收到“access denied”消息。/** * Implementation of hook_permission() */ function menufun_permission() { return array( 'receive greeting' => array( 'title' => t('Receive a greeting'), 'description' => t('Allow users receive a greeting message'), ), ); } /** * Implementation of hook_menu(). */ function menufun_menu() { $items['menufun'] = array( 'title' => 'Menu Fun', 'page callback' => 'menufun_greeting', 'file' => 'menufun_greeting.inc', 'page arguments' => array('Jane', 'Doe'), 'access callback' => 'user_access', 'access arguments' => array('receive greeting'), 'type' => MENU_NORMAL_ITEM, 'weight' => '-1', ); $items['menufun/farewell'] = array( 'title' => 'Farewell', 'page callback' => 'menufun_farewell', 'file' => 'menufun_greeting.inc', 'access callback' => 'user_access', 'access arguments' => array('receive greeting'), 'type' => MENU_NORMAL_ITEM, ); return $items; }在前面的代码中,范文将被调用user_access('receive greeting')的结果决定,在这种方式下,菜单系统服务就像个看门人,基于用户规则决定哪个路径可以访问、哪个拒绝访问。 TIP:user_access()函数是默认的访问回调,如果你不定义访问回调函数,你的访问参数将有菜单系统传递给user_access()函数。 子菜单项不从它的父菜单项继承访问回调和访问参数。access argument键应该在每个菜单项都定义,access callback键只在其与user_access不同的时候定义,例外情况是当菜单项的类型是MENU_DEFAULT_LOCAL_TASK的时候,它将继承父项的access callback和access arguments,所以为清晰,最好明确地定义这些键,即使是默认本地任务。
本地化和定制标题
菜单title有两种,静态和动态,静态标题有分配title键的值建立的,动态标题通过一个title回调来建立的。Drupal自动翻译静态标题,所以你不要把它封装到t()函数中,如果你用动态标题,你必须负责在你的回调中做翻译工作。'title' => t('Greeting') // 不要这样做。注意:description永远都是静态的,值是由description键设定,由Drupal自动翻译。
定义标题回调
标题可以在运行时用标题回调动态创建,下面例子演示使用标题回调将标题设置成当前日期和时间,然后我用一个标题回调,此函数负责在返回值之前执行翻译,为执行翻译,我们将要返回的值封装在t()中。functionmenufun_menu() { $items['menufun'] = array( 'title' => 'Greeting', 'title_calback' => '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-9,菜单项标题的设置可以在运行时通过使用一个定制的标题回调来完成。 Figure 4-9. Title callback setting the title of a menu item 但是你想分隔菜单项标题和页面标题怎么办?简单 -- 你可用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。 Figure 4-10. Separate titles for the menu item and the page
菜单项通配符
到目前为止,我们在我们的菜单项中始终使用正规的Drupal路径名称,名称如menufun和menufun/farewell,但是Drupal经常使用user/4/track或node/15/edit等等,路径的某部分是动态的,让我们看看它是怎样工作的。基本通配符
字符 % 是Drupal菜单项中的通配符,意味着这个值是在运行时由在URL的通配符位置找到的值决定,这是一个使用通配符的菜单项:function menufun_menu() { $items['menufun/%'] = array( 'title' => 'Hi', 'page callback' => 'menufun_hello', 'page arguments' => array(1), 'access callback' => TRUE, ); return $items; }此菜单项对于Drupal路径menufun/hi menufun/foo/bar menufun/123和menufun/file.html都可以工作,但是它对路径menufun无效。菜单的路径应该分隔开来写,使它只包含路径的一部分,通配符只匹配两部分之中的一个字符串。注意尽管%通常设计成匹配一个数字(如user/%/edit匹配user/2375/edit),但实际上它匹配位置上的任何文本。 注意:带有通配符的菜单项将不再显示在导航菜单中,即使是菜单项type设置成为MENU_NORMAL_ITEM,原因显而易见,因为带有通配符的路径结构Drupal不知道怎样为这个链接构建URL,但是请看后面章节“Building Paths from Wildcards Using to_arg()函数”找到怎样告诉Drupal来使用URL。
通配符和页面回调参数
在菜单路径结尾处的通配符不影响传递给页面回调的URL的附加部分,因为通配符只是匹配到下一个斜杠为止,继续我们的例子路径menufun/%,URL http://example.com/?q=menufun/foo/Fred只有字符串foo匹配通配符,路径的下一个位置(Fred)将被作为一个参数传递给页面回调。使用通配符的值
要使用路径匹配的部分,需指定在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($name = NULL) { return t('Hello. $name is @name', array('@name' => $name)); }页面回调函数menufun_hello()接收的参数展示在图4-11中: Figure 4-11. The first parameter is from the matched wildcard. 第一个参数$name将被传给页面回调,array(1)意味着“请将路径的部分1传递,不管它是什么”,我们从0开始算,部分0是menufun、部分1是通配符匹配的、部分2是bar等等,第二个参数$b也将被传递,因为Drupal传递路径的组成部分优先于Drupal传递路径参数(请看Page Callback Argument)。
通配符和参数替换
在实际应用中,Drupal路径成分通常用来去查看和改变一个诸如节点或用户等对象,例如,路径node/%/edit用来去编辑node,路径user/%用来去查看关于此用户ID的信息。让我们注视一下后面的菜单项,那个能在modules/user/user.module中找到hook_menu()的实现,这路径相应的URL匹配的应该像http://example.com/?q=user/2375这样的,那是你可能在Drupal页面点击“我的账户”时的URL:
$items['user/%user_uid_only_optional'] = array( 'title' => 'My account', 'title callbcak' => 'user_page_title', 'title arguments' => array(1), 'page callback' => 'user_view_page', 'page arguments' => array(1), 'access callback' => 'user_view_access', 'access argument' => array(1), 'weight' => -10, 'menu_name' => 'user-menu', );
当Drupal用user/%user_uid_only_optional创建菜单,它用下列描述的程序替换%user_uid_only_optional: 1. 在第二段,匹配%后面和下一个可能的/之前的字符串,在此,字符串为user_uid_optional。 2. 附加一个_load到字符串尾部生成函数名,在此,函数名为user_uid_optional_load。 3. 调用这个函数并传递一个参数,Drupal路径中的通配符的值。如URL是http://example.com/?q=user=2375, Drupal路径是user/2375,通配符匹配第二个段就是2375,一个就是user_uid_optional_load('2375')。 4. 调用的结果然后用在通配符位置,然后标题回调使用array(1)作为参数来调用, 代替Drupal(2375)路径的部分1,我们传递user_uid_optional_load('2375')的调用结果,它是个 用户对象,想象一下它是个Drupal路径的部分将用它描述的对象来代替。 5. 注意页面和访问回调也要使用替换对象。那么在前面的菜单项,user_view_access()将为访问调用 user_view()将被调用生成页面内容,并且两个都要被传递用户2375的对象。
TIP:思考关于在一个Drupal路径如node/%node/edit中对象替换是容易的,如果年想象%node成一个带有注释的通配符,就是说node/%node/edit就是node/%/edit,带有暗示的就够来在通配符匹配上运行node_load()。
给载入函数传递附加参数
如果有附加的参数要传递给载入函数,它们可以被定义在load arguments键中。下面是一个节点例子,这个菜单项查看一个节点版本,节点ID和节点版本ID都要传递给载入函数node_load()。
$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), );
菜单项指定array(3)给load arguments键,这意味着为附加nodeID附加通配符的值,它自动传到给载入函数,就像先前的轮廓,一个附加的参数呗传到载入函数,而后array(3)成为一个成员,在此,就是数字3,就像你在“Using the Value of a Wildcard”节你看到的,这意味着路径的第三位置部分将被使用,此位置和路径参数例子URL http://example.com/?q=node/56/revision/4/view在下表中展示: --------------------------------------------------------- Position Argument Value from URL --------------------------------------------------------- 0 node node 1 %node 56 2 revisions revisions 3 % 4 4 view view ---------------------------------------------------------
因此,定义 load argument 键意味着调用node_load('56', '4')将替代node_node('56')。 当页面回调运行时,载入函数将用在如的节点对象替换56,因此页面回调是node_show($node, NULL, TRUE)。
特殊、预定义载入参数:%map和%index
这有两个特殊的载入函数,%map标志以数组形式传递Drupal当前路径,在前面的例子中,如果%map作为载入参数传递,它的值应该是array('node', '56', 'revisions', '4', 'view')。如果载入函数声明参数是一个引用,那么map值可以被它操作;前面的例子标志%index的值是1,因为通配符在位置1 。
使用to_arg()函数从通配符创建路径
前面说过,Drupal不能从一个由通配符构成的Drupal路径中构建有效的链接,例如user/%(毕竟Drupal不知道怎样替换这个%),但这么说不完全严密,我们能定义一个helper函数能为通配符生成一个替换,而后Drupal能用啦构建链接。在“My account”菜单项,“My account”链接的路径是有下几部产生的: 1. Drupal路径本来是user/%user_uid_optional。 2. 当构建链接是,Drupal查看名为user_uid_optional_to_arg()的函数,如果没定义Drupal不知道怎样绘出建立路径并且不会显示这个链接。 3. 如果函数找到了,Drupal使用函数结果作为链接中通配符的替换,user_uid_optional_to_arg()函数返回当前用户ID,例子如果你是用户4,Drupal链接“My account”到http://example.com/?q=user/4。 函数to_arg()的使用不是针对给定的页面执行的,就是说,to_arg()函数在任何页面的链接创立是运行的,不只是那些匹配菜单项的Drupal路径的页面,“My account”链接将在所有页面上显示,而不只在浏览http://example.com.com/?q=user/3页面时。
通配符和to_arg()函数的特殊情形
当Drupal从基于Drupal路径的通配符跟随字符串的菜单项创建链接是要查看to_arg()函数,这可能是任意字符串,如下例:
/** * 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 menufun_hello($animal) { return t("Hello $anima"); } function a_zoo_animal_to_arg($arg) { // $arg is '%' since it is a wildcard // let't replace it with a zoo animal. return 'tiger'; }
这使得链接“Hi”呈现在导航区块中,链接的URL是http://example.com/?q=menufun/tiger。通常你不能用一个静态的字符串替换通配符,如此例。当然啦,to_arg()函数能提供一些动态的东西,像当前用户的uid和当前节点的nid。
从其它模块变更菜单项
当Drupal重建menu_router表和更新menu_link表时(比如当一个新的模块被激活),模块有机会去改变一个菜单项,通过实现hook_menu_alter()。例如,“Log off”菜单项使用user_logout()签退当前用户,终止用户会话,然后重定向用户到站点首页。user_logout()函数在modules/user/user.pages.inc中,那么此drupal路径的菜单项有一个file键定义,正常情况下,当用户点击导航区块上的“Log off”链接,Drupal装入modules/user/user.pages.inc文件并运行user_logout(),让我们改变这些重定向签退用户到drupal.org。
/** * Implementation of hook_menu_alter(). * * @param array $items * Menu items keyed bu 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'; $items['logout']['access callback'] = 'user_is_logged_in'; // 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() implementation. $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, )
当我们变更之后,page callback被设置到 menufun_user_logout:
array( 'access callback' => 'user_is_logged_in', 'module' => 'user', 'page callback' => 'menufun_user_logout', 'title' => 'Log out', 'weight' => 10, )
从其它模块变更菜单链接
当Drupal保存一个菜单项到menu_link表,将给模块一个改变此链接的机会,利用实现hook_menu_link_alter()。这就是“Log out”菜单项能改变标题到“Sign off”。
/** * Implements hook_menu_link_alter(). * * @param $item * Associative array defining a menu link as passed into menu_link_save() */ function menufun_menu_link_alter(&$item) { if ($item['link_path'] == 'user/logout') { $item['link_title'] = 'Sign off'; } }
这个钩子可以用来修改链接的标题或权重。如果你需要修改菜单项的其它参数诸如access callback,请用hook_menu_alter()。
注意:在hook_menu_link_alter()中改变并制造一个菜单项不能重写由用户接口提供的菜单项,就是menu.module提供的Administer->Site building->Menus接口。
菜单项类别
当你在菜单钩子中增加一个菜单项,一个你可能使用的键是type,如果你没定义一个type,默认的MENU_NORMAL_ITEM将被使用。Drupal区别对待我们分配的菜单项类型,每一个菜单项类型都是有一串flags或属性组成,下表列出菜单项类型flags: --------------------------------------------------------------------------------------------------------------------- Binary Hexadecimal Decimal Constant Description --------------------------------------------------------------------------------------------------------------------- 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 项可由管理员修改 000010000000 0x0080 128 MENU_IS_LOCAL_TASK 项是本地任务 000100000000 0x0100 256 MENU_IS_LOCAL_ACTION 项是本地动作 --------------------------------------------------------------------------------------------------------------------- 例如,常数MENU_NORMAL_ITEM(define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB)就有MENU_VISIBLE_IN_TREE、MENU_VISIBLE_IN_BREADCRUMB标记,如下
面展示: -------------------------------------------------------------------------------- Binary Constant -------------------------------------------------------------------------------- 000000000010 MENU_VISIBLE_IN_TREE 000000000100 MENU_VISIBLE_IN_BREADCRUMB 000000000110 MENU_NORMAL_ITEM -------------------------------------------------------------------------------- 因此,MENU_NORMAL_ITEM有下面的标志000000000110,下表列出可用的菜单项类型和他们体现的flags:
----------------------------------------------------------------------------------------------------------------------------------------- Menu Flags Menu Type Constants ----------------------------------------------------------------------------------------------------------------------------------------- MENU_ MENU_ MENU_ MENU_ MENU_ NORMAL_ CALLBACK SUGGESTED_ LOCAL_ DEFAULT_ ITEM ITEM* TASK LOCAL_TASK MENU_IS_ROOT MENU_VISIBLE_IN_TREE X MENU_VISIBLE_IN_BREADCRUMB XX X MENU_LINKS_TO_PARENT X MENU_MODIFIED_BY_ADMIN MENU_CREATED_BY_ADMIN MENU_IS_LOCAL_TASK X X ------------------------------------------------------------------------------------------------------------------------------------------
那么哪些常数是你在定义自己的菜单项时使用的,看一下上表看看你想激活哪些标志并使用常数包含这些标志,每个常数的细节说明都在includes/menu.inc的注释中,大多数通常使用MENU_CALLBACK, MENU_LOCAL_TASK, and MENU_DEFAULT_LOCAL_TASK,读一读细节。
常见任务
本节我们展示一些典型的方法去面对在使用菜单工作时开发者面对的一些问题。
给菜单指定一个回调但不指定连接
常常我们可能想映射一个URL到一个函数而不建个可视的菜单项,例如,可能你有个javascript函数在web表单中需要去从drupal中获得州的列表,那么你需要封装一个URL到一个PHP函数但是不需要在任何导航菜单包含这些,你能通过将MENU_CALLBACK指派给你的菜单项类型来做到这些,就像本章第一个例子。
显示菜单项为选项卡
一个显示为选项卡那样的回调一看就知是个本地任务(local task)并且有类型MENU_LOCAL_TASK或MENU_DEFAULT_LOCAL_TASK。本地任务的标题应该是短动词,例如“add”或“list”等。本地任务通常活动在特定的对象上,例如一个节点、用户。你可以将本地任务看成是一个菜单项的语义声明,它正常渲染为选项卡 -- 类似<strong>是个语义声明,通常渲染成粗体。 本地任务在渲染选项卡序列中必须有一个父项目,一个常用的惯例是分配一个回调到一个像milkshake一样的根路径,然后分配本地任务到这个路径的扩展部分,如milkshake/prepare、milkshake/drink,等等。Drupal内建的主题支持两级本地任务选项卡(附加的层级也是潜在支持的,但是你的主题必须提供显示附加层级的功能)。选项卡渲染的顺序是依靠菜单项标题的alpha排序决定的,如果不是你想要的,你可以增加一个weight键到你的菜单项,那就按权重排序了。下面的例子展示结果为两个主选项卡,并在默认本地任务下有两个子选项卡,建立sites/all/modules/custom/milkshake/milkshake.info输入:
name = Milkshake description = Demostrates menu local tasks. package = Pro Drupal Development core = 7.x files[] = milkshake.module
然后在sites/all/modules/custom/milkshake/milkshake.module中输入:
<?php /** * @file * Use this module to learn about Drupla's menu system. * specifically how local tasks work. */ /** * Implements hook_menu(). */ function milkshake_menu() { $items['milkshake'] = array( 'title' => 'Milkshake flavors', 'access arguments' => TRUE, 'page callback' => 'milkshake_overview', 'type' => MENU_NORMAL_ITEM, ); $items['milkshake/list'] = array( 'title' => 'List flavors', 'access arguments' => TRUE, 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0, ); $items['milkshake/add'] = array( 'title' => 'Add flavor', 'access arguments' => TRUE, 'page callback' => 'milkshake_add', 'type' => MENU_LOCAL_TASK, 'weight' => 1, ); $items['milkshake/list/fruity'] = array( 'title' => 'Fruity flavors', 'access arguments' => TRUE, 'page callback' => 'milkshake_list', 'page arguments' => array(2), // pass 'fruity'. 'type' => MENU_LOCAL_TASK, ); $items['milkshake/list/candy'] = array( 'title' => 'Candy flavors', 'access arguments' => TRUE, 'page callback' => 'milkshake_list', 'page arguments' => array(3), // 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...'); } fucntion milkshake_list($type) { return t('List @type flavors', array('@type' => $type)); }
隐藏存在的菜单项
存在的菜单项可以通过改变链接项目的hidden属性来隐藏,假设你由于某种原因想移除“Create content”菜单项,使用我们的老朋友hook_menu_link_alter():
/** * Implements hook_menu_link_alter(). */ function milkshake_menu_link_alter(&$item) { // Hide the Create content link. if ($item['link_path'] == 'node/add') { $item['hidden'] = 1; } }
使用menu.module
激活Drupal的menu模块提供了一个便利的用户接口,使站点管理员可以定制存在的菜单,如导航和主菜单,可以新增加一个菜单。当menu_rebuild()函数运行时,菜单树呈现的数据结构就保存进数据库,这发生在你激活或停用模块或者其它搞混了菜单树结构的情况。数据保存进menu_router表,关于链接的信息保存进menu_links表。 当处理建立一个页面的链接时,Drupal首先建立基于从模块的菜单钩子的实现接收到的路径信息的树,并存储进menu_router表,然后它按照从数据库来的菜单信息布局。这种行允许你使用menu.module去改变菜单树的parent、标题、路径和描述 -- 你并没有真正改变潜在的树,当然啦,你是在它上边建立可以覆盖它的数据。
注意:菜单项类型,如MENU_CALLBACK或DEFAULT_LOCAL_TASK在数据库中用相等的十进制表示。
常见错误
你已经实现了菜单钩子,但是你的回调没有激发、你的菜单没显示出来或者就是不工作,请你检查下面列出的情况: + 你是否将access callback键射成了一个返回FALSE的函数? + 你是否忘记在菜单钩子函数结尾处增加 return $items;? + 你是否将access arguments或page arguments的值意外地设成了字符串而不是数组? + 你清空你的菜单缓存并重建菜单了吗? + 你要讲菜单显示为选项卡,你给它指派一个有页面回调的父项了吗? + 如果你使用本地任务,你做到在一个页面上最少两个选项卡了吗(这是显示它们所必须的)?
小结
读完这章,你也该可以:
+ 映射URL到你的模块、其它模块或.inc文件中的函数 + 理解访问控制如何工作 + 理解路径中的通配符如何工作 + 建立带映射到函数的选项卡(本地任务)的页面 + 程序化修改存在的菜单项和链接
为加深阅读,menu.inc的注释已经抽出,见http://api.drupal.org/?q=api/group/menu/7