Drupal 模块开发基本教程。

适用于:初级drupal开发者
基本要求:php编程-基础,drupal管理-熟练
鸣谢:drewish 的模块样例

第一部分:基本模块结构

事实上,模块真正必须实现的函数只有一个,那就是hook_help()。这里我们约定在本文中对drupal的系统“钩子”我们都写为“hook_钩子名”的形式,你实现的时候的函数名实际上是“模块名_钩子名”的形式。

什么是钩子(hook)?
Drupal是一个内容管理系统的编程框架,其核心功能已经很完备了。我们编写扩展模块时并不需要自己完成每一个功能,大多数扩展系统的方法只是按命名规 则写好钩子(Hook)的实现函数,系统就会在特定的时候来调用你的函数,这样你的扩展功能就被实现了。记住,不是你去调用系统,而是系统来调用你,我觉 得这个和Windows的消息循环是有点类似的。

Hook_help()可以告诉drupal系统模块的信息以使模块在“模块”管理页面上列出来,这样用户就可以选择是否启用或禁用此模块了。模块一但被 启用,模块文件里的全部代码都被包含到系统中,同时,模块的函数也可以调用drupal的所有可用函数和访问所有可用变量了。

下面是最简单的hook_help()写法,这里的模块文件名是example.module,模块名是example,记得在php文件要有〈?php和?〉标记在文件首尾。

function example_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      // 下面的描述会显示在admin/modules路径下的模块列表里
      return t('An example module showing how to develop a module for Drupal.');
  }
}

只要将包含上面函数的example.module文件放到drupal目录下的modules子目录里,访问“模块”管理页面(在admin/modules)就可以启用这个模块了,简单吧。

注:上面的返回值中我们使用了t()函数,这个函数用以实现drupal的本地化,虽然代码里是写的英文,但如果我们有对应的本地语言的翻译,用户会看到对应的本地化字符串。

现在,我们有了一个可启用/禁用的模块了,在开始为它添加功能之前,我们先了解一下Drupal的模块开发编程规范(全文见 http://cvs.drupal.org/viewcvsdrupal/contributions/CODING_STANDARDS.html?rev=1.1&content-type=text/plain )。Drupal为了最大程度的兼容PHP的不同版本,它不使用命名空间或类的封装特性,因此,我们需要特别注意命名的问题,一般的约定是:

1)函数和方法使用小写字符,并用下划线分隔单词。函数应当使用模块名作为前缀以避免不同模块间的函数名冲突。私有的函数和方法再在名称前加一个下划线。
2)常量名完全大写,并用下划线分隔单词。前面应当加上大写的模块名前缀。
3)你定义的全局变量的名字前加上“_模块名_”前缀。
其他的约定请参见编码规范。

模块可以实现的功能很多,一般分为如下几个功能类别:
1)产生页面显示内容;
2)自定义node类型;
3)扩展现有的node类型;
4)生成定制的区块(block);
5)控制node的显示权限;
6)文件上传;
7)定制过滤器(Filter);
8)其他高级应用。

下面我们以自定义页面显示内容为例开始module开发之旅。

 

 

第二部分:模块的自定义页面显示方法

许多时候我们需要为一些数据显示一个自定义格式的页面。熟悉模板的同志们可能曾经失望的发现,模板只能控制除$content之外的那部分页面。在模板 里,内容区之外的其他部分你想怎么定义都行,但要控制内容的格式,对不起,它是由一个名为$content的变量一次输出了整个内容正文。

这就决定了,一般情况下,内容的格式控制只能通过模块来实现,呵呵——不会写程序的同志们可能要晕倒了;其实别的方法也不是没有,就是……通过能写模块的 人来实现,哈哈哈,这个是开玩笑哈,另一个方法是修改theme引擎来实现(这个另文阐述,放在这里离题了)。还有非标准的方法,那些个就不介绍了,会误 导群众,最后必将导致网站维护、升级异常困难等后果。

下面是一个最简单的页面显示函数,输出我们要的内容:

