第2章 编写自己的模块

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

  Drupal可以用于构建各种类型的网站,比如论坛、电子商务、博客、新闻聚合、相册、视频等等。由于每种类型的网站之间,其具体功能相去甚远,如果仅仅依靠Drupal核心模块,那么是远远满足不了这么多需求的。但是DrupalPHP语言的基础之上,封装了一套自己的API,基于DrupalAPI,用户就可以根据自己网站的具体需求,开发出对应的模块,来满足自己的需求。以电子商务类型的网站为例,drupal社区的相关开发者,先后开发出来了ecomerceubercartcommerce模块,用来满足该类型网站的建站需求。

 

    Drupal的模块大体分作两类,一种是Drupal核心模块,它又可分为核心必选模块和核心可选模块;另一种是第3方模块,就是Drupal核心以外的模块,在drupal.org上的模块下载中,可以找到各种各样的模块,比如常见的viewspanelsctoolsrules模块。当我们要实现一个功能的时候,我们就要寻找相应的模块,我们首先想到的是Drupal核心模块是否能够满足我们的需求,其次是在drupal.org的模块库中寻找,有时可能会在别的网站上找到。通常我们总能找到一个现成的模块,或者基于多个基础模块的组合功能,恰好满足我们的需求。但是总存在这样的情况,现有的模块仅能满足我们80%的需求,或者没有现成的模块可用。这个时候,我们就不得不开发自己的模块了。

 

    在本章中,我们将根据实际的需求,来开发一个自己的模块。通过这个实例,我们能够了解到Drupal模块的结构,常用的Drupal API,基本的编码规范。首先让我们了解一下这个模块的具体需求,我们知道,中文的网站,通常信息量比较大,页面全是链接,而链接放在一个又一个的区块当中,通常中文网站的区块有3部分组成,区块标题,更多链接,区块内容列表。而Drupal
心的区块,只包含了标题和内容两个组成部分。为了在区块中,添加一个更多链接,可以采用多种办法,但是很多时候,我们需要把这个链接硬编码到模板文件中。
如果能够在区块的配置页面,能够配置更多链接,那岂不是更好?而此时我们又没有找到一个现成的模块可用,所以我们只好编写一个这样的模块了。


Drupal版本:

1 创建相关文件

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

  首先我们需要做的是为模块起一个名字。原来我想到的名字是“more_link”名字,考虑到这个模块只用于区块系统,又改用“block_more_link”,打算使用这个名字的时候,忽然想到link本身是一个钩子(注:Drupal7中已取消这个钩子),这样我又改为了“block_morelink”,看来为模块起一个名字有时候也需要细心的考虑一下。为一个模块起一个名字,对于我们这些中文用户来说,还真的有点麻烦。

 

    模块的名字包含用户可读名字和机读名字两种,上面用到的“block_morelink”就是模块的机读名字,它在Drupal系统内部使用,只能使用字母、数字、下划线。

 

    接着,我们需要找个地方来放置这个模块。我们可以把这个模块放在核心模块所在的目录中去,不过这样的话,我们需要记住哪些是核心模块,哪些是我们自定义的模块,将来升级维护的时候会比较麻烦。按照Drupal的最佳实践,我们应该把它放在目录sites/all/modules下面,以将其与核心模块区分开来。

我们在sites/all/modules下面在创建一个名为custom的文件夹,专门用来放置我们自己定义的模块。同时创建一个standard文件夹,用于放置第3方模块。是否将sites/all/modules下面的模块分成两类分别存放,取决于开发者的习惯和项目的实际情况。如果项目用到的模块比较少,自定义模块只有一个,此时通常不分。如果项目比较大,用到的第3方模块和自定义的模块都比较多,为了方便,此时建议分开管理。

    然后我们在sites/all/modules/custom下面创建一个名为block_morelink文件夹。在Drupal 中,一个模块通常对应于一个文件夹,里面包含这个模块的所有相关文件。通常包含的文件有info文件、module文件、install文件、inc文件。由于我们的模块,需要在数据库中创建一个表结构,首先让我们创建3个文件,里面不包含任何内容,分别为block_morelink.infoblock_morelink.moduleblock_morelink.install

 

   接着,让我们为info文件添加内容。


Drupal版本:

2 info文件

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

 info文件的作用是,为Drupal模块提供元数据信息,比如这个模块的用户可读名字、模块的描述、这个模块所依赖的模块。我们为block_morelink.info文件添加以下内容: 

 

name = 区块更多链接

description = 为所有的区块提供了一个更多链接

core = 7.x

dependencies[] = block

configure = admin/config/block/morelink

 

    根据上面的内容,我们可以看出来,info文件是一个纯文本文件,它与标准的ini配置文件非常类似。它的指令语法为:

name = value

 

    就是一组由等号分割的名值对。如果一个名字可以有多个值的话,那么可以使用类似数组的形式:

name[] = value

 

    让我们来看一下,这些内容的含义:第一条指令的意思是说,模块的名字为“区块更多链接”;第二条指令为我们声明了这个模块的描述,告诉用户这个模块是用来做什么的,namedescription的值会显示在模块列表页面;第三条指令,告诉Drupal这个模块所适用的Drupal主版本,这里为7.xDrupal主版本之间是不兼容的;第四条指令,表示这个模块依赖于block模块,如果block模块没有启用,那么我们的这个模块也就无法启用;第五条指令,用于告诉Drupal这个模块的配置页面链接,同样显示在模块列表页面。

 

1.png 

                 图 模块启用后在模块列表页面上显示的内容

 

    如果我们需要在info文件添加注释,那么可以使用“;”,语法如下:

;files[] = morelink.module

;files[] = morelink.admin.inc

;files[] = morelink.install

 

    注意我们在上面的注释中,使用了files[]指令,它用来表示这个模块都包含哪些文件。在Drupal7中,实现了缓加载机制,用来提升Drupal的性能。最初设计的目标是,Drupal可以缓加载所有的PHP函数、类、接口。但是在Drupal7正式版发布时,只实现了类、接口的缓加载。所以files[]指令的具体含义,就是声明模块中有哪些文件里面包含php类、接口的代码。由于我们这个模块中,没有使用到PHP面向对象技术,也就没有包含任何PHP类、接口。所以我们完全可以把与这个指令相关的代码注释掉。

 

    除此之外,常用的还有packagephp指令。package用来表示,这个模块放在哪个包下面,反映在模块列表页面,就是放在哪个fieldset下面。Drupal核心模块,都放在Core下面。如果没有声明package,那么系统会自动将模块放在Other包下面。php表示所需PHP的最小版本,Drupal所需PHP的最小版本是PHP5.2。如果你的模块代码中,用到了PHP6.0中的最新特性,此时你可以添加以下代码:

php = 6.0

 

    对于从drupal.org上下载的第3方模块,你通常还会看到以下信息:

; Information added by drupal.org packaging script on 2011-05-06

version = "7.x-1.0-alpha1"

core = "7.x"

project = "field_validation"

datestamp = "1304650916"

 

    这些信息是由drupal.org上的打包脚本添加的,用来声明模块的版本、项目名称、Drupal核心、此版本发布时的时间戳。注意这里使用了双引号。双引号,通常可用可不用。

 

    最后需要注意的是,我们这个文件中包含中文,所以我们必须把info文件的文本格式设置为UTF-8,否则在Drupal中,就无法正确的显示我们的文本。对于我们中文圈内的Drupal开发者,时常应该记着把infomoduleincphp文件的文本格式设置为UTF-8,这样可以避免很多不必要的编码错误。

 

    现在我们创建好了info文件,让我们为module文件中添加内容。


Drupal版本:

3 module文件

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

我们在文件的开始处使用PHP的开始标签,接着添加了一段简洁的注释:


<?php

 