function example_foo() {
  $content = '<p>The quick brown fox jumps over the lazy dog.</p>';
  return $content;
}

 

真够简单,问题是我们该访问那个URL这个页面才会出来呢?下面drupal的url定义钩子hook_menu()隆重登场:

function example_menu($may_cache) {
  $items = array();
  // $may_cache参数用来将菜单项分为两类。
  // $may_cache 为 TRUE时返回的菜单项对当前用户在任何时候都可用(并被缓存);
  // 其他的则是可更改的或只在一定的路径下才被定义的,例如带参数的动态路径。
  // 绝大多数模块都会有可缓存的菜单项。
  if ($may_cache) {
    // 这是你必须提供的最基本信息
    $items[] = array('path' => 'foo',
       'title' => t('foo'),
       'callback' => 'example_foo',
       'access' => TRUE);
  }
  return $items;
}

 

好了,现在访问http://example.com/foo,你可以看到输出的内容了……“Ooops,显示说404 not found”。这个还是有可能的,因为$may_cache的菜单项都被缓存了,新的路径还没有刷新到缓存里面,此时,要么用devel模块clear cache,要么浏览一下管理页面中的“菜单”页面就会刷新菜单路径缓存了。现在应该可以了,这样,你在example_foo()的$content里想怎么构建你的页面都行,那个只是html+css的问题了。

顺带讲一下hook_menu()。这个hook的用法蛮灵活,复杂的导航必备,例如通过定义MENU_LOCAL_TASK类型的路径,可以在其他模块 产生的页面上的Secondary Tabs部分嵌入自己的页面。前面的例子里,我们在$items数组里用数组定义了页面的URL相对路径“path”,页面标题“title”,回调函数 “callback”和权限控制“access”,整一个意思就是告诉drupal的菜单系统“如果访问foo这个路径,那就调用example_foo 这个函数,产生的页面的标题是foo,同时允许任意用户访问(因为access始终是TRUE)”。

下面是一个带参数的url地址定义:

function example_menu($may_cache) {
  $items = array();
    // 通过使用MENU_CALLBACK类型的路径,我们可以为指定路径注册一个
    // 回调函数而不出现在菜单项列表里,管理员也不能在菜单管理里禁用这个路径
    $items[] = array('path' => 'bar/baz', 'title' => t('baz'),
      'callback' => 'example_baz',
      'access' => TRUE,
      'type' => MENU_CALLBACK);
    // 下面的菜单项没有注册回调函数,此时,属性就会从父路径继承。
    // 例如,这里也会使用父路径的权限控制。不过如果路径上有指定参数的话,
    // 我们重定义了标题。
    // 注意:如果没有'type'属性,此项会在菜单中显示,也就是说'type'不被继承。
    $items[] = array('path' => 'bar/baz/52/97',
      'title' => t('the magic numbers'),
      'type' => MENU_CALLBACK);
  }
  return $items;
}function example_baz($alice = 0, $bob = 0) {
  // 永远不要相信URL中传来的值是安全的!一定要记得检查这些值。
  if (!is_numeric($alice) || !is_numeric($bob)) {
    // 如果参数都不是数字,我们将显示一个标准的“你无权访问”的页面
    drupal_access_denied();
    return;
  }
  $list[] = "Alice's number was $alice.";
  $list[] = "Bob's number was $bob.";
  $list[] = 'The total was '. ($alice + $bob) .'.';
  // 调用theme函数实现输出的格式化,theme_item_list()只是许多theme函数中的一个
  $content = theme('item_list', $list);
  return $content;
}

 

如果用户访问http://example.com/?q=bar/baz,菜单系统会执行example_baz(),如果用户访问http: //example.com/?q=bar/baz/1/2,菜单系统会首先查找bar/baz/1/2,如果找不到对应定义会接着找bar/baz /1。如果又找不到,它就会找bar/baz,这样它就会执行example_baz(1,2)。注意路径中的数字部分作为参数传递给了函数,这个实在是 非常好用。

如果用户访问http://example.com/?q=bar/baz/52/97,菜单系统找到了匹配,但由于回调函数被省略,因此它最终调用的是example_baz(52,97)。有什么不同呢,此时页面标题不再是“baz”,而是“the magic numbers”了。

前面所有的路径定义中access都为TRUE,但通常我们都希望对页面内容的访问作一些权限控制。此时我们需要在模块里实现hook_perm()函数:

function example_perm() {
  return array('access foo', 'access baz');
}

 

现在,你在“访问控制”页面就可以分配这两个权限给指定的角色了。同时,你还要在hook_menu里这样定义’access’:

'access' => user_access('access foo'),

 

user_access()函数会访问$user全局变量和权限设定以确定访问页面的当前用户是否有’access foo’这个权限,有就返回TRUE,没有返回FALSE。用户ID为1的用户默认拥有任何权限,即user_access(‘任意值’)对他来说都会返回TRUE;权限管理对他完全没用,乖乖。——所以,有特殊要求时,记得还要控制uid=1的用户。

好了,我相信你已经能够用模块定义自己的页面了——虽然看似有些繁琐,但恭喜你已经迈出模块之旅的第一步。

 

 

 

第三部分:自定义node类型

Drupal系统本质上是一个CMS系统Framework,你可以在此基础上为满足自己的要求自由的扩展。从Drupal的观点看,所有的内容对象都应 该是节点(node),整个Framework都基于这个假设来运转,从一个熟悉OOP的程序员的观点来看,node就是对象,处理不同类型的node就 象处理从node类派生的各种子类。当然,这不过是PHP程序而已,你尽可以想怎样就怎样,但是,和OOP的继承的好处一样,将内容纳入node的管理体 系至少可以立刻获得如下的功能和好处:

1) Drupal核心中node的基本增删修改删除功能,评论功能,分类功能,审核功能,日志功能,状态统计功能,搜索功能,各种node相关管理功能;
2)所有支持node的drupal扩展模块提供的功能,包括但不仅限于节点附件上传,节点计划发布/隐藏,所见即所得编辑器,图片插入助理等等;
3) 核心和扩展模块的升级将立刻使你的节点类型享受增强的功能或特性;
4) 核心和扩展模块的安全补丁将使你的节点类型更加安全;
等等……,这一切只需要你定义一下节点类型即可获得,何乐而不为呢?如果你将内容放入drupal进行管理却不遵循node定义和管理的方式,我可以肯定 地说你会在系统升级方面遇到困难和麻烦——简言之就是大伙儿都不是你这么想的,你的代码和别人不兼容。这可能会是你在开源系统里能够遇到的最糟糕的一件事 了。

定义新的node类型其实非常简单,现在仅需一个hook_node_info(),而不向4.6版本时需要 hook_node_name() 和 hook_node_types()。模块文件名叫node_example.module:

function node_example_node_info() {
  return array('node_example' => array('name' => t('example node'), 'base' => 'node_example'));
}

 

hook_node_info()钩子是必须要实现的。此函数描述了这个模块可提供的节点。’name’的值是节点的人性化名字,’base’就是 drupal系统识别的名字了。Durpal通过’base’的值知道操作这种类型的节点时应当在hook函数将加上什么样的前缀:如果’base’为 “node_foo”,那么插入一个这种节点时调用的对应函数是’node_foo_insert’。

为界面友好的目的,我们一般还会实现hook_help(),这样用户创建节点时可以看到诸如介绍一类的东西:

function node_example_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      // 在admin/modules页面显示的模块描述信息
      return t('An example module showing how to define a custom node type.');
    case 'node/add#node_example':
      // 在node/add“创建内容”页面上显示的此类型节点的帮助信息。
      return t('This is an example node type with a few fields.');
  }
}

 

自然我们也要定义hook_perm以实现节点的权限控制。我们定义了三个权限,一个用来限制谁可以浏览此种节点,另一个用来限制谁可以创建此种节点,最 后一个用来限制是否允许用户修改自己创建的节点。为什么需要第三个权限呢?如果管理员允许未注册用户创建此种节点,那么管理员应该同时取消未注册用户修改 自己创建的节点的权限,因为未注册用户对系统来说都是uid为0的同一个用户。

function node_example_perm() {
  return array('view example node', 'create example node', 'edit own example nodes');
}

 

另外节点权限管理需要定义的钩子是hook_access(),这个钩子可以自定义用户对节点的增删修改等操作的权限。下面是最常见的定义模式:

function node_example_access($op, $node) {
  global $user;
  if ($op == 'view') {
    // 有浏览权限的用户才能浏览此种节点
    return user_access('view example node');
  }
  if ($op == 'create') {
    // 有创建权限的用户才能创建此种节点
    return user_access('create example node');
  }
  // 如果有需要的权限,创建节点的用户可以修改和删除它
  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own example nodes') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

 

另一个需要定义权限的地方是节点添加的页面和路径,下面按照国际惯例定义hook_menu以控制谁可以访问增加节点的页面:

function node_example_menu($may_cache) {
  $items = array();
  if ($may_cache) {
// 此处只定义访问路径的权限即可
    $items[] = array('path' => 'node/add/node_example', 'title' => t('example node'),
      'access' => user_access('create example node'));
  }
  return $items;
}

 

节点定义和用户权限都有了,下面我们可以开始完善节点的增删修改浏览的功能。这里使用的节点例子类型叫node_example,它允许用户节点中保存 “颜色(color)”和“数量(quantity)”两种自定义信息。因为要存储这些附加信息,我们需要在数据库中增加额外的表。

数据库定义:
  CREATE TABLE node_example (
      vid int(10) unsigned NOT NULL default '0',
      nid int(10) unsigned NOT NULL default '0',
      color varchar(255) NOT NULL default '',
      quantity int(10) unsigned NOT NULL default '0',
      PRIMARY KEY (vid, nid),
      KEY `node_example_nid` (nid)
    );

 

事实上,如果不需处理这些额外的附加信息,你会发现有上述函数的模块已经能够处理新定义的节点类型了,前面叙述的你该即刻享受的系统好处你都能享受到了,呵呵。但要处理节点类型特有的附加信息,我们还有许多工作要做:

(一) 增加自定义节点

首先,虽然基本的增加节点的提交页面系统已经提供了,但是上面没有我们要的自定义信息的输入框或选择框,这样我们就必须实现hook_form()以在提 交页面上收集自定义信息。这个钩子要求我们返回一个数组值,其中包含了定制表单各元素的定义信息,注意这也是drupal 4.7的标准表单定义方法。

function node_example_form(&$node) {
  // 用来输入节点标题的文本框
  $form['title'] = array(
    '#type' => 'textfield', // 表单元素的类型
    '#title' => t('Title'), // 表单元素的标题
    '#required' => TRUE, // 是否必须输入的值
    '#default_value' => $node->title, //缺省值
    '#weight' => -5
  );
  // 如果我们希望表单元素按特定的顺序排列,我们可以设置元素的Weight值。
  // 但是其他模块可能也插入设置了weight值的表单元素,这样元素顺序可能仍然
  // 不是我们想要的样子。为避免这一情况,我们可以把元素放到子数组中,一个
  // 数组中的元素总是可以保证顺序的了吧!
  $form['body_filter']['body'] = array(
    '#type' => 'textarea',
    '#title' => t('Body'),
    '#default_value' => $node->body,
    '#required' => FALSE
  );
  $form['body_filter']['filter'] = filter_form($node->format);
  // 上面是普通应有的标题和正文,下面是我们特有的节点信息了
  $form['color'] = array(
    '#type' => 'textfield',
    '#title' => t('Color'),
    '#default_value' => $node->color
  );
  $form['quantity'] = array(
    '#type' => 'textfield',
    '#title' => t('Quantity'),
    '#default_value' => $node->quantity,
    '#size' => 10,
    '#maxlength' => 10
  );
  return $form;
}

 

用户用上面的表单提交数据,drupal自动会过滤提交的数据,对于表单里没有定义却被提交的值,drupal会自动把它们过滤掉以保证系统安全。然后, 我们可以通过hook_validate()验证提交的有效数据是不是我们真正想要的,下面我们验证一下用户提交的quantity值是不是一个数字,不 是就报告错误,用户会被要求重新输入,如果没有数据提交(因为前面表单定义里并没有要求此值是必须的),则认为quantity是0:

function node_example_validate(&$node) {
  if ($node->quantity) {
    if (!is_numeric($node->quantity)) {
      form_set_error('quantity', t('The quantity must be a number.'));
    }
  }
  else {
    // Let an empty field mean "zero."
    $node->quantity = 0;
  }
}

要注意的是告诉系统出错了并不是返回一个什么值,而是调用form_set_error设置表单错误,这样可以在一个函数里设定多个错误。   

如果数据验证成功,则新节点就会生成了,新节点信息会写入到数据库中。且慢,我们好像并没有写入附加信息到数据库的程序段。当节点插入时,drupal会调用所有实现hook_insert()的函数,这让我们有机会向数据库写入我们自己的节点自定义信息:

function node_example_insert($node) {
  db_query("INSERT INTO {node_example} (vid, nid, color, quantity) VALUES (%d, %d, '%s', %d)", $node->vid, $node->nid, $node->color, $node->quantity);
}

格式很简单,你不用考虑用户的提交是怎么被处理的,你只要直接引用它们就可以了,$node->color、$node->quantity什么的,它们一定正确的在那里。   

(二) 修改自定义节点

Drupal本身支持节点拥有多个版本,我们在这里也需要考虑到这一点。

function node_example_update($node) {
  // 处理产生一个新版本的情况
  if ($node->revision) {
    node_example_insert($node);
  }
  else {
    db_query("UPDATE {node_example} SET color = '%s', quantity = %d WHERE vid = %d", $node->color, $node->quantity, $node->vid);
  }
}

 

(三) 删除自定义节点

删除很简单,系统本来就实现了节点的删除功能,我们需要做的是在适当的时候把我们写入数据库的附加信息给清除掉。当节点删除时,drupal会调用所有实现hook_delete()的函数,这样各个模块都有机会清理自己生产的垃圾了。

function node_example_delete($node) {
  // 注意这里我们删除了指定nid的所有版本
  db_query('DELETE FROM {node_example} WHERE nid = %d', $node->nid);
}

 

(四) 与系统其他部分互动

负责载入节点对象的node_load()函数会发出hook_load()调用,以便扩展模块将自定义属性补充进它返回的$node对象中。因此我们还必须实现hook_load(),附加信息才会在载入节点对象时自动也被载入。

function node_example_load($node) {
  $additions = db_fetch_object(db_query('SELECT color, quantity FROM {node_example} WHERE vid = %d', $node->vid));
  return $additions;
}

 

(五) 自定义节点显示格式

下面的代码实现了hook_view(),这个钩子在用户浏览节点时被调用。实现得很简单,节点的正文(body)和节选(teaser)都是系统功能提 供和实现的,我们只需要把自定义的信息加到其中即可。这里我们用的是最简单的方式,直接加在正文和节选的后面就可以了。实际情况下,这个显示的格式是可以 任意调整的。

function node_example_view(&$node, $teaser = FALSE, $page = FALSE) {
  $node = node_prepare($node, $teaser);
  $order_info = '<div class="node_example_order_info">';
  $order_info .= t('The order is for %quantity %color items.', array('%quantity' => $node->quantity, '%color' => $node->color));
  $order_info .= '</div>';
  $node->body .= $order_info;
  $node->teaser .= $order_info;
}

 

写到这里,发现要用好自定义节点类型要实现的hook还是蛮多的,不过只要按部就班,也应该能很快实现,就跟定义一个类然后实现些方法差不多。然后这个定义的节点就可以在系统中被自由使用了,似乎比OOP还要略微简单点。

 

评论

(required. But it will not be published)