/**

 * @file

 * 让用户为区块添加一个更多链接.

 *

 * 在区块的配置页面,允许用户输入更多链接,在区块显示的时候,显示一个更多链接.

 */

 

    首先需要注意的是,PHP开始标签“<?php”的前面不要有任何空格以及任何字符,否则有可能带来不必要的麻烦,举个例子userpoints_nc的module文件中,<?php”的前面带有了两个空格,结果导致RSS输出不能正常工作(参看http://drupal.org/node/1096746)。其次需要注意的是,module文件中不要使用php结束标签 ?>;这个结束标签对于PHP来说是可选的,但在Drupal中,有可能导致文件的尾部空格问题(参看http://drupal.org/node/545)。

 

接着,让我们看一下Drupal中注释的写法。我们首先从/**开始,在接下来的每一行中,缩进一格并以*开头,最后以*/结束。指令@file表示它下面的文本,是一个给出这个文件用途的描述。可以使用模块api.module,将这些注释提取成API文档。接着空了一行,后面跟着一段更长的描述,它用来向其它程序员说明这个模块是做什么的。

 

    下面我们要做的就是编写模块的逻辑了。我们在前面讲过,我们这个模块目的是为区块添加一个“更多链接”属性。为此,我们首先需要让用户为区块输入这个“更多链接”,在什么地方输入呢?应该在区块的配置页面,以及区块的添加页面,在这两个页面允许用户输入“更多链接”。首先让我们以区块的配置页面为例,我们先来看看这个页面。

 

1.png

                       图2-1默认的区块配置页面

 

    我们看到这个配置页面里面包含了一个表单,如果在这个页面,能够有那么一个表单元素,允许我们输入这个区块所指向的更多链接,这样就完美了。然而这个默认页面,并没有为我们提供这样的表单元素,这个时候,对于很多刚刚接触Drupal的其它程序员来说,首先想到应该就是修改这个页面对应程序的源代码,直接将我们想要的表单元素加进来。这个时候,聪明一点的程序员,就会顺藤摸瓜,根据路径信息,或者页面里面表单元素的信息,就会找到这个页面对应的代码,这就是位于block模块中block.admin.inc里面的block_admin_configure函数。

 

function block_admin_configure($form, &$form_state, $module, $delta) {

  $block = block_load($module, $delta);

  $form['module'] = array(

    '#type' => 'value',

    '#value' => $block->module,

  );

  $form['delta'] = array(

    '#type' => 'value',

    '#value' => $block->delta,

  );

 

  // Get the block subject for the page title.

  $info = module_invoke($block->module, 'block_info');

  if (isset($info[$block->delta])) {

    drupal_set_title(t("'%name' block", array('%name' => $info[$block->delta]['info'])), PASS_THROUGH);

  }

 

  $form['settings']['title'] = array(

    '#type' => 'textfield',

    '#title' => t('Block title'),

    '#maxlength' => 64,

    '#description' => $block->module == 'block' ? t('The title of the block as shown to the user.') : t('Override the default title for the block. Use <em>!placeholder</em> to display no title, or leave blank to use the default block title.', array('!placeholder' => '<none>')),

    '#default_value' => isset($block->title) ? $block->title : '',

    '#weight' => -18,

  );

 

  // Module-specific block configuration.

  if ($settings = module_invoke($block->module, 'block_configure', $block->delta)) {

    foreach ($settings as $k => $v) {

      $form['settings'][$k] = $v;

    }

  }

 

  // Region settings.

  $form['regions'] = array(

    '#type' => 'fieldset',

    '#title' => t('Region settings'),

    '#collapsible' => FALSE,

    '#description' => t('Specify in which themes and regions this block is displayed.'),

    '#tree' => TRUE,

  );

.

$form['actions'] = array('#type' => 'actions');

  $form['actions']['submit'] = array(

    '#type' => 'submit',

    '#value' => t('Save block'),

  );

 

  return $form;

}

 

    这是一个表单API函数,现在我们还没有必要完全掌握表单API,我们只需要了解这个函数是用来定义表单元素的,在这里它将表单定义成为一个大的数组$form,而这个表单中的每一个元素,同样是一个由键值构成的数组。比如$form['settings']['title'],其定义如下:

 

$form['settings']['title'] = array(

    '#type' => 'textfield',

    '#title' => t('Block title'),

    '#maxlength' => 64,

    '#description' => $block->module == 'block' ? t('The title of the block as shown to the user.') : t('Override the default title for the block. Use <em>!placeholder</em> to display no title, or leave blank to use the default block title.', array('!placeholder' => '<none>')),

    '#default_value' => isset($block->title) ? $block->title : '',

    '#weight' => -18,

  );

 

    这里面,左边的'#type''#title''#maxlength''#description''#default_value''#weight'是表单元素的键名,右边是对应的值。'#type'用来设定这个表单元素的类型,这里是'textfield''#title'对应于这个表单元素的label'#maxlength'用来限制这个表单元素的最大长度,'#description'是表单元素后面的描述;'#default_value'用来设定这个表单元素的默认值;'#weight'决定了这个表单元素在整个表单中的位置,默认为0,重量越小越靠前,这里的值为-18,表示这个元素应该靠前放置。上述代码,经过Drupal的表单系统,就会被转换为HTML对应的表单元素。

 

    我想对于很多刚刚接触Drupal的人来说,上来就学习表单API的相关知识,开始肯定吃不消。对于这个模块实例,我没有从实现hook_menuhook_permhook_help这样简单的钩子开始讲解,主要是为了给大家呈现一个解决问题完整过程,希望让读者在学习Drupal api的同时,能够掌握解决问题的方法,另外就是想突出一下表单APIDrupal模块开发中的重要性,只有掌握了表单API,才能算的上真正熟悉了Drupal模块开发。

 

    如果我们在$form['settings']['title']下面,添加一个新的表单元素,用来让用户输入“更多链接”,也同样能够解决问题,但是这会带来更多的问题,比如Drupal版本升级了,从Drupal7.0升级到了Drupal7.2,此时如果你修改了源代码,升级到Drupal7.2以后,原有的改动都被替换掉了,除非你再次修改同样的代码,或者使用打补丁的方式。但是这些方式都是不足取的,对于我们这些普通的Drupal开发者来说,永远不要修改Drupal核心代码,这在Drupal中是不允许的。

 

    那么有读者就会问了,不在这里修改源代码,那在什么地方添加我们的表单元素?难道我们能够不修改Drupal核心代码,就能将想要的表单元素添加到这个页面?答案当然是可以的。Drupal将表单抽象成为了数组,这为表单定义、呈现、处理带来了极大的灵活性。Drupal在呈现表单时,为我们提供了两个钩子,用来修改表单,一个是hook_form_alter,另一个是hook_form_FORM_ID_alter。讲到这里,聪明一点的读者就会想到,通过实现这两个钩子函数,我们就可以向区块配置页面添加我们的“更多链接”表单元素了。

 

    但是这里使用哪个钩子呢?是同时需要实现两个钩子函数,还是只需要实现其中的一个就可以了?实际上,对于上面的两个钩子,在很多时候两者之间是通用的,我们在hook_form_alter中,可以修改多个表单,而在hook_form_FORM_ID_alter中只能修改一个表单,而从性能方面来看,hook_form_FORM_ID_alter的效率要稍微高一点,但是这点性能提升,绝对不会对你站点的性能带来显著的影响。

 

    经过前面的准备工作,我们知道我们应该在module文件中实现hook_form_FORM_ID_alter这个钩子函数。对于这个钩子函数,我们首先需要知道,这里面大写的FORM_ID是需要被替换成为实际的表单ID的,什么是表单ID?它通常就是定义表单的函数的名字,由于我们在前面找到了区块配置表单的对应函数block_admin_configure,所以这里的FORM_ID就应该被替换为block_admin_configure

 

    让我们来定义我们的第一个钩子函数:

 

function block_morelink_form_block_admin_configure_alter(&$form, &$form_state){

  $default_morelink_url = '';

  $form['settings']['morelink_url'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link url'),

    '#maxlength' => 255,

    '#description' => t('The More Link url of the block as shown to the user.') ,

    '#default_value' =>  $default_morelink_url,

    '#weight' => -17,

  );

}

 

    在这个钩子函数中,我们新增了一个表单元素$form['settings']['morelink_url'],我们可以通过这个表单元素来输入更多链接所指向的URL

 

    对于一个链接,它通常包含2部分,一部分是链接文本,这里面我们可以使用“更多”这一固定文本就可以了;另一部分是链接指向的路径,就是前面我们所定义的;此外,还有一个重要的组成部分,那就是鼠标移到链接上,所显示的提示文本,这是我们目前所忽略的,但对于实际的SEO非常有用。尽管后者可有可无,让我们还是在这个钩子函数中,为其新增一个表单元素:

 

function block_morelink_form_block_admin_configure_alter(&$form, &$form_state){

  $default_morelink_url = '';

  $default_morelink_title = '';

  $form['settings']['morelink_url'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link url'),

    '#maxlength' => 255,

    '#description' => t('The More Link url of the block as shown to the user.') ,

    '#default_value' =>  $default_morelink_url,

    '#weight' => -17,

  );

  $form['settings']['morelink_title'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link title'),

    '#maxlength' => 255,

    '#description' => t('The More Link title of the block as shown to the user.') ,

    '#default_value' =>  $default_morelink_title,

    '#weight' => -17,

  );

}

    我们在解决问题的时候,很多时候并不能一步到位,比如这里新增的这个表单元素,我们就是出于SEO的考虑,而新增过来的。新增的这个表单元素,并不会为后续开发带来很多麻烦。启用这个模块,现在我们在区块的配置页面,就能够看到我们新增的两个表单元素了。如图2-2所示。

 

    我们在前面还提到区块的添加页面,这里也是一个表单页面,我们也需要为其新增两个同样的表单元素。我们找到区块的添加页面对应的函数block_add_block_form,同样位于block模块的block.admin.inc文件里面,代码如下:

 

function block_add_block_form($form, &$form_state) {

  return block_admin_configure($form, $form_state, 'block', NULL);

}

 

    这个函数相当简单,它直接把表单的定义工作委托给了block_admin_configure。我们在这里知道了这个表单的IDblock_add_block_form,尽管实际工作都是由block_admin_configure完成的,但是在这里,表单ID变了。因此我们需要在我们的模块中,为block_add_block_form定义钩子函数,代码如下:

 

function block_morelink_form_block_add_block_form_alter(&$form, &$form_state) {

  block_morelink_form_block_admin_configure_alter($form, $form_state);

}

 

2.png 

                       图2-2 带有更多链接输入的区块配置页面

 

    

    我们也仿照着block_add_block_form,在我们的钩子函数block_morelink_form_block_add_block_form_alter中,我们将表单的修改工作,都委托给了block_morelink_form_block_admin_configure_alter,这就是我们前面定义好的钩子函数。我们再次打开区块的添加页面,就可以看到新增的两个表单元素了。

 

    接下来需要考虑的是,这个表单提交时,对于新增表单元素所提交的数据,我们如何处理?显然Drupal核心中的代码,并不知道我们新增了两个表单元素,所以核心部分是不会负责处理我们新增的这两个元素的,因此我们需要自己对这两个元素负责。Drupal允许我们在hook_form_FORM_ID_alter钩子函数中,追加新的表单验证函数和提交函数。我们分别追加一个验证函数和一个提交函数:

function block_morelink_form_block_admin_configure_alter(&$form, &$form_state){

  $default_morelink_url = '';

  $default_morelink_title = '';

  $form['settings']['morelink_url'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link url'),

    '#maxlength' => 255,

    '#description' => t('The More Link url of the block as shown to the user.') ,

    '#default_value' =>  $default_morelink_url,

    '#weight' => -17,

  );

  $form['settings']['morelink_title'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link title'),

    '#maxlength' => 255,

    '#description' => t('The More Link title of the block as shown to the user.') ,

    '#default_value' =>  $default_morelink_title,

    '#weight' => -17,

  );

  $form['#validate'][] = 'block_morelink_block_admin_configure_validate';

  $form['#submit'][] = 'block_morelink_block_admin_configure_submit';

}

 

    注意这里面$form['#validate']$form['#submit'],它们是两个数组,里面分别包含了这个表单的验证函数集,和提交函数集。我们新增的验证函数和提交函数,通常并不影响已有的验证函数和提交函数,表单在验证阶段会调用它上面所有的验证函数,而在提交阶段,则会调用它上面所有的提交函数。

 

    我们在module文件中,建立这两个函数:

 

/**

 * Form validate handler for block configuration form.

 */

function block_morelink_block_admin_configure_validate($form, &$form_state){

  //Todo

}

/**

 * Form submit handler for block configuration form.

 */

function block_morelink_block_admin_configure_submit($form, &$form_state){

  //Todo

}

 

    在验证函数中,我们可以验证URL的有效性,但是在实际的开发应用中,这些验证并不是很重要,用户只需要自己输入有效的URL就可以了。所以我们暂时先不考虑这个验证函数。而在提交函数中,我们则需要把用户输入的信息保存到数据库中,显然我们现在还没有准备好用来存储数据的数据库表。


Drupal版本:

4 创建自己的数据库表结构

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

模块中存储所用数据的常用方式,就是为这些数据创建一个单独的数据库表。当我们决定为模块创建数据库表结构时,应该问问自己:我们需要存储哪些数据?如果我们要对这个表进行查询,那么会用到哪些字段和索引?最后,还要考虑一下,将来对这个模块,有没有可能会作些扩展?

 

    首先让我们分析一下需要存储的数据。除了上面新增表单元素里面的信息外,我们还需要保存区块的ID信息,这样我们才能知道这个更多链接信息是属于哪个区块的。使用一个mysql的管理工具,比如phpmyadmin,我们打开Drupal的数据库结构,我们发现,在Drupal中,区块的ID是由模块名和delta值共同组成的。这样对于一个区块,我们需要保存4个值,分别为moduledeltaurltitle。另外我们需要为我们的数据库表起个名字,为了简单起见,就叫做block_morelink,和我们的模块名保持一致。

 

    我们表的SQL语句如下所示:

 

CREATE TABLE block_morelink (

  module varchar(64) NOT NULL,

  delta varchar(32) NOT NULL,

  url varchar(255) NOT NULL,

  title varchar(255) NOT NULL,

  PRIMARY KEY (module,delta,url,title),

)

我们可以把这段sql语句放到我们模块的README.txt文件中,这样我们就省事了。但是对于想要安装这个模块的其他用户来说,他们需要手工的使用上述SQL语句来创建数据库表,这样做会比较麻烦。实际上,在Drupal中,有更好的解决方式,我们知道,在启用核心模块时,Drupal能自动的创建相应的数据库表;我们可以使用Drupal的这一特性。我们在前面创建的install文件中,添加以下代码

 

/**

 * Implements hook_schema().

 */

function block_morelink_schema() {

  $schema['block_morelink'] = array(

    'description' => 'Stores more link path.',

    'fields' => array(

      'module' => array(

        'type' => 'varchar',

        'length' => 64,

        'not null' => TRUE,

        'description' => "The block's origin module, from {block}.module.",

      ),

      'delta' => array(

        'type' => 'varchar',

        'length' => 32,

        'not null' => TRUE,

        'description' => "The block's unique delta within module, from {block}.delta.",

      ),

      'url' => array(

        'type' => 'varchar',

        'length' => 255,

        'not null' => TRUE,

        'description' => "The more link url of a block.",

      ),

      'title' => array(

        'type' => 'varchar',

        'length' => 255,

        'not null' => TRUE,

        'description' => "The more link title of a block.",

      ),

    ),

    'primary key' => array('module', 'delta', 'url', 'title'),

    'indexes' => array(

      'url' => array('url'),

    ),

  );

  return $schema;

}

 

在第一次启用block_morelink模块时,drupal会检查block_morelink.install文件,看里面是否实现了hook_schema钩子,它将读取hook_schema中所定义的数据库表结构模式,并将它们转化为了当前数据库的标准SQL语句。有关这方面的更多信息,可参看Schema API一章。如果一切顺利的话,这样就完成了数据库表的创建工作。让我们试验一下。由于我们在前面还没有创建数据库表的时候,就启用了该模块,所以我们需要重新安装一下这个模块,需要按照以下步骤进行重装:

 

1导航到“管理 > 模块”,先将block_morelink模块禁用。

2在管理界面“管理 模块”,找到卸载标签,点击这个标签,选择block_morelink模块,卸载。这样Drupal就会删除与这个模块有关的数据库表。

3启用这个模块。这次,在模块启用时,Drupal会创建相关的数据库表。

 

 

    当Drupal创建了用来存储数据的block_morelink表以后,现在让我们添加与数据处理相关的代码。首先,我们需要在提交处理函数中添加一些逻辑代码,这样在用户修改了区块配置信息后,它可以用来处理与更多链接相关的数据。我们的表单提交函数如下所示:

 

function block_morelink_block_admin_configure_submit($form, &$form_state){

  db_delete('block_morelink')

    ->condition('module', $form_state['values']['module'])

    ->condition('delta', $form_state['values']['delta'])

    ->execute();

    

  if(!empty($form_state['values']['morelink_url'])){

  $query = db_insert('block_morelink')->fields(array('url', 'title', 'module', 'delta'));

 $query->values(array(

    'url' => $form_state['values']['morelink_url'],

    'title' => $form_state['values']['morelink_title'],

    'module' => $form_state['values']['module'],

    'delta' => $form_state['values']['delta'],

  ));

  $query->execute();

  }

  

}

由于我们在一个区块上只允许有一个更多链接,所以我们可以先删除以前的信息,然后把最新的信息插入到数据库中。对于我们与数据库之间的交互,首先需要注意的是,我们不需要考虑数据库连接,这是因为Drupal在它的引导指令中已经完成了这一工作。其次,对于上述代码,我们目前只需要了解它做了哪些操作就可以了:首先是删除与该区块相关的更多链接信息,接着做了一个判断,如果输入的更多链接路径不为空,此时我们将更多链接有关信息插入到block_morelink表中。这里面我们使用了db_delete、db_insert两个数据库操作函数,分别用来负责删除与插入操作。

 

   最后,我们需要修改block_morelink_form_block_admin_configure_alter中代码,这样,如果已经存在了一个更多链接,那么将把它从数据库中读取出来,并用来将其作为默认值传递给我们的表单元素。我们将原有的代码:

 

  $default_morelink_url = '';

  $default_morelink_title = '';

 

   替换为

 

  $result = db_query("SELECT url, title FROM {block_morelink} WHERE module = :module AND delta = :delta", array(

    ':module' => $form['module']['#value'],

    ':delta' => $form['delta']['#value'],

  ))->fetch();

  $default_morelink_url = empty($result)?'':$result->url;

  $default_morelink_title = empty($result)?'':$result->title;

 

    这里我们使用了db_query函数从数据库中取出更多链接的url、title,并将其设置为现有表单元素的默认值。对于db_query中的sql,这里需要注意两点:首先,在我们用到一个数据库表时,我们需要把它放到花括号{}里;这样可以方便的实现在数据库表名的前面添加前缀(关于表名前缀的更多信息,可参看文件sites/default/settings.php中的注释)。其次,我们在查询语句中使用了占位符“:module”和“:delta”,并为其提供了相应的变量,这样Drupal内置的安全机制就可以帮助我们阻止SQL注入。

 


Drupal版本:

5 创建自己的预处理函数

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

  对于我们的这个模块来说,现在已经到了万事俱备只欠东风的阶段了。我们已经准备好了数据,现在我们需要在区块中将其显示出来。对于区块,我们首先想到的是区块的模板文件,下面是Drupal核心中自带的区块模板文件:

 

<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>

 

  <?php print render($title_prefix); ?>

<?php if ($block->subject): ?>

  <h2<?php print $title_attributes; ?>><?php print $block->subject ?></h2>

<?php endif;?>

  <?php print render($title_suffix); ?>

 

  <div class="content"<?php print $content_attributes; ?>>

    <?php print $content ?>

  </div>

</div>

 

    在这里面,html代码片段中间,嵌套了一些PHP变量,这些变量是在哪里定义的呢?Drupal中模板中的变量通常都是定义在预处理函数中的。对于block.tpl.php,其变量来源于template_preprocesstemplate_preprocess_blocktemplate_process。其中template_preprocess_block是专门针对区块的预处理函数,其代码如下:

 

function template_preprocess_block(&$variables) {

  $block_counter = &drupal_static(__FUNCTION__, array());

  $variables['block'] = $variables['elements']['#block'];

  // All blocks get an independent counter for each region.

  if (!isset($block_counter[$variables['block']->region])) {

    $block_counter[$variables['block']->region] = 1;

  }

  // Same with zebra striping.

  $variables['block_zebra'] = ($block_counter[$variables['block']->region] % 2) ? 'odd' : 'even';

  $variables['block_id'] = $block_counter[$variables['block']->region]++;

 

  // Create the $content variable that templates expect.

  $variables['content'] = $variables['elements']['#children'];

 

  $variables['classes_array'][] = drupal_html_class('block-' . $variables['block']->module);

 

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;

  $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module . '__' . $variables['block']->delta;

 

  // Create a valid HTML ID and make sure it is unique.

  $variables['block_html_id'] = drupal_html_id('block-' . $variables['block']->module . '-' . $variables['block']->delta);

}

    在这个函数中定义了变量blockblock_zebrablock_idcontentclasses_arrayblock_html_id,同时还定义了区块的模板建议theme_hook_suggestions。但是这里面并没有为我们定义“更多链接”。可以在我们的模块中实现区块的预处理函数,为block变量追加一个morelink属性:

 

/**

 * 为block变量添加morelink属性

 * @see block.tpl.php

 */

function block_morelink_preprocess_block(&$variables) {

  $result = db_query("SELECT url, title FROM {block_morelink} WHERE module = :module AND delta = :delta", array(

    ':module' => $variables['block']->module,

    ':delta' => $variables['block']->delta,

  ))->fetch();

  $morelink_url = empty($result)?'':$result->url;

  $morelink_title = empty($result)?'':$result->title;

  $variables['block']->morelink = '<span class="block-more-link">' . l(t('More'), $morelink_url, array('attributes' => array('title' => $morelink_title))). '</span>';

}

    在这段代码中,我们首先取出来了“更多链接”的url,title。然后使用l()函数构建了一个更多链接。将block.tpl.php模板文件复制到了themes\bartik\templates目录下面,并编辑里面的代码,以输出更多链接:

 

<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>

 

  <?php print render($title_prefix); ?>

<?php if ($block->subject): ?>

  <h2<?php print $title_attributes; ?>><?php print $block->subject ?></h2>

<?php endif;?>

  <?php print render($title_suffix); ?>

 

  <div class="content"<?php print $content_attributes; ?>>

    <?php print $content ?>

  </div>

<?php if ($block->morelink): ?>

    <?php print $block->morelink ?>

  <?php endif;?>

</div>

 

    这样,我们配置一下搜索表单区块,输入一个测试用的更多链接urltitle。保存区块,回到区块的显示页面,我们就会看到一个更多链接。

 

                          1.png

                           图2-3搜索表单多了一个更多链接

 

    模块写到这里,功能基本上就完成了,如果是一个实际的项目,现在就可以将其应用于在线站点了。但是作为模块开发中的一个练习来讲,我们还需要进一步的来完善这个模块,使其具有较强的可配置性和可定制性。

 


Drupal版本:

6 改进我们的代码

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

   在我们的预处理函数中,在为morelink属性赋值时,我们直接使用了 l() 函数并附加了一些html标签,我们把html标签写死在了里面,如果别人使用我们的代码,他们想修改这一输出时,只有通过修改module文件中的对应代码才能实现。我们可以采用Drupal的主题函数的方式,来改进我们的代码:

 

/**

 * Implements hook_theme().

 *

 */

function block_morelink_theme(){

  return array(

    'block_morelink_link' => array(

       'variables' => array('url' => NULL,'title' => NULL,)

    ),

    

  );

}

 

/**

 * Returns HTML for a "more" link, like those used in blocks.

 *

 * @param $variables

 *   An associative array containing:

 *   - url: The url of the main page.

 *   - title: A descriptive verb for the link, like 'Read more'.

 */

function theme_block_morelink_link($variables) {

$output = "";

if(!empty($variables['url'])){

$morelink_label = t('More');

   $output .= '<span class="block-more-link">' . l($morelink_label, $variables['url'], array('attributes' => array('title' => $variables['title']))). '</span>';

}

return $output;

}

 

    同时修改预处理函数中的代码,将

$variables['block']->morelink = '<span class="block-more-link">' . l(t('More'), $morelink_url, array('attributes' => array('title' => $morelink_title))). '</span>';

    替换为:

$variables['block']->morelink =  theme('block_morelink_link', array('url' => $morelink_url, 'title' => $morelink_title))

 

    在这里面,我们将原来的逻辑放在了theme_block_morelink_link函数中了,注意对于这个主题函数,这里需要注意以下两点:首先、我们没有直接显性的调用theme_block_morelink_link,而是通过theme()函数调用,这样就利用了Drupal的主题覆写机制,其他Drupal开发者就可以在主题层覆写我们的主题函数了。其次,所有的主题函数,都需要在hook_theme中注册一下,只有这样才能被Drupal识别出来,hook_theme中返回的是一个数组,一个这样的钩子函数中可以注册多个主题函数。

 

    现在编辑bartiktemplate文件,我们在这个文件的最下面追加以下函数:

 

function bartik_block_morelink_link($variables) {

$output = "";

if(!empty($variables['url'])){

$morelink_label = t('More');

   $output .= '<div class="block-more-link">' . l($morelink_label, $variables['url'], array('attributes' => array('title' => $variables['title']))). '</div>';

}

return $output;

}

 

    这里我们没有直接修改module文件中的theme_block_morelink_link,就实现了对这个主题函数的覆写,将span标签替换为了div

 


Drupal版本:

7继续改进我们的代码

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

我们在前面的代码中,morelink的标签t(More)是写死在里面的,通过翻译机制我们可以将其翻译成任意的中文。但是如果这一文本,能够配置的话,那么这个模块就会更通用一点。如果你觉得现在已经足够好的话,那么也无需改进,我们这里仅仅是作为一个例子,让大家接触更多的钩子和API函数。

 

/**

 * Implements hook_menu().

 */

function block_morelink_menu() {

 // Block settings.

  $items['admin/config/block'] = array(

    'title' => 'Block',

    'description' => 'Block configuration.',

    'position' => 'left',

    'weight' => -10,

    'page callback' => 'system_admin_menu_block_page',

    'access arguments' => array('access administration pages'),

    'file' => 'system.admin.inc',

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

  );

  $items['admin/config/block/morelink'] = array(

    'title' => 'More link',

    'description' => 'the more link lable of block.',

    'page callback' => 'drupal_get_form',

    'page arguments' => array('block_morelink_label_settings'),

    'access arguments' => array('administer site configuration'),

    'weight' => -10,

   // 'file' => 'block_morelink.admin.inc',

  );

   return $items;

}

    这里我们实现了hook_menu(),它返回一个包含菜单项的数组。这里面每一项都以路径为键,在这里就是admin/config/blockadmin/config/block/morelink。菜单项的值是一个数组,里面包含的键和值,描述了Drupal在处理该路径下的回调函数时可以做些什么。有关菜单方面的更多详细,可参看菜单系统一章。

 

Drupal的配置页面有多个类别,比如内容、用户、系统,都出现在主配置页面上。我们的配置是关于区块的,并没有一个类别特别适合我们。所以在这里,我们创建一个名为“Block”的新类别。

2.png

                 图2-4 显示在主配置页面的Block”的新类别

在第二个菜单项中,我们将我们的配置页面放在Block”类别的下面。这段代码说,“当用户访问页http://example.com/admin/config/block/morelink 时,调用函数drupal_get_form, 并向它传递一个表单ID block_morelink_label_settings作为参数。只有具有管理站点配置权限的用户才有权查看这个菜单。”当需要显示表单时,Drupal就会让我们提供一个表单定义。当有人访问这个路径时,Drupal就会将其映射到表单定义函数上。

 

function block_morelink_label_settings(){

  $form['block_morelink_label'] = array(

    '#type' => 'textfield',

    '#title' => t('More Link lable'),

    '#maxlength' => 40,

    '#description' => t('The More Link lable of the block as shown to the user.') ,

    '#default_value' =>  variable_get('block_morelink_label', t('More')),

    '#weight' => -17,

  );

  

  return system_settings_form($form, TRUE);

}

 

    注意这个表单定义函数,我们现在将其放在了module文件中,我们也可以创建一个block_morelink.admin.inc文件,然后将这个函数放在该文件中。此时需要取消“'file' => 'block_morelink.admin.inc',”前面的注释符号。Module文件中,通常只放置钩子函数和API函数,其它逻辑处理,通常都放在inc文件中。对于每个页面请求,Drupal通常都会加载所有的module文件,把相关文件放在inc文件中,有利于降低module文件的大小,可以实现缓加载,从而提升性能。但是我们的这个函数,代码量非常少,新增一个inc文件,也是有性能成本的。此时所带来的性能提升,可能还抵消不了新引入的性能成本。

 

在这个表单函数中,我们添加了一个文本输入框,允许用户输入更多链接的标签。在这里,我们自己没有管理表单的处理流程,而是使用了函数system_settings_form()来让系统模块为表单添加一些按钮,并让它来管理表单的验证和提交。图2-5给出了的当前表单的样子。

 

1.png 

            图2-5  block_morelink的配置页面 

 

variable_get与variable_set 

 

    在前面的例子中,修改配置并点击“保存配置”按钮,就可以正常工作。下面部分将描述如何实现这一点。Drupal在数据库中有一个variable表,用来存储“名-值”对。使用variable_set($key,$value来存储“名-值”对,使用variable_get($key,$default)来取回“名-值”对。在前面的代码中,

 

  variable_get('block_morelink_label', t('More'))

 

用来取回名为block_morelink_label的变量的值,如果这个值为空,则使用默认值t('More')

 

    可能有读者会问,这个变量是怎么保存到数据库中的?由于我们调用了system_settings_form,我们的表单提交时,会调用system_settings_form_submit函数,在这个函数中,有以下代码:

 

foreach ($form_state['values'] as $key => $value) {

    if ($op == t('Reset to defaults')) {

      variable_del($key);

    }

    else {

      if (is_array($value) && isset($form_state['values']['array_filter'])) {

        $value = array_keys(array_filter($value));

      }

      variable_set($key, $value);

    }

  }

 

    它会按照表单元素的名字,来保存变量的值。所以我们要保证,表单元素的名称和variable_get里面的名称保持一致。在variable表中存储和取回设置时,为了避免命名空间的冲突,前面应该以模块名字开头。表单字段和变量的键应使用同一名字。

 

    现在,我们可以把theme_block_morelink_link($variables)函数中的相应代码替换掉了,将

$morelink_label = t('More');

    替换为

$morelink_label = variable_get('block_morelink_label', 'more');

 

    最后,由于我们在模块中,新增了一个变量,除了我们需要维护这个变量的读取与存储以外,我们还需要在这个模块被卸载时,能够删除它所定义的变量。在install文件中增加以下代码:

 

function block_morelink_uninstall(){

  variable_del('block_morelink_label');

}

 

    这里我们实现了hook_uninstall钩子,在这个钩子中,在这个模块被卸载时,使用variable_del函数删除变量block_morelink_label

 


Drupal版本:

8贡献我们的代码

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

尽管贡献自己的代码在中国这样的环境中,并不是很流行,但是环境还是在逐步的改善,通过向Drupal社区贡献自己的模块,如果你的模块被其它用户使用的话,也是可以为你带来很多潜在的机会的。开发好了block_morelink模块以后,我们可以在drupal.org上创建一个项目,然后将这个模块通过GIT上传上去,这样我们就将这个模块分享出去了。项目地址:http://drupal.org/project/block_morelink/。注意drupal.org上的模块,需要遵守GPL协议。

1.png 

               图2-6  drupal.org上该模块项目页面


Drupal版本:

9总结

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

总结

 

    读完本章以后,我们应该掌握以下几点:

从头创建一个Drupal模块。

使用Drupal的表单API来创建简单的表单。

使用hook_form_FORM_ID_alter来修改其它表单。

了解Drupal的主题覆写机制

了解Drupal中的预处理函数

使用hook_schema创建数据库表

使用hook_menu建立简单的回调映射。

使用variable_get与variable_set来读取和存储配置信息。



Drupal版本: