Think in Drupal 第1集

老葛

北京亚艾元软件有限责任公司

http://www.yaiyuan.com


第一章 Drupal的工作原理11

1,什么是Drupal11

2, drupal的技术堆栈11

2.1 PHP12

2.2 web服务器12

2.3 数据库12

2.4操作系统12

2.5 HTML,CSS,JavaScript13

3 Drupal文件夹结构13

3.1 includes13

3.2 misc14

3.3 modules14

3.4 profiles14

3.5 scripts14

3.6 sites14

3.7 themes:15

3.8 authorize.php:15

3.9 cron.php:15

3.10 index.php:15

3.11 install.php:15

3.12 update.php:15

3.13 xmlrpc.php:15

3.14 robots.txt:15

4 Drupal核心概念16

模块16

钩子16

主题18

节点18

区块19

菜单19

用户19

字段与实体19

5 Drupal执行流程20

5.1 引导指令21

5.2 钩子的执行顺序23

总结24

第2章  编写自己的模块25

创建相关文件25

info文件26

module文件27

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

创建自己的预处理函数39

改进我们的代码41

继续改进我们的代码43

variable_get与variable_set45

贡献我们的代码46

总结47

第3章 Drupal 菜单系统48

创建一个菜单项48

调整菜单项的位置51

调整菜单项所属的菜单52

不在菜单中显示菜单项53

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

访问控制54

标题的本地化和定制56

定义标题回调56

菜单嵌套58

页面回调参数60

将菜单项显示为标签63

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

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

菜单项中的通配符70

基本通配符71

通配符和页面回调参数72

使用通配符的值73

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

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

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

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

Hook_menu的键值属性78

菜单项的类型80

相关的钩子函数80

总结80

第四章 数据库API81

一般概念81

驱动81

连接82

查询82

语句82

数据库配置83

连接键83

目标83

$databases语法83

依赖于PDO85

静态查询85

前缀化86

占位符86

占位符数组87

查询选项88

结果集88

存到类中90

动态查询91

内容结构92

概貌92

关联93

字段93

Distinct94

表达式95

排序95

随机排序96

分组96

范围和限制96

表排序97

条件语句97

执行查询97

总计查询98

调试98

扩展器98

修改查询101

插入查询103

紧凑形式104

退化形式105

多值插入形式106

基于选择查询的结果插入107

默认值108

更新查询108

删除查询109

合并查询110

只是设置它110

有条件设置111

有限制的更新112

优先级112

条件语句113

概念113

API114

数组运算符114

嵌套的条件语句115

Null值115

子查询116

示例116

编写数据库驱动117

PDO117

错误处理117

事务117

链式118

函数和运算符120

逻辑运算符120

比较运算符120

类型操作运算符121

字符串函数和运算符121

数学函数和运算符121

日期/时间函数121

聚合函数121

总结121

第5章 Schema(模式) API122

模块的install文件122

创建数据库表122

使用Schema(模式)模块124

Schema与数据库字段类型之间的映射关系125

文本型126

Varchar126

Char126

Text127

数字型127

Integer127

Serial127

Float128

Numeric128

二进位:Blob128

相关API函数130

维护我们的数据库表131

第6章  Form API133

两步表单133

创建相关文件133

“联系我们”页面134

控制表单的外观140

添加验证函数和提交函数142

确认页面144

邮件发送149

“致谢”页面150

AJAX表单151

准备工作151

创建相关文件152

Ajax表单的三个关键要点157

Ajax表单流程分析158

表单元素161

Actions(动作)161

Button(按钮)161

Checkbox(复选框)162

Checkboxes(复选框)162

Container(容器)163

Date(日期)163

fieldset(字段集)164

File(文件)165

hidden(隐藏域)165

image_button(图片按钮)166

item(条目)166

machine_name(机读名字)167

managed_file(受管理的文件)168

markup(标识文本)169

password(密码)169

password_confirm(带确认的密码)170

Radio(单选按钮)170

radios(单选按钮)171

select(下拉选择框)171

submit(提交按钮)171

Tableselect(表选择)172

text_format(文本格式)173

textarea(文本域)173

textfield(文本字段)174

value(值)174

vertical_tabs(垂直标签)175

weight(重量)175

呈现API176

第7章 Drupal用户177

对象$user177

测试用户是否登录了179

用户系统的钩子函数179

班主任模块181

钩子hook_user_view185

钩子hook_user_login186

钩子hook_username_alter187

统一用户登录188

与Drupal6站点整合用户188

常用解决方案介绍191

内置单点登录192

总结195

第8章 Drupal区块196

什么是区块?196

区块配置选项197

区块位置198

理解区块的呈现199

区块的数据库表结构200

区块钩子介绍202

创建一个区块203

钩子hook_block_info204

钩子hook_block_configure205

钩子hook_block_save206

钩子hook_block_view207

PHP代码的形式208

扩展阅读210

Bean211

Boxes211

Node Blocks211

MultiBlock211

CCK Blocks211

总结211

第9章 Field API212

自定义一个字段类型212

准备工作212

钩子hook_field_info213

钩子hook_field_widget_info214

钩子hook_field_schema215

钩子hook_field_widget_settings_form217

钩子hook_field_widget_form218

钩子hook_element_info219

对应表单元素的主题函数220

钩子hook_content_is_empty221

钩子hook_field_validate221

钩子hook_field_presave222

钩子hook_field_formatter_info224

验证已有的字段226

伪字段227

为已有字段定制格式器228

总结234

附录一 数据库表结构235

accesslog (统计模块)235

actions (system(系统)模块)235

aggregator_category (aggregator(聚合器)模块)235

aggregator_category_feed (聚合器模块)235

aggregator_category_item (聚合器模块)236

aggregator_feed (聚合器模块)236

aggregator_item (聚合器模块)236

authmap (用户模块)237

batch (系统模块)237

block (区块模块)237

block_custom (区块模块)238

block_node_type (节点模块)238

block_role (区块模块)239

blocked_ips (系统模块)239

book (手册模块)239

cache (系统模块)239

cache_block (区块模块)240

cache_bootstrap (系统模块)240

cache_field (字段模块)240

cache_filter (过滤器模块)241

cache_form (系统模块)241

cache_image (图片模块)241

cache_menu (系统模块)242

cache_page (系统模块)242

cache_path (系统模块)242

cache_update (更新模块)243

comment (评论模块)243

contact (联系模块)244

date_format_locale (系统模块)244

date_format_type (系统模块)244

date_formats (系统模块)245

field_config (字段模块)245

field_config_instance (字段模块)245

field_data_body (field_sql_storage 模块)246

field_data_comment_body (field_sql_storage 模块)246

field_data_field_image (field_sql_storage 模块)247

field_data_field_tags (field_sql_storage 模块)247

field_data_taxonomy_forums (field_sql_storage 模块)248

field_revision_body (field_sql_storage 模块)248

field_revision_comment_body (field_sql_storage 模块)249

field_revision_field_image (field_sql_storage 模块)249

field_revision_field_tags (field_sql_storage 模块)250

field_revision_taxonomy_forums (field_sql_storage 模块)250

file_managed (系统模块)250

file_usage (系统模块)251

filter (过滤器模块)251

filter_format (过滤器模块)252

flood (系统模块)252

forum (论坛模块)252

forum_index (论坛模块)252

history (系统模块)253

image_effects (图片模块)253

image_styles (图片模块)253

languages (本地化模块)254

locales_source (本地化模块)254

locales_target (本地化模块)254

menu_custom (菜单模块)255

menu_links (系统模块)255

menu_router (系统模块)257

node (节点模块)258

node_access (节点模块)259

node_comment_statistics (评论模块)259

node_counter (统计模块)259

node_revision (节点模块)260

node_type (节点模块)260

poll (投票模块)261

poll_choice (投票模块)261

poll_vote (投票模块)261

queue (系统模块)262

rdf_mapping (rdf 模块)262

registry (系统模块)262

registry_file (系统模块)263

role (用户模块)263

role_permission (用户模块)263

search_dataset (搜索模块)263

search_index (搜索模块)264

search_node_links (搜索模块)264

search_total (搜索模块)264

semaphore (系统模块)264

sequences (系统模块)265

sessions (系统模块)265

shortcut_set (shortcut(快捷方式)模块)265

shortcut_set_users (快捷方式模块)266

simpletest (simpletest(简单测试)模块)266

simpletest_test_id (simpletest(简单测试) 模块)266

system (系统模块)267

taxonomy_index (分类模块)267

taxonomy_term_data (分类模块)268

taxonomy_term_hierarchy (分类模块)268

taxonomy_vocabulary (分类模块)268

tracker_node (tracker(追踪器)模块)269

tracker_user (追踪器模块)269

trigger_assignments (触发器模块)269

url_alias (系统模块)269

users (用户模块)270

users_roles (用户模块)270

variable (系统模块)270

watchdog (dblog(数据库日志)模块)271

致谢272

thinkindrupal.com273


Drupal版本:

第5章 Schema(模式) API

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

通过数据库抽象层, Drupal可以支持多个数据库,比如内置支持的MySQLPostreSQLSQLite,以及通过第三方模块支持的SqlServer、Oracle等等。除此以外,Drupal在数据库方面,还提供了进一步的支持,这就是使用Schema来描述数据库表结构,这对于那些需要创建自己的数据库表的模块,提供了极大的方便。这样,我们创建好Schema定义,Drupal就能够将其翻译成具体数据库的语法,比如MySQL的、PostreSQL的。

 


Drupal版本:

1 模块的install文件

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

我们在第2章中已经看到,当我们编写的模块需要创建一个或者多个数据库表来存储信息时,创建和维护表结构的指令都放在了模块的install文件中。


Drupal版本:

10 Integer

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

这个字段是用来存储整数的,比如节点id、vid。如果unsigned键为TRUE的话,那么将不允许使用负整数。Node表中vid字段就是采用的这种类型:

 

'vid' => array(

  'description' => 'The current {node_revision}.vid version identifier.',

  'type' => 'int',

  'unsigned' => TRUE,

  'not null' => TRUE,

  'default' => 0,

),


Drupal版本:

11 Serial

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

一个序列字段是用来保存自增数字的。例如,当添加一个节点时,node表中的nid字段将会自增。序列字段必须索引;通常会把它作为主键进行索引。

'nid' => array(

  'description' => 'The primary identifier for a node.',

  'type' => 'serial',

  'unsigned' => TRUE,

  'not null' => TRUE,

 ),

 


Drupal版本:

12 Float

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

浮点数字是用来存储浮点数据类型的。对于浮点数字来说,tiny, small, medium, normal型浮点一般是没有区别的;另外,big型浮点用来声明双精度字段。Ubercart的uc_products表中weight字段用到了这一个类型。

 'weight' => array(

  'description' => 'Physical weight.',

  'type' => 'float',

  'not null' => TRUE,

  'default' => 0.0,

),


Drupal版本:

13 Numeric

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

数字数据类型允许你声明数字的精度和小数位数。精度指的是数字的有效数字位数。小数位数指的是小数点右边的数字位数。例如,123.45的精度为5,小数位数为2。这里不使用size键。到目前为止,Drupal核心中还没有用到该字段。Ubercart的uc_products表中list_price字段用到了这一个类型。

 

'list_price' => array(

  'description' => 'Suggested retail price.',

  'type' => 'numeric',

  'precision' => 16,

  'scale' => 5,

  'not null' => TRUE,

  'default' => 0.0,

),



Drupal版本:

14 二进位:Blob

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

二进位大型对象数据类型用于存储二进制数据。二进位数据包括音乐,图片,或者视频。Size的可选值有normal big。Field模块的field_config表中data字段就是使用的这种类型,用来存储序列化的数据。

 

'data' => array(

  'type' => 'blob',

  'size' => 'big',

  'not null' => TRUE,

  'serialize' => TRUE,

  'description' => 'Serialized data containing the field properties that do not warrant a dedicated column.',

),

 

 

.模式定义中的TypeSize键与本地数据库类型的对应关系

 

type

size

MySQL type & size/range

PostgreSQL type & size/range

SQLite type

serial

tiny

tinyint, 1 B

serial, 4 B

integer

serial

small

smallint, 2 B

serial, 4 B

integer

serial

medium

mediumint, 3 B

serial, 4 B

integer

serial

big

bigint, 8 B

bigserial, 8 B

integer

serial

normal

int, 4 B

serial, 4 B

integer

int

tiny

tinyint, 1 B

smallint, 2 B

integer

int

small

smallint, 2 B

smallint, 2 B

integer

int

medium

mediumint, 3 B

int, 4 B

integer

int

big

bigint, 8 B

bigint, 8 B

integer

int

normal

int, 4 B

int, 4 B

integer

float

tiny

float, 4 B

real, 6 digits

float

float

small

float, 4 B

real, 6 digits

float

float

medium

float, 4 B

real, 6 digits

float

float

big

double, 8 B

double precision, 15 digits

float

float

normal

float, 4 B

real, 6 digits

float

numeric

normal

numeric, 65 digits

numeric, 1000 digits

numeric

varchar

normal

varchar, 255 B or 64 KB (1)

varchar, 1 GB

varchar

char

normal

char, 255 B

character, 1 GB

(UNSUPPORTED)

text

tiny

tinytext, 256 B

text, unlimited

text

text

small

tinytext, 256 B

text, unlimited

text

text

medium

mediumtext, 16 MB

text, unlimited

text

text

big

longtext, 4 GB

text, unlimited

text

text

normal

text, 16 KB

text, unlimited

text

blob

big

longblob, 4 GB

bytea, 4 GB

blob

blob

normal

blob, 16 KB

bytea, 4 GB

blob


Drupal版本:

15 相关API函数

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


db_add_field

向数据库表添加一个字段。

db_add_index

向数据库表添加一个索引。

db_add_primary_key

向数据库表添加一个主键。

db_add_unique_key

向数据库表添加一个唯一键。

db_change_field

修改一个字段定义。

db_create_table

创建一个数据库表。

db_drop_field

删除一个字段。

db_drop_index

删除一个索引

db_drop_primary_key

删除数据库的主键。

db_drop_table

删除一个表。

db_drop_unique_key

删除一个唯一键。

db_field_exists

检查给定表中该字段是否存在。

db_field_names

根据指定的包含字段键的数组,返回包含字段名字的数组。

db_field_set_default

为字段设置默认值。

db_field_set_no_default

把一个字段设置为没有默认值。

db_find_tables

按照给定的基表名字,查找对应的所有数据库表。

db_index_exists

检查给定表中该索引是否存在。

db_rename_table

重命名一个表。

db_table_exists

检查该数据库表是否存在。

drupal_get_schema

获取一个表的模式定义,或者整个数据库的模式定义。

drupal_get_schema_unprocessed

返回一个模块的未处理过并且未修改过的模式(schema)。

drupal_install_schema

创建模块中hook_schema里面定义的所有数据库表。

drupal_schema_fields_sql

从一个表的模式定义中返回一列字段。该字段列表适用于SQL查询。

drupal_uninstall_schema

删除模块中hook_schema里面定义的所有数据库表。

drupal_write_record

基于数据库模式,向数据库中保存(插入或者更新)一个记录。

hook_schema

定义数据库模式的当前版本。


Drupal版本:

16 维护我们的数据库表

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

 我们在前面定义了block_morelink这个数据库表。但是有人在使用的过程中,出现错误,并且在我们的项目页面提交了bug,http://drupal.org/node/1159446,“Install Exception on new site: Syntax error or access violation: 1071 Specified key was too long”。这个问题的原因是,url,title的长度加在一起超过了主键的限制。此时我们需要修改数据库模式的定义。

 

    将block_morelink_schema中的主键

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

    修改为:

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

 

对于那些已经安装了这个模块的用户来说,我们需要为其提供一个升级路径,代码如下:

 

/**

 * change 'primary key' to array('module', 'delta').

 */

function block_morelink_update_7000(&$sandbox) {

  db_drop_primary_key('block_morelink');

  db_add_primary_key('block_morelink', array('module', 'delta'));

}

 

    在这里我们实现了钩子函数hook_update_N(&$sandbox),由于这是block_morelink模块的第一个更新函数,所以我们将这里的N设置为了7000,那么第二个更新函数就应该是7001了。在钩子函数中,我们首先删除了原有的主键,接着添加了新版的主键。Drupal会追踪模块模式的当前版本,在运行完这个更新以后,该模块的模式版本就设置为了7000,这一信息存储在system表的schema_version列中。

 

    我们再看一个例子,这段代码摘自于feeds模块的install文件:

function feeds_update_7100(&$sandbox) {

  $spec = array(

    'type' => 'text',

    'size' => 'big',

    'not null' => FALSE,

    'description' => 'State of import or clearing batches.',

    'serialize' => TRUE,

  );

  db_change_field('feeds_source', 'batch', 'state', $spec);

 

  $spec = array(

    'type' => 'text',

    'size' => 'big',

    'not null' => FALSE,

    'description' => 'Cache for fetcher result.',

    'serialize' => TRUE,

  );

  db_add_field('feeds_source', 'fetcher_result', $spec);

}

    在这个更新中,首先是将feeds_source中的字段batch重命名为了state,接着添加了一个新的字段fetcher_result。注意这里面分别用到了db_change_fielddb_add_field



Drupal版本:

2 创建数据库表

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

Drupal在安装过程中,一般将数据库表的创建,委托给drupal_install_schema()函数;而drupal_install_schema()负责从模块的schema钩子中获取模式定义,转换为具体数据库上的语法,最后创建相应的表结构。我们回顾一下,第二章中,我们创建的模式:

 

/**

 * 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表,它包含4个varchar类型的字段。它还定义了一个联合主键,为url定义了一个普通索引。注意,在字段描述中,引用另一个表中的字段时,需要为其使用花括号。这样模式模块(参看下一节)可以为表的描述构建方便的超链接。

 

    hook_schema为模块定义的每一个数据库表,返回一个带有键的数组。在该数组中,可以使用以下键:

· 'description':一个纯文本字符串,用来描述这个表及其目的。如果这里引用了其它表,那么需要将其放在花括号中。例如,node_revisions表的描述字段包含“为每一个{node},存储每个修订本的标题和正文”。

· 'fields':一个关联数组,用来描述数据库表的列。它的定义仍然是一个关联数组,里面可以使用以下参数:

· 'description': 一个纯文本字符串,用来描述这个字段及其目的。如果这里引用了其它表,那么需要将其放在花括号中。例如,节点表的vid字段的描述包括“总是为这个字段保存最大的(最新的){node_revision}.vid的值”。

· 'type': 通用的数据类型有:'char' 'varchar' 'text''blob''int' 'float' 'numeric' 'serial'。大多数类型会映射到对应数据库引擎上的具体数据库类型。对于自增字段,需要使用'serial'。在MySQL上,这就会转换为'INT auto_increment'

· 'mysql_type', 'pgsql_type', 'sqlite_type',等等:如果你需要的类型,没有包含在前面所列的内置支持的数据类型列表中,那么你可以为每个后台的数据库指定一个类型。在这种情况下,你就用不到type参数了,但是你需要注意,对于有些数据库,如果你没有单独为其指定可用的类型,那么你的模式在该类型的数据库上将无法运行。可行的解决办法是使用"text"类型作为一个回退。

· 'serialize':一个布尔值,用来指示该字段是否将存储为序列化的字符串。 

· 'size':数据的大小,可选值有:'tiny''small''medium''normal''big'。这个用来提示该字段可以存储的最大值,以及用来判定将会使用数据库引擎的哪个具体的数据类型;例如,MySQL上,TINYINT vs. INT vs. BIGINT。默认为'normal',表示选择基础类型,比如,在MySQL上,INTVARCHAR BLOB等等。不是所有的大小,都适用于所有的数据类型。对于可用的联合情况,可参看DatabaseSchema::getFieldTypeMap()

· 'not null':如果为true,那么在这个数据库中,就不允许存在NULL值;默认为false

· 'default':字段的默认值。这里区别PHP的类型:'', '0',  0表示不同含义。如果你为一个'int'类型的字段指定默认值为'0',此时它将不能正常工作,因为这里的'0'是一个字符串,而不是一个整数。

· 'length': 'char''varchar' 'text'字段类型的最大长度。对于其它字段类型,将忽略这个参数。

· 'unsigned':布尔值,用来指示'int' 'float''numeric'类型的字段是否可以包含符号。默认为FALSE。对于其它字段类型,将忽略这个参数。

· 'precision', 'scale':用于'numeric'类型的字段,用来表示数字的精度和保留的小数位数。这两个值都是必须的。对于其它字段类型,将忽略这两个参数。

    对于所有的类型,只有'type'是必须的,其它是可选的;对于'numeric'类型,除了'type'是必须的以外,'precision''scale'也是必须的。

· 'primary key':一个数组,里面包含一个或多个字段的键名,用来表示数据库表的主键。

· 'unique keys':包含唯一键的关联数组。它里面包含一个或多个字段的键名,用来表示数据库表的唯一键。

· 'foreign keys': 表关联的关联数组。它里面包含被引用表的名字,和一个包含对应列的数组。对应列的定义使用键值对的形式('source_column' => 'referenced_column')。

· 'indexes': 包含索引的关联数组。它里面包含一个或多个字段的键名,用来表示数据库表的一个索引。

 


Drupal版本:

3使用Schema(模式)模块

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

现在你可能会想,花这么大的功夫,创建一个这么复杂的数组,来向Drupal描述我们的表结构,是不是有点得不偿失啊?当你熟悉了这些语法结构以后,大多数的代码都是拷贝来,拷贝去,实际上你并不需要记忆太多,这非常类似于我们写的八股文。其次,存在第三方的模块,帮我们干这些脏活,这就是Schema(模式)模块,它的下载地址为:http://drupal.org/project/schema。下载了模块,将其启用。导航到“管理 结构 Schema”,点击Inspect(检查)标签,在这里,我们可以看到所有数据库表的模式定义。如果你为你的数据库表准备好了SQL脚本,那么使用模式模块,它就可以帮你自动生成模式定义,接着将其复制粘贴到你的.install文件中就可以了。

 

提示 你一般很少需要从头编写一个模式定义。一般情况下,你可以拷贝已有的模式,以此为基础定义自己的模式。或者,你可以使用已有的表,使用模式模块的Inspect(检查)标签,让它帮你创建模式定义。

 

模式模块还允许你查看任意模块的模式。如图所示,在模式模块中显示了block_morelink模块的模式。

 

图片1.png 

 4-1.模式模块显示了block_morelink模块的模式。


Drupal版本:

4Schema与数据库字段类型之间的映射关系

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

在模式定义中声明的字段类型,将会映射成具体数据库中的本地字段类型。例如,一个sizetiny的整数字段将映射为MySQL中的TINYINT字段,或者PostgreSQL中的smallint字段。我们以mysql为例,看看实际的映射,在includes/database/mysql/schema.inc文件中,类DatabaseSchema_mysql中的公共方法getFieldTypeMap(),列出了对应的映射关系。

 

  public function getFieldTypeMap() {

    

    static $map = array(

      'varchar:normal'  => 'VARCHAR',

      'char:normal'     => 'CHAR',

 

      'text:tiny'       => 'TINYTEXT',

      'text:small'      => 'TINYTEXT',

      'text:medium'     => 'MEDIUMTEXT',

      'text:big'        => 'LONGTEXT',

      'text:normal'     => 'TEXT',

 

      'serial:tiny'     => 'TINYINT',

      'serial:small'    => 'SMALLINT',

      'serial:medium'   => 'MEDIUMINT',

      'serial:big'      => 'BIGINT',

      'serial:normal'   => 'INT',

 

      'int:tiny'        => 'TINYINT',

      'int:small'       => 'SMALLINT',

      'int:medium'      => 'MEDIUMINT',

      'int:big'         => 'BIGINT',

      'int:normal'      => 'INT',

 

      'float:tiny'      => 'FLOAT',

      'float:small'     => 'FLOAT',

      'float:medium'    => 'FLOAT',

      'float:big'       => 'DOUBLE',

      'float:normal'    => 'FLOAT',

 

      'numeric:normal'  => 'DECIMAL',

 

      'blob:big'        => 'LONGBLOB',

      'blob:normal'     => 'BLOB',

    );

    return $map;

  }

 

  'not null' => TRUE,

  'default' => '',

),

如果default键未被设置,并且not null键被设置为了FALSE,那么默认值将被设置为NULL

 

Text

Text字段用于大块的文本。例如,node_type表中的description字段就是这种类型。Text字段可以不使用默认值。

 

'description' => array(

  'description' => 'A brief description of this type.',

  'type' => 'text',

  'not null' => TRUE,

  'size' => 'medium',

  'translatable' => TRUE,

 ),

    其中size的类型可以为tinysmallmediumbignormal

 

 


Drupal版本:

5文本型

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

文本型字段是用来包含文本的。


Drupal版本:

6Varchar

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

Varchar,也就是变长字符字段;对于长度小于256字符的文本,通常使用这一字段类型。它的最大的字符长度,可以使用length键定义。MySQL varchar 字段的长度为0–255字符(MySQL 5.0.2 及更早版本)和0–65,535字符(MySQL 5.0.3及以后版本);而PostgreSQLvarchar字段的长度则可以更大一些。

 

例如,node_chema中node表type字段的定义:

'type' => array(

  'description' => 'The {node_type}.type of this node.',

  'type' => 'varchar',

  'length' => 32,

  'not null' => TRUE,

  'default' => '',

 ),

如果default键未被设置,并且not null键被设置为了FALSE,那么默认值将被设置为NULL

 


Drupal版本:

7Char

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

Char字段是定长字符字段。该字段的字符长度,可以使用length键定义。MySQLchar字段的长度为0–255字符。Drupal核心中,我并没有找到char类型的实例。Ubercart模块中的uc_countries表的country_iso_code_2country_iso_code_3字段用到这一类型:

'country_iso_code_2' => array(

  'description' => 'The two-character ISO country code.',

  'type' => 'char',

  'length' => 2,Edit summary

  'not null' => TRUE,

  'default' => '',

),

如果default键未被设置,并且not null键被设置为了FALSE,那么默认值将被设置为NULL


Drupal版本:

8Text

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

Text字段用于大块的文本。例如,node_type表中的description字段就是这种类型。Text字段可以不使用默认值。

 

'description' => array(

  'description' => 'A brief description of this type.',

  'type' => 'text',

  'not null' => TRUE,

  'size' => 'medium',

  'translatable' => TRUE,

 ),

    其中size的类型可以为tinysmallmediumbignormal


Drupal版本:

9数字型

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

数字型数据类型是用来存储数字的,它包括integer(整数)、serial(序列数)、 float(浮点数)、 numeric(数字)类型。


Drupal版本:

第一章 Drupal的工作原理

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

 在本书中,我们首先介绍Drupal的基本原理,接着对Drupal开发中,所涉及到的基本技术和常见技巧,进行详细的描述。在很多的技术图书中,都会包含与软件安装相关的章节,由于本书主要是讲解Drupal模块开发的,我们在这里不会讲解Drupal的安装及相关模块的配置,对于Drupal的安装,由于相对来讲,是很简单的,并且网络上有很多这样的文档,所以在这里就不会为安装配置浪费笔墨了。

 

  在本章中,我们将为大家介绍什么是DrupalDrupal相关技术、Drupal核心术语、引导指令流程、Drupal特有的钩子机制。


Drupal版本:

1什么是Drupal

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

Drupal从2001年诞生到现在,经过了不断的版本演化和市场检验以后,日趋成熟和完善。Drupal已经超越一个传统意义上的CMS范畴,越来越多的程序员把它做看作一个内容管理框架(CMF),总之Drupal正在演化为一门平台性质的技术。我们可以从三个方面来理解Drupal:

Drupal是一个基于GPL协议开源的内容管理系统。Drupal7包含了CMS的各种标准功能。比如文章的发布、文件图片的管理、用户帐号管理、菜单导航、频道分类等等。我们使用Drupal,可以搭建各种网站,比如个人的博客,企业的宣传网站,B2C门户网站,视频网站。

Drupal是一个内容管理框架,它提供了各种方式,允许开发者使用可插拔的模块来定制Drupal的功能。Drupal的开发者和使用者,可以从drupal.org上面下载各种模块,来扩展现有的功能,并且可以按照Drupal的规范,自己开发模块以满足需求。

Drupal背后有一个开放的社区,Drupal的成功,与社区的活跃程度是分不开的。在2011年1月Drupal7发布时,全球有800多个社区成员向Drupal核心贡献了代码。有数以千计的开发者,向社区贡献了7000多个模块,还有更多的人,在测试、文档、用户支持、翻译等方面为Drupal作出了贡献。

 

    对于Drupal的理解,仁者见仁,智者见智。我们在这里,就不用过多的纠结于Drupal到底是一个CMS,还是一个CMF这样的争论了,我们只需要了解Drupal能够做什么,我们需要它做什么就可以了。Drupal把很多复杂的事情,简单化了;同时Drupal也把很多简单的事情,复杂化了。这也是我们为什么对Drupal又爱又恨的原因。


Drupal版本:

2drupal的技术堆栈

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


我们首先来看一下,在Drupal中,都会用到哪些技术。

2.1 PHP

  Drupal使用的编程语言是PHPhttp://php.net)。PHP是一个流行的、跨平台的、服务器端执行的脚本语言。所以熟悉PHP,对于学习Drupal开发很有帮助。但是这并不是说,熟悉PHP 是必须的,由于Drupal本身,在PHP的基础上,又做了大量的封装,很多功能只需要调用自己的API即可实现,所以其它语言的程序员,转学Drupal,并不比PHP程序员转学Drupal更加困难。

 

  因为PHP易于入门,所以大量的PHP代码都是由新手编写的。而新手的水平大家也知道,他们的代码总是存在这样或者那样的问题,这就给PHP的名声带来了比较坏的影响。不过, PHP也可以用于构建严谨的程序。Drupal核心中的所有代码都遵守了严格的编码规范(http://drupal.org/nodes/318),通过开源,Drupal代码也经过了成千上万人的锤炼。对于Drupal来讲,PHP的入门门槛比较低,这就意味着有更多的人能够为Drupal贡献代码,通过开源,会有很多人对这些代码进行检查,这样就保证了代码的质量。

 

  最后,需要注意的是,Drupal7所需的最低PHP版本是PHP5.2。由于在PHP5.2及后续版本中,面向对象编程正占据着主流地位,因而尽管Drupal本身是面向过程的,但是Drupal核心中的一些子系统,以及很多第3方模块,都广泛采用了面向对象编程技术。

 

2.2 web服务器

    最常用的web服务器就数Apache了,所以一开始Drupal就对Apache提供了内置支持。当然,这并不是说,Drupal不能运行在其它web服务器上。随着Drupal的流行,对其它web服务器的支持,也越来越完善了,比如IIS、lighttpd、nginx。最近两年,在Drupal的高性能应用实践中,越来越多的Drupal程序员把Nginx作为首选服务器,用以提升性能。

   我们在本书中不会涉及太多与web服务器相关的知识。这里值得一提的是,在Drupal中,简洁URL用到了web服务器的相关设置。有关简洁URL的相关配置,可以参看相关的文档。

 

2.3 数据库

  最初,Drupal对MySQL提供了内置支持,在Drupal的后续版本中,增加了对PostgreSQL的支持。在Drupal7中,内置支持了MySQL、PostgreSQL、SQLite三个数据库系统。由于Drupal7的数据库API,是完全基于PHP5的PDO,而PDO能够支持各种各样的数据库,比如Oracle、SQL Server、DB2,所以通过第3方模块,就可以实现Drupal7对Oracle、SQL Server,DB2的支持。Drupal 对商业数据库的支持,能够让Drupal 在更广泛的领域中得以应用。

 

2.4操作系统

  操作系统位于Drupal相关技术堆栈的最下面的一层,由于Drupal是基于PHP编写的,而PHP语言也具有跨操作系统的特点,所以只要操作系统能够支持PHP,我们就可以使用它来运行DrupalWindowsLinuxMac OS等等,在这些主流的操作系统上,都可以运行Drupal。操作系统层,主要负责网站相关的最底层的任务,比如网络连接,文件的权限。如果你是在linux下面安装运行Drupal,你经常会遇到文件夹权限不可写,这样的权限问题。

  本书的作者,使用的是Vista操作系统,所用的环境是XAMPP,所有的代码都是在这个操作系统下面编写的,相信这些程序也能够应用于其它操作系统及相关环境下。

 

2.5 HTMLCSSJavaScript

  Drupal系统中最终返回给浏览器的是HTML,而在HTML中,CSS是用来定义页面样式的,JavaScript则是浏览器客户端的脚本语言。需要注意的是,DrupalJavaScript的支持,是通过jQuery实现的。jQuery是一个优秀的轻量级的JavaScript框架 (压缩后只有21k) ,它兼容CSS3,还兼容各种浏览器。作为一个Drupal开发者,我们今后肯定会涉及到这三种技术。


Drupal版本:

3Drupal文件夹结构

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

我们从drupal.org上下载最新的Drupal7版本,把它结压缩后,放到本地的web文档目录下,这是我们就会看到Drupal核心文件夹下的目录结构了。

 

 

               Drupal7核心的默认文件夹结构

1.png

 

    通过了解Drupal默认安装的目录结构,我们能够了解一些最佳实践,比如下载的第3方模块和主题的放置位置,以及如何拥有不同的Drupal安装轮廓。我们来进一步的了解一下Drupal的文件夹目录:

 

3.1 includes 

  这个文件夹下面包含了Drupal通用的函数库,比如ajax、批处理、表单API、数据库API、本地化API等等。这些通用函数库,是Drupal程序运行的基石。

 

3.2 misc

这个文件夹下面包含了JavaScript文件,和其它各种图标和图片文件。其中JavaScript文件包括jquery.jsjquery.form.jsjquery.cookie.jsdrupal.jsajax.j、jQuery UI等等。

 

3.3 modules

   这个文件夹下面包含了所有核心模块,一个模块对应一个文件夹,总共40个模块。最好不要乱动这个文件夹(以及除profiles和sites外的其它文件夹)下面的任何东西。对于第3方模块、主题,或者自定义的模块、主题,应该放到sites目录下

 

3.4 profiles 

    这个文件夹下面包含一个站点的不同安装轮廓(profile)。Drupal7自带了两个profile,一个是标准化安装(standard),一个是最小化安装(minimal)。安装轮廓的主要目的是,用来自动的启用核心的或者第3方的模块,并作一些初始化设置。比如rszrama为了方便大家测试commerce模块,就提供了一个commercedev安装轮廓(https://github.com/rszrama/commercedev),使用这个profile,用户就能够方便的搭建一个电子商务测试站点。

 

3.5 scripts

    这个文件夹下面包含了许多脚本,这些脚本可用于语法检查、清理代码、从命令行运行Drupal、使用cron处理特定情况、以及运行单元测试等等。在Drupal自身程序运行过程中,调用不到这些脚本;这里面都是一些shellPerl的实用脚本。

 

3.6 sites

    这个文件夹下面用来放置Drupal的配置文件、第3方模块与主题、自定义模块与主题等等。你从第3方模块库中下载的模块,通常都放在sites/all/modules/standard下面;而你自己编写的模块,则放在sites/all/modules/custom目录下面。我们对Drupal所进行的任何修改,基本上都放在这个文件夹下进行。

   在sites下面有一个名为default的文件夹,里面包含了Drupal默认配置文件--- default.settings.php。在Drupal安装过程中,系统将会基于你提供的数据库帐号信息和这个默认文件,为你自动创建一个settings.php文件。对于多站点安装,配置文件通常位于sites/www.example.com/settings.php

 

    另外sites/default/files通常用作Drupal文件系统所在的目录。Web服务器需要具有这个子目录的读写权限。默认情况下,Drupal在安装时会自动为你创建这个文件夹,并检查是否设置了相应的权限。

 

3.7 themes

   这个文件夹下面包含了Drupal的模板引擎和默认主题。这里的默认主题有bartikgarlandseven等等。你下载的第3方主题以及自己创建的主题,不能放在这个位置,而应该放在sites/all/themes目录下面。

 

3.8 authorize.php

   这个PHP文件里面包含了运行认证文件操作的管理脚本。通过settings.php中的全局变量killswitch以及'administer software updates'权限,可以控制对这个文件中脚本的访问。

 

3.9 cron.php

   这个PHP文件用于执行定时任务,比如清理过期的缓存数据,以及计算统计信息。Drupal7在运行定时任务时,首先会检查cron_key是否正确,从而避免cron.php被恶意的调用执行。

 

3.10 index.php

   这个PHP文件是Drupal处理http请求的主入口程序。它就相当于一个路由器,用来将程序的执行控制权分发给合适的处理器上,而后者会输出相应的页面内容。

 

3.11 install.php 

   这个PHP文件是Drupal安装器的主入口程序。

 

3.12 update.php

    这个PHP文件是Drupal升级时的主入口程序。通过设置settings.php中的全局变量update_free_access,可以绕过升级时的权限检查。

 

3.13 xmlrpc.php 

    这个PHP文件用来接收XML-RPC请求,如果你的网站没有用到XML-RPC,那么可以将这个文件从中删除。

 

3.14 robots.txt

    这个文件是搜索引擎爬虫排除标准的默认实现。在这个文件中,你可以定义搜索引擎爬虫能够访问哪些页面,不能访问哪些页面。

 

  此外,.htaccess文件是apache的相关配置文件;而web.config则是IIS的配置文件,它是Drupal7中新增的一个文件。其余文件则是相关的文档文件。

 


Drupal版本:

4Drupal核心概念

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

  通过了解Drupal自身的文件夹结构,我们对Drupal有了初步的认识。我们还需要对Drupal中的常用概念,或者说专有术语,有更好的界定。这能够帮助我们更好的学习使用Drupal。常用的术语有模块、钩子、主题、节点、菜单、区块、字段、实体等等。Drupal相关术语不仅仅是这里所列的这么几个,还有更多的相关术语。有兴趣的读者,可以参看http://drupal.org/node/19828



Drupal版本:

4.1 模块

   作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com 
   我们浏览Drupal核心文件夹的modules目录,就会发现这里包含了许多子文件夹,正如前文中所述的那样,每个下面都有一个模块。而每一个模块,都实
现了特定的功能。通过模块的安装与卸载,我们就可以为Drupal站点添加或者删除特定功能。Drupal作为一个框架,其最大的一个优势,就是完全的模
块化。当我们建设一个站点时,我们只需要根据需求组装相应的模块就可以了;当然,这里所说的只是大多数时候。

 

    Drupal7的模块我们大致可以分为4类:核心必选模块、核心可选模块、第3方模块、自定义的模块。核心必选模块,位于modules目录,有字段(Field)、字段SQL存储(Field SQL storage) 、文本字段(Text)、过滤器(Filter)、节点(Node)、系统(System)、用户(User),这里需要注意的是modules/field目录下,包含了多个模块,其中3个模块是核心必选模块。modules目录中,除去必选模块以外,剩余的就是核心可选模块,核心可选模块的info文件中,不包含 “required = TRUE
这句话。第3方模块,就是由Drupal的社区成员,贡献到drupal.org上的模块,目前(2011年5月),drupal.org上有7000+
多个第3方模块,而且这个数量正在稳步增加。自定义模块,就是为了实现网站的特殊需求,程序员自己开发的模块,通常没有上传到drupal.org上。

 

    Drupal
本身是不向下兼容的,每个主版本之间,差别往往很大。随着主版本的升级,一些模块,原来是Drupal核心可选模块,后来变成了第3方模块,比如
drupal.module;有一些模块,原来是核心必选模块,后来变成了核心可选模块,比如block.module;也有一些模块,原来是第3方模
块,后来变成了核心可选或者必选模块,比如
simpletest,cck。如果你自己定义的模块,贡献到了drupal.org上,那么这个模块就变成了第3方模块。Drupal的模块,就像大自然一样,是在不断演化的,而且也存在优胜劣汰这样的自然法则。



Drupal版本:

4.2 钩子

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

  谈到模块,就不得不提到钩子这个概念。我有时也喜欢把钩子称作钩子机制,我们可以把钩子看做Drupal的内部事件。有时也可以钩子看作是特殊的回调函数。
模块就是通过钩子,与Drupal核心系统无缝整合在一起了。钩子是一个很抽象的概念,我们通过代码来理解一下钩子机制。

function module_invoke_all() {

  $args = func_get_args();

  $hook = $args[0];

  unset($args[0]);

  $return = array();

  foreach (module_implements($hookas $module) {

    $function = $module . '_' . $hook;

    if (function_exists($function)) {

      $result = call_user_func_array($function$args);

      if (isset($result) && is_array($result)) {

        $return = array_merge_recursive($return$result);

      }

      elseif (isset($result)) {

        $return[] = $result;

      }

    }

  }

 

  return $return;

}

 

    代码参考地址:

http://api.drupal.org/api/drupal/includes--module.inc/function/module_invoke_all

    函数module_invoke_all是理解钩子的关键,而这里面,foreach循环中,函数module_implements是理解钩子的又一关键,它用来获取所有实现了钩子$hook的模块。而在这个循环体代码里面,第一句给出了钩子的命名规范。模块在实现钩子的时候,必须采用“模块名_钩子名”形式。其余代码的含义,就是如果这个具体的钩子函数存在(function_exists($function)),那么就调用这个钩子函数(call_user_func_array($function, $args))。

 

    Drupal中的钩子大致可以分为3类,采用module_invoke_all调用的钩子是一类,也是最常见的;采用module_invoke调用的钩子是一类,这种钩子在Drupal核心中经常出现;还有一个就是主题钩子,比如theme_item_list,这里的item_list有时也被称为主题钩子。

 

    上面提到了module_invoke,让我们看一下它的代码:

<?php

function module_invoke() {

  $args = func_get_args();

  $module = $args[0];

  $hook = $args[1];

  unset($args[0], $args[1]);

  if (module_hook($module$hook)) {

    return call_user_func_array($module . '_' . $hook$args);

  }

}

?>

    在这个函数中,call_user_func_array函数只调用了一次,而在前面的module_invoke_all函数的代码中,它被循环调用了多次。这就是两类钩子之间的区别。

    有关钩子的更多信息,可以参看api.drupal.org上的在线文档,http://api.drupal.org/api/drupal/includes--module.inc/group/hooks/7。此外,如果一个模块对外提供了钩子,那么在这个模块的文件夹下面,通常会有一个modulename.api.php这样的文件,里面包含了钩子的具体说明,比如在用户模块下面,就有一个这样的文件user.api.php,里面包含了用户模块对外提供的所有钩子。

 

 


Drupal版本:

4.3 主题

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

  我们在前面的Drupal文件结构的分析中,提到过themes文件夹,这里面放置的就是Drupal核心自带的主题。Drupal中的主题主要负责把原始
数据转化为格式化的HTML输出。通常一个Drupal主题,由info文件、模板文件、template.php、CSS文件、JS文件、图片构成。我
们打开一个Drupal主题,比如garland,就会看到在模板文件中,里面包含了html代码片断和PHP变量。主题对外观的控制,就是通过这样的模
板文件和主题函数实现的。除了Drupal核心自带的这些主题外,
Drupal.org还有很多第3方的主题可用,比较常用的有Zen、fusion、tao等等(http://drupal.org/project/themes)。

 

    前面所说的主题,指的是具体的主题。在Drupal中,主题系统,所包含的含义就会更加广泛一些。除了上面的所说的具体的主题外,它还包含Drupal的主
题机制,包含在Drupal核心模块中、第3方模块中的各种模板文件和主题函数,以及Drupal的主题覆写。Drupal通过自己的主题系统,将逻辑层
与表现层作了分离;将逻辑与外观分离,这也是Drupal的最佳实践之一。在
includes文件夹下,有一个theme.inc文件,里面的代码包含了Drupal的主题覆写机制,也包含了各种预处理函数、处理函数、还有常用的主题函数,阅读这些代码,有助于大家熟悉Drupal的主题系统。

 

Drupal
通过主题系统来控制网站的外观,我们可以通过定制自己的主题,来实现自己的具体外观。在定制主题的过程中,对于Drupal核心或者第3方模块的默认输
出,我们通常有两种定制方式:一种就是使用CSS的覆写机制,重新定义CSS规则,保留原有的markup输出;这种方式的优点是,简单方便,缺点就是有
大量的垃圾html输出,在后续过程中,不易复用。另一种方式就是在自己的主题中通过覆写模板与主题函数实现,这样可以完全使用自己的markup输出、
使用自己的CSS规则;这种方式的优点是html代码干净、浏览器兼容性比较好,易于复用,缺点就是比较复杂,前期成本高。


Drupal版本:

4.4 区块

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

   区块一般指的是放置在模板中的边栏、页首、或者页尾中,主内容外的内容片断。通过区块管理界面,可以启用或者禁用这些信息。举例来说,我们通常在站点的页脚
处,显示的“版权信息”,就可以处理成区块;常常显示在站点边栏的“热门内容”,每个站点的主导航链接,都可以处理成区块;比如“用户登录”和“我的帐
号”功能,可以结合在一起,处理成区块显示,这样匿名用户看到的就是“用户登录”表单,而注册用户看到的就是“我的帐号”链接。

    我们可以控制单个区块的显示位置,比如显示在特定页面,显示给特定角色的用户,显示在特定节点类型的页面中,显示在特定的主题区域下。区块通常是放在区域中的,而区域的定义则位于主题的info文件中。


4.5 菜单

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

    在Drupal中,菜单有两层含义:一种是处理请求的路由系统,它会将页面请求所提供的URL映射到Drupal内部的回调函数上,这是Drupal程序员所关心的;另一种就是页面上的导航,它负责组织站点的内容关系。

 

    菜单具有层级的树形结构,一个菜单项下面可以有多个子菜单项,子菜单项下面又可以包含菜单项。需要注意的是Drupal菜单的层级,最多可以有9级,超过了9级,系统就不能正常工作了,而在实际的站点导航中,很难遇到包含9级的导航。

 


Drupal版本:

4.6 用户

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

    对于你站点的每位访问者,无论他拥有你站点的一个账号,或者是匿名访问,在Drupal中,都会将其处理成用户。每个用户都具有一个ID,注册用户还具有用
户名、电子邮件等信息。用户本身是实体的一个具体实现,所以我们可以为用户添加更多的字段;此外,使用profile2模块,就可以定义不同的
profile类型,并将其与用户关联起来。

 

    ID为0的用户为匿名用户,我们可以为匿名用户启用缓存。ID为1的用户,是Drupal站点的超级管理员,具有站点操作的各种权限。Drupal通过角色来
管理不同注册用户的权限。一个角色就代表着一组权限的集合。Drupal自带了3中角色:匿名用户、注册用户、管理员。根据站点的需求,可以添加更多的角
色,比如“站内编辑”。


Drupal版本:

4.7 字段与实体

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

    字段和实体,是Drupal7中新引入的两个概念。在Drupal7以前,人们使用CCK模块来扩展节点类型,为节点类型添加各种字段。这种方式渐渐的演变成为了主流方式,并最终在Drupal7中进入了Drupal内核。

 

    以
前,人们尝试着,将所有的内容都统一到节点上去,比如区块,评论,分类,用户profile,都存在相应的第3方模块将其实现为相应的节点类型。在
Drupal7中,核心开发者将这方面的努力做了进一步的抽象,把核心中的节点、分类、评论、用户都抽象成为了实体。实体具有相同的增删改查,可以为实体
添加更多的字段。

 

    第3方模块,Entity API正在成为Drupal7中的CCK模块,基于这个模块,我们可以方便的定义出来新的实体类型,比如在Ubercart和Commerce模块中,就基于这个模块将订单实现成为了一种新的实体类型。


Drupal版本:

5Drupal执行流程

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

  Drupal是如何处理一个HTTP页面请求呢?如果对于这样的请求处理,我们能够理解其基本的流程结构,那么对于今后的学习,将会很有帮助。如果你想追踪Drupal的代码执行过程,那么首先可以搭建好调试环境,从index.php文件开始,逐步设置断点,这样就可以快速的了解Drupal的基本原理了。让我们以一个简单的例子,来分析一下常用请求的处理流程。假定一个注册用户访问我的站点http://zhupou.cn,并浏览文章“Drupal入围2008全球开源CMS大奖赛决赛”,也就是访问路径http://zhupou.cn/node/88(我这里假定zhupou.cn已经升级到Drupal7)。

 

    1、首先,我们启用了简洁URLWeb服务器收到请求后,会按照URL重写把用户看到的URL,转换为系统可以理解的URL,在这里就是将http://zhupou.cn/node/88 转换为了http://zhupou.cn/index.php?node/88ApacheIISNginx都支持URL重写。

 

    2PHP开始执行Drupalindex.php文件,Drupal获取到内部路径“node/88.

 

    3Drupal启动完整地引导指令流程,完成资源的初始化,加载所有启用的模块后,将内部路径“node/88”映射到了节点模块的对应回调函数node_page_view上。

 

    4、节点模块执行node_page_view函数,经过node_page_view -à node_show à node_view_multipleànode_viewànode_build_content这样的持续回调,它从数据库中读取ID88的节点,并将返回的数据封装成一个drupal_render可以识别的数组。在node_viewnode_build_content函数中,先后触发了以下钩子函数:hook_field_prepare_view,hook_field_formatter_prepare_viewhook_entity_prepare_viewhook_field_formatter_viewhook_node_viewhook_entity_viewhook_node_view_alterhook_entity_view_alter

 

    5、主题系统获取节点88的数据信息,并将其使用html代码进行封装,同时应用CSS。主题系统获取与节点88相关的其它页面元素数据信息,并分别将其使用html代码进行封装,同时应用CSS

 

    6Drupal完成所有的处理后,把最终封装好的HTMLCSS数据传送给用户的浏览器。浏览器将这些数据显示成web页面,呈现给最终用户。

 

    这里重点需要理解的就是Drupal的引导指令,和相关的钩子函数调用。让我们对这两点进一步的解释。

1e30e924b899a901947c2b8d1d950a7b0308f5c4.jpg


Drupal版本:

5.1 引导指令

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

 我们在前面提到,Drupal会启动一个完整的引导指令,实际上,对于每个由Drupal处理的页面请求,Drupal都会启用自己的引导指令。Drupal的引导指令,定义在bootstrap.inc文件中,它包含八个阶段:

 

 

初始化配置阶段

在这一阶段,通过include_once()来解析settings.php文件,提取该文件中保存的键值信息,将会初始化Drupal的内部配置数组,并建立站点的基路径($base_url),获取HTTP请求的内部路径,初始化一些全局变量。

 

页面缓存阶段

在有些情况下,我们需要更高性能的站点,因此就需要不经过数据库就调用缓存系统。在页面缓存阶段,系统会加载自带的缓存处理器,并尝试加载第3方模块(比如memcached)的缓存处理器,如果启用了页面缓存,那么系统将会在本阶段直接返回缓存的页面,并终止引导指令后续阶段的执行。

 

初始化数据库阶段

在初始化数据库阶段,将会初始化数据库系统。需要注意的是,数据库连接只有在实际调用时,才会正式初始化;同时会注册自动加载的函数,这样我们就可以方便的访问系统中定义的类和接口了。

 

初始化变量系统阶段

 

在这个阶段,将会初始化Drupal的锁系统;加载所有的系统变量,注意这里没有覆写settings.php中的变量;加载引导指令模块。

 

初始化会话处理阶段

Drupal采用了PHP内置的会话处理机制,并在在此基础上,实现了自己用户层级的会话存储处理器,使用session_set_save_handler函数重载了SESSION存储方式,这样就可以使用数据库来存储会话信息了。这里我们需要注意的是,Drupal的会话信息不是存储在内存中的,而是存储在数据库中的。在本阶段,将会初始化或者重新构建会话。代表当前用户的全局对象$User也会在这一阶段初始化,不过出于效率的考虑,并不是对象的所有属性都是可用的(当需要时,可以通过明确的调用函数user_load()来加载这些属性)。

 

设立页面头部阶段

在这个阶段,会使用bootstrap_invoke_all触发hook_boot钩子,系统会使用这个钩子来设置一些全局参数,以供后面调用;同时还会初始化锁系统;发送默认的HTTP头部。需要注意的是,在调用hook_boot钩子时,大多数的模块和许多通用函数库还没有加载进来,它是处理页面请求所调用的第一个钩子,比hook_init还要早。如果你所用的模块实现了这个钩子,那么会在“性能”管理页面,提示你这个模块与激进缓存模式不兼容。

 

语言判定阶段

    在这个阶段,会初始化所有已定义的语言类型。如果站点启用了多语言特性,系统会基于语言协定设置,为每个给定类型选择一个语言;同时在多语言环境下,完成了语言系统初始化后,还会调用hook_language_init钩子。

 

完成

该阶段是引导指令的最后一个阶段,它包括加载一些通用函数库,比如path.inctheme.incpager.incmenu.incfile.incform.incajax.inctoken.inc等等。在这里将设置Drupal定制的错误处理器,并加载所有启用了的模块。同时还会初始化Drupal内部路径,初始化主题系统。最后Drupal调用hook_init钩子,这样在对请求正式处理以前,为相应模块提供一个交互的机会。

 


Drupal版本:

5.2 钩子的执行顺序

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

  前面我们在Drupal概念中讲到钩子,现在进一步的对其进行分析,掌握了Drupal的钩子机制,熟悉各种常用的钩子,这是Drupal开发过程中的必备条件。

 

    钩子的触发,通常使用module_invoke_all来完成,这是一种常用的方式,但是还存在其它的方式。比如在引导指令阶段,就使用了bootstrap_invoke_all触发hook_boot钩子。代码如下:

   bootstrap_invoke_all('boot');

   

    而在node_build_content函数中,hook_node_view_alterhook_entity_view_alter钩子的触发,则是使用drupal_alter完成的,代码如下:

    drupal_alter(array('node_view', 'entity_view'), $build, $type);

 

   而hook_field_prepare_viewhook_field_formatter_prepare_view钩子的触发,使用的代码分别是:

_field_invoke_multiple('prepare_view', $entity_type, $prepare);

_field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode);

 

    注意,这两个都是内部函数,专门用于field相关的模块。后者对前者作了封装,最终将使用field_default_prepare_view()来触发hook_field_formatter_prepare_view钩子。

 

    这些触发钩子的函数,尽管名字各不相同,但是里面的核心代码,是一致的。

 

 foreach ($modules as $module) {  

    $function = $module . '_hookname';

    if (function_exists($function)) {

      $function();

    }

  }  

   

    如果多个函数都实现相同的钩子,那么这些钩子之间的执行顺序是怎么决定的呢?Drupal首先会使用模块的重量进行排序,按照顺序依次执行。重量越小,越靠前;重量越大,越靠后。但是通常情况下,模块的重量都默认为0。在重量相同的情况下,则按照模块名字的字母顺序进行排列。比如,我们使用form_alter模块修改特定表单,如果有多个模块同时修改了该表单,那么form_alter钩子的执行顺序将会对结果产生影响。而默认的按模块名字的字母顺序执行,有时候并不能得到我们想要的结果,这个时候我们可以调整模块本身的重量。示例代码如下(这里假定模块名字为module_name):

db_update('system')  

  ->fields(array(    

    'weight' => 999,      

  ))  

  ->condition('name', 'module_name')  

  ->execute();

 

    我画了一张钩子执行顺序的流程图,希望能够方便大家理解构字的执行顺序。

1.png 

                   钩子执行顺序图

 

 

总结

读完本章以后,你应该能够

理解Drupal是什么,

Drupal核心的文件结构、

Drupal常用术语,

Drupal处理http请求的大致流程,

引导指令流程,

钩子回调流程。

 



Thinkindrupal.com

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

   Thinkindrupal.com是老葛的个人网站,主要面向自己的各种读者群,为大家提供优质的Drupal中文技术资源。

    Thinkindrupal.com主要解决Drupal的中文文档的问题,尽可能的去改进,完善,增加各种各样的Drupal中文文档,以此来帮助更多的人学习使用Drupal

    我们知道,Drupal不同于JavaPHP这样的平台性的技术,后面有专门的商业公司的支持。Drupal作为一个开源项目,像所有的其它开源项目一样,文档方面是比较缺乏的,对于一个非中文的项目,中文文档更缺乏。文档的建设始终是开源软件领域的冷板凳,Drupal也不例外。Thinkindrupal.com希望通过自己的努力,帮助更多的英文不好的朋友了解Drupal,使用Drupal,通过降低大家的成本来收益。

  

 


Drupal版本:

作者

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

    老葛,原名葛红儒,自由职业者Thinkindrupal.com站长。2u4u高级技术顾问。

    同学同事都管我叫老葛,叫着叫着就习惯了。现在在北京,专职从事Drupal方面的培训咨询技术支持、图书创作、英文文档翻译工作,是国内多家知名Drupal站点的技术顾问。

    专职从事Drupal工作4年有余,先后参与过150余个Drupal项目,比如intel Flex、2u4u、中华书局网上书店、FLTRP等等主要工作是为这些网站开发Drupal模块和以及相关的技术培训与指导工作。

    先后翻译编写过1000余页的Drupal技术文章。比如《Drupal简明教程》 、  《Drupal6主题制作指南》、《Drupal5主题制作指南》、 《Drupal专业开发指南第一版、 《Drupal专业开发指南第二版、 《Drupal菜鸟20问》,以及最近创作编写翻译的《Think In Drupal》等等

    为Drupal.org上贡献过多个模块,比如Ubercart的支付宝模块、Block_morelink、Field_validation、Image URL Formatter等等。

    在Drupal6下,先后汉化了Ubercart, Views, OG, CCK, Ctools, Panels, LightBox2, FileField, ImageField, ImageCache, ImageAPI, Token, Admin_menu, backup_migrate, Date, Calendar, nodequeue, nodewords, poormanscron, tagadelic, webform等模块。极大的降低了中国人学习使用Drupal的成本.

    先后回答过网友付费会员各种Drupal问题数百个。

 

 


Drupal版本:

第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版本:

第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版本:

第6章 Form API

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

Drupal提供了一套表单API,用来生成、验证和处理HTML表单。表单API将表单抽象为一个关联数组,里面包含了各种属性和对应的值。在生成表单页面时,呈现引擎会在适当的时候将数组呈现出来。表单API为我们带来了很多好处,由于我们将表单表示成为了结构化的数组,所以我们可以添加、删除、重新排序、和修改表单。当我们想用一种干净的方式,来对其它模块创建的表单进行修改时,这会特别方便。此外,表单API还对表单操作进行了保护,从而能够有效的防止表单注入攻击。对于任意的表单,我们可以使用表单API为其添加附加的验证和处理函数。

 

    当然,表单API再给我们带来很多便利和灵活性的同时,也给我们带来了高昂的学习成本,除此以外,定制表单的外观,通常也是非常琐碎和费时的工作。

 

    我们在前面已经接触过Drupal的表单了,并且用到了hook_form_alterhook_form_FORM_ID_alter。对于表单、表单元素应该有了感性的认识。如果以前,就使用过Drupal6下面的表单API写过模块,那么接下来的内容,就不难理解了。

 

在本章中,我们将迎难而上。首先通过两个实际的例子,来学习Drupal表单API最后,我们列出来了Drupal核心自带的各种表单元素,及实例代码,并对呈现(render)API做了简单介绍。


Drupal版本:

1 两步表单

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

 

    让我们来看第一个实例,我们先介绍一下,实例的背景。我们想建立一个联系我们的表单页面,我们知道有两种方法可用,一是使用Drupal核心自带的contact模块,另一个就是使用webform模块,通常这两个模块可以满足大多数的情况。但是假如,客户希望用户提交联系我们表单的时候,先确认一下,确认无误以后再提交。这在实际中,也是常见的需求。此时,contact模块和webform模块就不大适用了。每个客户对于表单元素的需求也各不相同,所以此时我们可以选择通过定制自己的模块来实现。

 

    假定客户的具体需求是这样的,在联系表单页面,用户可以输入姓名、单位名称、电子邮件、电话号码,邮件正文、访问来源等信息,用户填写了这些信息后,提交表单,进入确认页面;在确认页面,用户可以检查填写的信息,如果信息有误,可以返回来修改,如果信息无误,正式提交;正式提交后,用户看到一个致谢页面,用来“感谢用户的来信”。


Drupal版本:

10 AJAX表单

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

 

    Drupal7中,不再使用“AHAH”术语了,而是采用更通用的大家已经习以为常的“Ajax”。有时候,错误的表述被一而再,再而三的使用以后,也就成为了标准,Ajax就是这样。在7里面,表单APIAJAX提供了进一步的支持,我们可以方便的创建Ajax表单,实现动态表单的效果,而不需要借助于第三方模块,这比Drupal6进步了很多。

 

    现在就让我们通过一个实例,来实际学习一下Ajax表单吧。我们在构建各种站点时,经常会遇到让用户选择他所在的地区。比如他的出生地,他的户口所在地,他的常住地址。当一个销售商品时,我们需要客户的配送地址。中国的地址信息,通常包含省、市、县这一信息,为了让网站更加友好,通常将省市县做成联动的表单。我们的这个例子,就是在Drupal中实现省市县三级联动。

 


Drupal版本:

11准备工作

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

   首先,我们先做一些准备工作。Drupal7中,分类术语被处理成了实体,与以前的版本相比,Drupal7下面的分类更好用了。我们不妨把省市县信息存储成为分类,这样以后用起来也更加方便。首先我们创建一个“地区”词汇表,如图所示。

图片1.png 

在里面添加一些省市县测试信息。

图片2.png 

    注意,这里面,长垣县在20116月以前,是隶属于新乡市的,201161日改为省辖县(市)了,不过这里还是把它放在了新乡市下面,长垣是我老家。题外话。北京市由于是直辖市,所以这里给了一个中间层“区/县”。

 

    实际的省市县信息,可以借助于第三方模块,比如feedsmigrate,导入到Drupal的分类系统中去。这里就不再详细介绍了。


Drupal版本:

12创建相关文件

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

    接下来,就是创建我们的模块,不妨把这个模块叫做“shengshixian”,在sites/all/modules/custom目录下创建一个shengshixian目录,接着创建两个文件,shengshixian.infoshengshixian.module。我们向info文件添加以下内容:

 

name = 省市县

description = 中国省市县三级联动.

core = 7.x

 

    接着向module文件添加逻辑代码:

 

<?php

/**

 * @file

 * 省市县三级联动实例代码,

 */

 

 

/**

 * 实现钩子hook_menu().

 */

function shengshixian_menu() {

  $items['ssx'] = array(

'title' => '省市县',

    'page callback' => 'shengshixian_test_page',

    'access callback' => TRUE,

    'type' => MENU_CALLBACK,

  );

 

  return $items;

}

 

/**

 * 路径“ssx”页面的回调函数

 */

function shengshixian_test_page(){

$render_array = array();

//我们为这个页面设置标题

drupal_set_title('省市县三级联动');

//页面的正文为一个表单,注意drupal_get_form返回的不是html,需要使用drupal_render呈现一下。

$render_array['#markup'] .= drupal_render(drupal_get_form('shengshixian_test_form'));

//Drupal7的页面回调,返回的应该是一个数组,只有在退化形式下,才返回字符串。

return $render_array;

}

 

    我们首先建立一个测试菜单路径“ssx”,然后添加了它的回调函数shengshixian_test_page。这里强调一下,drupal_get_form返回的不是html,需要需要使用drupal_render呈现一下,这与Drupal6下的有所不同。现在我们来看看这个表单的逻辑:

 

/**

 * 表单shengshixian_test_form的构建函数

 */

function shengshixian_test_form($form, &$form_state){

  //设置省市县对应元素的默认值

    $default_sheng = !empty($form_state['values']['sheng']) ? $form_state['values']['sheng'] : '';

$default_shi = !empty($form_state['values']['shi']) ? $form_state['values']['shi'] : '';

$default_xian = !empty($form_state['values']['xian']) ? $form_state['values']['xian'] : '';


//构建省份的选项数组,首先设置了一个提示语

    $sheng_options = array(

  '' => '请选择省份',

);


//向数据库中查询省份信息,

$query_sheng = db_select('taxonomy_term_data','ttd')

->fields('ttd', array('tid', 'name'));


//因为省份是第一级术语,分类术语的父亲为0

$query_sheng->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid ');

$query_sheng->condition('tth.parent', 0);


//需要确定术语所在的词汇表,就是我们在前面创建的地区

$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid ');

$query_sheng->condition('tv.machine_name', 'diqu');


//按照tid排序,并执行

$result_sheng = $query_sheng->orderBy('tid')->execute();


//将返回的结果,进行迭代,为$sheng_options赋值。

foreach ($result_sheng as $record) {

  $sheng_options[$record->tid] = $record->name;

}

//省份表单元素

$form['sheng'] = array(    

  '#title' => t('请选择您所在的省份?'),    

  '#type' => 'select',    

  '#options' => $sheng_options,    

  '#default_value' => $default_sheng, 

    //#ajax属性数组  

  '#ajax' => array(      

    'callback' => 'shengshixian_sheng_callback',      

    'wrapper' => 'shi-wrapper-div',      

    'method' => 'replace',      

    'effect' => 'fade',    

  ),  

);


//构建市的选项数组,首先设置了一个提示语

$shi_options = array(

  '' => '请选择市',

);

//在省份不为空的情况下,取该省份下的所有的市

if(!empty($default_sheng)){


  //向数据库中查询术语信息,

$query_shi = db_select('taxonomy_term_data','ttd')

->fields('ttd', array('tid', 'name'));

//将其父术语限定在前面的省份的具体值上

$query_shi->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid ');

$query_shi->condition('tth.parent', $default_sheng);

//由于省份信息里面,已经包含了词汇表信息,所以我们不再需要关联这个taxonomy_vocabulary表。

//$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid AND tv.machine_name = :machine_name', array(':machine_name' => 'diqu'));


//按照tid排序,并执行

$result_shi = $query_shi->orderBy('tid')->execute();


//将返回的结果,进行迭代,为$shi_options赋值。

foreach ($result_shi as $record) {

$shi_options[$record->tid] = $record->name;

}

 

}

/*

//测试代码,中间测试的时候用的,这里保留了,开发模块所用到的测试代码很多,多数都已删除。

  $form['test'] = array(  

  '#markup' => '123456:'.$default_sheng 

);

*/

//表单元素市

$form['shi'] = array(    

  '#title' => t('请选择您所在的市?'),    

'#prefix' => '<div id="shi-wrapper-div">',    

'#suffix' => '</div>',

  '#type' => 'select',    

  '#options' => $shi_options,    

  '#default_value' => $default_shi,

    '#ajax' => array(      

    'callback' => 'shengshixian_shi_callback',      

    'wrapper' => 'xian-wrapper-div',      

    'method' => 'replace',      

    'effect' => 'fade',    

  ),      

);


//构建县的选项数组,首先设置了一个提示语

$xian_options = array(

  '' => '请选择县',

);

//在市不为空的情况下,取该市下的所有的县

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


  //向数据库中查询术语信息,

$query_xian = db_select('taxonomy_term_data','ttd')

->fields('ttd', array('tid', 'name'));


//将其父术语限定在前面的市的具体值上

$query_xian->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid AND tth.parent = :parent', array(':parent' => $form_state['values']['shi']));

$query_xian->condition('tth.parent', $default_shi);


//由于最前面省份信息里面,已经包含了词汇表信息,所以我们不再需要关联这个taxonomy_vocabulary表。

//$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid AND tv.machine_name = :machine_name', array(':machine_name' => 'diqu'));


//按照tid排序,并执行

$result_xian = $query_xian->orderBy('tid')->execute();


//将返回的结果,进行迭代,为$xian_options赋值。

foreach ($result_xian as $record) {

$xian_options[$record->tid] = $record->name;

}

}


//表单元素县

$form['xian'] = array(    

  '#title' => t('请选择您所在的县/?'),    

'#prefix' => '<div id="xian-wrapper-div">',    

'#suffix' => '</div>',

  '#type' => 'select',    

  '#options' => $xian_options,    

  '#default_value' => $default_xian,

    

);


  //提交按钮

  $form['submit'] = array(

    '#type' => 'submit',

    '#value' => t('提交'),

  );

  return $form;

}

 

/**

 * 表单元素sheng,它的值变更时,对应的Ajax回调函数。

 */

function shengshixian_sheng_callback($form,&$form_state){

  //根据当前省份,重新确定市的可选项。返回重新构建的表单元素shi

return $form['shi'];

}

 

/**

 * 表单元素sheng,它的值变更时,对应的Ajax回调函数。

 */

function shengshixian_shi_callback($form,&$form_state){

  //根据当前所选的市,重新确定县的可选项。返回重新构建的表单元素xian

return $form['xian'];

}

  

    对于上面的代码,对于从分类相关表中读取对应的省市县信息的代码,里面有详细的注释,相关的数据库操作,可参看数据库API一章。对于省市县对应的三个表单元素,这里都选用了下拉选择框。


Drupal版本:

13Ajax表单的三个关键要点

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

    这里值得我们学习的地方有三点,一个是

$default_sheng = !empty($form_state['values']['sheng']) ? $form_state['values']['sheng'] : '';

$default_shi = !empty($form_state['values']['shi']) ? $form_state['values']['shi'] : '';

$default_xian = !empty($form_state['values']['xian']) ? $form_state['values']['xian'] : '';

 

还有一个是:

'#ajax' => array(      

'callback' => 'shengshixian_sheng_callback',      

'wrapper' => 'shi-wrapper-div',      

'method' => 'replace',      

'effect' => 'fade',    

), 

 

'#ajax' => array(      

'callback' => 'shengshixian_shi_callback',      

'wrapper' => 'xian-wrapper-div',      

'method' => 'replace',      

'effect' => 'fade',    

),

最后便是Ajax的回调函数:

 

function shengshixian_sheng_callback($form,&$form_state){

  //根据当前省份,重新确定市的可选项。返回重新构建的表单元素shi

return $form['shi'];

}

 

function shengshixian_shi_callback($form,&$form_state){

  //根据当前所选的市,重新确定县的可选项。返回重新构建的表单元素xian

return $form['xian'];

}

 

    这三处,是Ajax表单的关键所在。对于第一处,获取默认的表单元素的值,看似非常普通,但确实是一个关键的地方。我们先来描述一下大致的执行流程。当我们访问页面ssx时,我们看到了一个普通的表单。如图所示:


Drupal版本:

14地区三级联动

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

图片1.png

Drupal版本:

15 Ajax表单流程分析

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

    此时,省份的可选项有“北京市”、“河南省”。市的可选项为空,县的可选项为空。也就是初次访问这个页面时,$default_sheng$default_shi$default_xian的值都为空。

    接着,当我们在表单页面选择“河南省”的时候,系统进行了Ajax调用,如图所示:

图片1.png 

    我们看到这里出现了“请稍等”的提示,表明系统进行了Ajax调用。聪明的读者会想到,Ajax回调应该就是hengshixian_sheng_callback了,这样理解是对的,但是还要再补充一点,那就是Drupal在后端重新构建了整个表单,在构建完整个表单后,根据hengshixian_sheng_callback回调函数,向浏览器端只返回变化的元素$form['shi'],浏览器端根据服务器端的返回值,重新设置了 “市”这一表单元素。

    在重新构建整个表单的过程中,此时$default_sheng的值,就是“河南省”的tid了,已经不再为空,所以此时“市”的可选项就不再为空了。

    当我们选择“新乡市”,系统再次进行了Ajax调用,如图所示:

图片2.png 

    此时,Drupal在后端再次重新构建了整个表单,在构建完整个表单后,根据shengshixian_shi_callback回调函数,向浏览器端返回更新过的元素$form['xian'],浏览器端根据服务器端的返回值,重新设置了 “县”这一表单元素。

   在重新构建整个表单的这次过程中,此时$default_sheng的值,就是“河南省”的tid了,此时“市”的可选项不为空。此时$default_shi的值,就是“新乡市”的tid了,所以此时“县”的可选项就不再为空了。如图所示:

图片3.png 

 

    当我们做完这样的流程分析以后,我们再来理解这段代码:

'#ajax' => array(      

'callback' => 'shengshixian_sheng_callback',      

'wrapper' => 'shi-wrapper-div',      

'method' => 'replace',      

'effect' => 'fade',    

),

    'callback'对应的是回调函数,通常这个回调函数非常简单,返回表单中的特定部分就可以了,注意这里是系统重新构建了整个表单后;'wrapper'表示动态更新的部分,这里对应于'#prefix' => '<div id="shi-wrapper-div">',也就是表单元素“市”的主内容;'method'表示对wrapper中内容的处理方法,可选值有'replace' (默认) 'after' 'append' 'before''prepend',这里是'replace''effect'表示Ajax特效,可选值有'none' (默认) 'fade''slide'

 

    最后,我们做一下归纳与总结,Drupal 7 中的Ajax表单,用起来非常简单,不需要编写自己的JQuery代码,回调函数也极其简单,只要我们遵守Drupal的规范,其余的工作都有Drupal替我们完成。从本质上来讲,这里的Ajax表单,就是多步表单的一个特例,我在前面,多次强调,重新构建整个表单,就是为了突出它其实就是一个多步表单。

 

    构建Ajax表单,需要注意三点:一是为带有ajax属性的元素提前设置默认值,二是正确的设置表单元素的ajax属性,三是编写ajax的回调函数。

 

    有关表单元素Ajax属性的更多信息,可参考http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html#properties


Drupal版本:

16 表单元素

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

http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html#properties

 

    在本节中,我们将通过例子来学习Drupal核心自带的表单元素。表单元素类型定义在

hook_element_info中,Drupal核心表单元素大部分都定义在systemk_element_info。使用element_info($type)函数可以查看元素类型的默认属性。为方便大家查阅,这里按照字母顺序排列。


Drupal版本:

17 Actions(动作)

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

    它是一个包装器(wrapper)元素,用来对其它按钮元素进行归类分组。把'actions'元素作为数组的键,可以方便设置主题样式,同时也方便其它模块修改该表单的动作。

 

示例代码,来自(node.admin.inc): 

$form['filters']['status']['actions'] = array(

    '#type' => 'actions',

    '#attributes' => array('class' => array('container-inline')),

  );

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

    '#type' => 'submit',

    '#value' => count($session) ? t('Refine') : t('Filter'),

  );

 

常用属性: #access #after_build#attributes #children #id#parents#post_render #pre_render #prefix #process #states #suffix #theme#theme_wrappers #tree #type#weight


Drupal版本:

18Button(按钮)

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

    按钮元素除了属性#executes_submit_callback默认为FALSE以外,其它属性与提交按钮元素完全相同。属性#executes_submit_callback告诉Drupal是否需要处理表单,为TRUE时处理表单,为FALSE时则简单的重新呈现表单。和提交按钮元素一样,可以将特定的验证和提交函数直接分配给这个按钮元素。

 

示例代码,来自(simpletest\tests\form_test.module):

 

  $form['continue_button'] = array(

    '#type' => 'button',

    '#value' => 'Reset',

    // Rebuilds the form without keeping the values.

  );

 

常用属性: #access #after_build #ajax #attributes #button_type (默认为submit) #disabled #element_validate #executes_submit_callback ((默认为FALSE) #limit_validation_errors #name ((默认为op) #parents #post_render #prefix#pre_render#process #submit #states #suffix #theme #theme_wrappers #tree #type #validate #value #weight

 


Drupal版本:

19Checkbox(复选框)

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

单个复选框。

示例代码,来自(node.pages.inc):

 

$form['revision_information']['revision'] = array(

    '#type' => 'checkbox',

    '#title' => t('Create new revision'),

    '#default_value' => $node->revision,

    '#access' => user_access('administer nodes'),

  ); 

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #return_value (默认为 1)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display (默认为after)、 #tree、 #type、 #weight

 


Drupal版本:

20Checkboxes(复选框)

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

    一组复选框。属性#options是一个关联数组,它的键是复选框的#return_value,对应的值则是用来显示给用户的。#options数组的键不能为0,假如为0了,那么系统就无法判断是否选中了这个选项。

 

示例代码,来自(node.module):

 

    $types = array_map('check_plain', node_type_get_names());

    $form['advanced']['type'] = array(

      '#type' => 'checkboxes',

      '#title' => t('Only of the type(s)'),

      '#prefix' => '<div class="criterion">',

      '#suffix' => '</div>',

      '#options' => $types,

    );

    在验证和提交函数中,通常使用array_filter()函数来获取复选框的键。在上述代码对应的验证函数中,就使用了array_filter()。代码如下:

 

$form_state['values']['type'] = array_filter($form_state['values']['type']);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree (默认为TRUE)、 #type、 #weight


Drupal版本:

21 Container(容器)

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

    一个html容器,用来封装子元素。把子元素放在一个<div>中,并可以为这个div设置类或ID属性。

 

 

示例代码,来自(user.admin.inc):

 

  $form['filters']['status'] = array(

    '#type' => 'container',

    '#attributes' => array('class' => array('clearfix')),

    '#prefix' => ($i ? '<div class="additional-filters">' . t('and where') . '</div>' : ''),

  );

  $form['filters']['status']['filters'] = array(

    '#type' => 'container',

    '#attributes' => array('class' => array('filters')),

  );

  foreach ($filters as $key => $filter) {

    $form['filters']['status']['filters'][$key] = array(

      '#type' => 'select',

      '#options' => $filter['options'],

      '#title' => $filter['title'],

      '#default_value' => '[any]',

    );

  }

 

常用属性: #access、 #after_build、 #attributes #children、 #id、 #parents、 #post_render、 #pre_render、 #prefix、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight

 



Drupal版本:

22 Date(日期)

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

    日期元素,它是一个由3个下拉选择框联合而成的元素。如果没有提供默认值,#default_value则默认为今天的日期。#default_value#return_value的格式是一个包含三个元素的关联数组,对应的键为:'year'month''day'。例如:

array('year' => 2011, 'month' => 7, 'day' => 26)

 

示例代码,来自(profile.module):

 


$fields[$category][$field->name] = array(
  '#type' => 'date', 
  '#title' => check_plain($field->title), 
  '#default_value' => $edit[$field->name], 
  '#description' => _profile_form_explanation($field), 
  '#required' => $field->required
);

    Date元素默认的显示顺序是“月,日,年”,对于中文用户来说,这个顺序不是很习惯,要把它改为“年,月,日”,只需要在“admin/config/regional/date-time”页面,简单的配置一下默认的日期格式就可以了,将其设置为中文用户习惯的日期格式,比如“Y/m/d H:i”。

 

常用属性: #access、 #after_build、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight

    属性#process默认设为form_process_date (),在该方法中年选择器被硬编码为从1900到2050。属性#element_validate默认设为date_validate()(两个函数都位于includes/form.inc中)。当你在表单中定义日期元素时,通过定义这两个属性,就可以使用你自己的代码来替代默认设置了。


Drupal版本:

23 fieldset(字段集)

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

    字段集元素是用来对其它表单元素进行归类分组的。可将其声明为可伸缩的,这样当用户查看表单并点击字段集标题时,由Drupal自动提供的JavaScript能够动态的打开和关闭字段集。

 

示例代码,来自(node.module):

 

  $form['visibility']['node_type'] = array(

    '#type' => 'fieldset',

    '#title' => t('Content types'),

    '#collapsible' => TRUE,

    '#collapsed' => TRUE,

    '#group' => 'visibility',

    '#weight' => 5,

  );

 

常用属性: #access、 #after_build、 #attributes、 #collapsed (默认为FALSE)、 #collapsible (默认为FALSE)、 #description、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight

 


Drupal版本:

24 File(文件)

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

 

创建了一个文件上传字段。注意,如果你使用了文件元素,那么你需要在你表单的根部设置属性enctype:

$form['#attributes']['enctype'] = 'multipart/form-data';

    代码enctype="multipart/form-data"在浏览器处理文件时是必须的。

 

示例代码,来自(user.module):

 

 ['picture']['picture_upload'] = array(

    '#type' => 'file',

    '#title' => t('Upload picture'),

    '#size' => 48,

    '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),

  );

 

常用属性: #access、 #after_build、 #array_parents、 #attached、 #attributes、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

25 hidden(隐藏域)

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

    该元素把值存放在了一个隐藏的表单字段中。注意,如果你想使用JavaScript来修改隐藏元素的值,此时应该使用#default_value属性,而不是#value属性。

示例代码,来自(node.admin.inc):

 

    $form['nodes'][$nid] = array(

      '#type' => 'hidden',

      '#value' => $nid,

      '#prefix' => '<li>',

      '#suffix' => check_plain($title) . "</li>\n",

    );

 

常用属性: #access、 #after_build、 #ajax、 #default_value、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #value、 #weight


Drupal版本:

26 mage_button(图片按钮)

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

    图片形式的表单提交按钮。它与提交按钮元素属性基本相同,但有两点例外。首先,它有一个#src属性,使用一个图片的URL作为它的值。其次,它把内部表单属性#has_garbage_value设置为了TRUE,这样就会阻止使用#default_value属性,从而避免在微软IE浏览器中的bug。不要在图片按钮中使用#default_value属性。图片按钮的值可从$form_state['clicked_button']['#value']中取出。

 

 

示例代码,来自(field_ui.admin.inc):

 

        $table[$name]['settings_edit'] = $base_button + array(

          '#type' => 'image_button',

          '#name' => $name . '_formatter_settings_edit',

          '#src' => 'misc/configure.png',

          '#attributes' => array('class' => array('field-formatter-settings-edit'), 'alt' => t('Edit')),

          '#op' => 'edit',

          // Do not check errors for the 'Edit' button, but make sure we get

          // the value of the 'formatter type' select.

          '#limit_validation_errors' => array(array('fields', $name, 'type')),

          '#prefix' => '<div class="field-formatter-settings-edit-wrapper">',

          '#suffix' => '</div>',

        );

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #button_type (默认为'submit')、 #disabled、 #element_validate、 #executes_submit_callback (默认为TRUE)、 #limit_validation_errors、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #return_value (默认为TRUE)、 #src、 #submit、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #validate、 #value、 #weight

 


Drupal版本:

27 item(条目)

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

     条目元素的格式与其它输入表单元素相同,但有一点不同,那就是它是只读的,没有输入框。由于这个元素是只读的,所有#required属性基本没有用处,除非你为了好看,想在#title旁边添加一个表示必填的红色图标。

 

示例代码,来自(user.admin.inc):

 

$form['permission'][$perm] = array(

'#type' => 'item',

'#markup' => $perm_item['title'],

'#description' => theme('user_permission_description', array('permission_item' => $perm_item, 'hide' => $hide_descriptions)),

);       

 

常用属性: #access、 #after_build、 #description、 #element_validate、 #markup、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

28 machine_name(机读名字)

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

    创建一个单行的文本字段,用来存储唯一的机读名字。对这个字段会进行验证,以确保它是唯一的,并且不包含非法字符。所有的非法字符将会通过JavaScript替换为有效字符。注意这个元素对中文的支持不是很友好。

 

    它还包含一个非标准的元素属性#machine_name,它是一个关联数组,可包含以下键existssourcelabelreplace_patternreplace。exists是一个回调函数,用来检查机读名字是否存在;source指的是表单元素的#array_parents包含的用户可读名字,可用作机读名字的源,默认为array('name')label,机读名字的标签文本,默认为“机读名字”;replace_pattern,正则表达式,用来匹配机读名字中不允许的字符,默认为'[^a-z0-9_]+'replace,在机读名字中,用来替换非法字符的字符,默认为'_'

 

示例代码,来自(content_type.inc):

 

$form['type'] = array(

'#type' => 'machine_name',

'#default_value' => $type->type,

'#maxlength' => 32,

'#disabled' => $type->locked,

'#machine_name' => array(

'exists' => 'node_type_load',

),

'#description' => t('A unique machine-readable name for this content type. It must only contain lowercase letters, numbers, and underscores. This name will be used for constructing the URL of the %node-add page, in which underscores will be converted into hyphens.', array(

'%node-add' => t('Add new content'),

)),

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #autocomplete_path (默认为FALSE)、 #default_value、 #description (默认为 '一个唯一的机读名字。只能包含小写的字母,数字和下划线。')、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为64)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required (默认为TRUE)、 #size (默认为60)、 #states、 #suffix、 #theme (默认为'textfield')、 #theme_wrappers (默认为'form_element')、 #title (默认为'机读名字')、 #title_display #tree、 #type、 #weight

 


Drupal版本:

29 managed_file(受管理的文件)

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

    提供一个完整的ajax控件,用来上传文件并将其保存到{file_managed}表中。它包含一组表单元素,有两个'#submit',一个用来上传,一个用于删除;一个'#file' 元素;多个'#hidden''#markup'元素,用于处理进度条和显示已上传的文件。

 

    注意:新上传的文件,如果默认状态为0,那么它们会当作临时文件,每隔6个小时被清空一次。如果你的模块用到这个元素了,那么你需要自己负责修改$file对象的状态,将其改为FILE_STATUS_PERMANENT,并保存到数据库中。下面是参考代码:

 

//通过file.fid加载文件。
$file = file_load($form_state['values']['my_file_field']);

//将状态修改为持久化。
$file->status = FILE_STATUS_PERMANENT;

//保存。
file_save($file);

 

    如果点击了删除按钮,将会把该字段的值设置为0,你的模块还需要使用file_delete()将文件从files表和文件系统中实际的删除。

 

    它包含多个非标准的表单元素属性:#progress_indicator,可选值有'none''bar''throbber',默认为'throbber'#progress_message,文件正被上传时的进度信息,默认为NULL#upload_validators,一组回调函数,用来验证上传的文件;#upload_location ,上传文件应被存储的位置,例如'public://files/my_files'

 

示例代码,来自(image.field.inc):

 

$form['default_image'] = array(

    '#title' => t('Default image'),

    '#type' => 'managed_file',

    '#description' => t('If no image is uploaded, this image will be shown on display.'),

    '#default_value' => $field['settings']['default_image'],

    '#upload_location' => 'public://default_images/',

  );

 

常用属性: #access、 #after_build、 #array_parents、 #attached、 #attributes、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #tree、 #weight


Drupal版本:

30 markup(标识文本)

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

    它用来在表单中引入一段文本或者HTML如果一个元素没有设置属性#type,那么它就默认为标识文本元素。。

 

    注意:如果你把文本输出在一个可伸缩的字段集的内部,那么要使用<div>或者<p>标签对其进行包装,这样当字段集被折叠起来时,你的文本也被折叠在了里面;否则你的文本将显示在字段集的外面。

 

示例代码,来自(user.pages.inc):

 

$form['help'] = array('#markup' => '<p>' . t('This login can be used only once.') . '</p>');

 

常用属性: #access、 #after_build、 #element_validate、 #markup、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight

 

 

password(密码)

    生成一个单行文本字段,在这里用户的输入不直接显示出来。

 

示例代码,来自(user.module):

 

$form['account']['current_pass'] = array(

'#type' => 'password',

'#title' => t('Current password'),

'#size' => 25,

'#access' => !empty($protected_values),

'#description' => $current_pass_description,

'#weight' => -5,

);

 

常用属性:  #access、 #after_build、 #ajax、 #attributes、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight

 

 


Drupal版本:

31 password(密码)

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

    生成一个单行文本字段,在这里用户的输入不直接显示出来。

 

示例代码,来自(user.module):

 

$form['account']['current_pass'] = array(

'#type' => 'password',

'#title' => t('Current password'),

'#size' => 25,

'#access' => !empty($protected_values),

'#description' => $current_pass_description,

'#weight' => -5,

);

 

常用属性:  #access、 #after_build、 #ajax、 #attributes、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

32 password_confirm(带确认的密码)

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

    该元素创建两个HTML密码字段,并附加一个验证器来检查两个密码是否匹配。

 

示例代码,来自(user.module):

 

$form['account']['pass'] = array(

'#type' => 'password_confirm',

'#size' => 25,

'#description' => t('Provide a password for the new account in both fields.'),

'#required' => TRUE,

); 

 

常用属性: #access、 #after_build、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

33 Radio(单选按钮)

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

    生成一个单选按钮。

 

示例代码,来自(user.pages.inc):

$form[$name] = array(

'#type' => 'radio',

'#title' => $method['title'],

'#description' => (isset($method['description']) ? $method['description'] : NULL),

'#return_value' => $name,

'#default_value' => $default_method,

'#parents' => array('user_cancel_method'),

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #return_value、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display (默认为after)、 #tree、 #type、 #weight


Drupal版本:

34 radios(单选按钮)

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

    生成一组单选按钮。

 

示例代码,来自(user.module):

 

$form['account']['status'] = array(

'#type' => 'radios',

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

'#default_value' => $status,

'#options' => array(t('Blocked'), t('Active')),

'#access' => $admin,

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

35 select(下拉选择框)

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

    生成一个下拉选择框。

 

示例代码,来自(node.module):

$form['node_recent_block_count'] = array(

'#type' => 'select',

'#title' => t('Number of recent content items to display'),

'#default_value' => variable_get('node_recent_block_count', 10),

'#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #empty_option、 #empty_value、 #field_prefix、 #field_suffix、 #multiple、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

36 submit(提交按钮)

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

    生成一个表单提交按钮。按钮内部显示的单词默认为“提交”,但是可以使用属性#value来修改它。

 

示例代码,来自(node.module):

 

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

'#type' => 'submit',

'#value' => t('Advanced search'),

'#prefix' => '<div class="action">',

'#suffix' => '</div>',

'#weight' => 100,

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #button_type (默认为'submit')、 #disabled、 #element_validate、 #executes_submit_callback (默认为TRUE)、 #limit_validation_errors、 #name (默认为 'op')、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #submit、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #validate、 #value、 #weight


Drupal版本:

37 Tableselect(表选择)


    生成一个表格形式的单选按钮或者复选框,#headers属性用来设置列,#options属性用来设置行。#multiple属性用来决定是用单选按钮还是复选框,如果#js_select属性设置为TRUE,那么还会添加一个基于JavaScript的全选按钮。

    admin/content页面的节点内容列表,就用到了这个元素,如图所示:

图片1.png 

 

示例代码,来自(node.admin.inc):

 

$form['nodes'] = array(

'#type' => 'tableselect',

'#header' => $header,

'#options' => $options,

'#empty' => t('No content available.'),

);

 

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #element_validate、 #empty、 #header、 #js_select、 #multiple、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight


Drupal版本:

38 text_format(文本格式)

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

    文本域启用了文本格式的版本。它还包含两个非标准属性:

· #format: 要应用的格式。如果你想使用默认格式,那么可将这个属性设置为NULL,剩下的工作便会交给过滤器系统。

· #base_type (optional): 默认为'textarea'。也可以将文本格式选择器附加在其它表单元素类型上,比如文本字段。

 

示例代码,来自(user.module):

 

$form['signature_settings']['signature'] = array(

'#type' => 'text_format',

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

'#default_value' => isset($account->signature) ? $account->signature : '',

'#description' => t('Your signature will be publicly displayed at the end of your comments.'),

'#format' => isset($account->signature_format) ? $account->signature_format : NULL,

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #cols (默认为 60)、 #default_value、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #resizable (默认为 TRUE)、 #rows (默认为 5)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、#type、 #weight


Drupal版本:

39 textarea(文本域)

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

    生成一个多行的文本字段。

 

示例代码,来自(user.admin.inc):

 

$form['email_admin_created']['user_mail_register_admin_created_body'] = array(

'#type' => 'textarea',

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

'#default_value' => _user_mail_text('register_admin_created_body', NULL, array(), FALSE),

'#rows' => 15,

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #cols (默认为60)、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #resizable (默认为TRUE)、 #rows (默认为5)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight 

    如果通过设置#resizable为TRUE,启用了动态的文本域调整器,那么属性#cols设置将不起作用。


Drupal版本:

40 textfield(文本字段)

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

    生成一个单行文本字段。

 

示例代码,来自(user.admin.inc):

$form['anonymous_settings']['anonymous'] = array(

'#type' => 'textfield',

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

'#default_value' => variable_get('anonymous', t('Anonymous')),

'#description' => t('The name used to indicate anonymous users.'),

'#required' => TRUE,

);

 

常用属性: #access、 #after_build、 #ajax、 #attributes、 #autocomplete_path (默认为FALSE)、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为 128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #text_format、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

41 value(值)

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

    值元素用在drupal表单内部,它不会显示出来。

 

示例代码,来自(user.module):

$form['picture']['picture'] = array(

'#type' => 'value',

'#value' => isset($account->picture) ? $account->picture : NULL,

);

    不要混淆了'#type' => 'value' '#value' => ’’。前者声明了正被描述的元素的类型,而后者声明了该元素的值。

 

常用属性: #type #value

 

 


Drupal版本:

42 vertical_tabs(垂直标签)

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

    把所有的子字段集显示为垂直的标签。如图所示:

 

QQ截图20150624113023.png 

 

示例代码,来自(block.admin.inc):

$form['visibility'] = array(

'#type' => 'vertical_tabs',

'#attached' => array(

'js' => array(drupal_get_path('module', 'block') . '/block.js'),

),

);

常用属性: #access、 #after_build、 #default_tab、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight

 

weight(重量)

    生成一个用来声明重量的下拉选择框。

 

示例代码,来自(taxonomy.admin.inc):

$form[$key]['weight'] = array(

'#type' => 'weight',

'#delta' => $delta,

'#title_display' => 'invisible',

'#title' => t('Weight for added term'),

'#default_value' => $term->weight,

);

 

常用属性: #access、 #after_build、 #attributes、 #default_value、 #delta (默认为10)、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight


Drupal版本:

43 呈现API

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

    如果你认真读过systemk_element_info函数中的代码,你会发现这里还定义了一个元素page。它是一个表单元素么?当然不是。在Drupal中,表单的这种数组形式的结构,经过不断的实践,发现它给我们带来了多个方面的便利性,因此在Drupal7中,这种概念又作了进一步的扩充,把它抽象成为了“呈现数组”(Render Array)。所有的表单、表单元素都属于“呈现数组”。但不是每一个“呈现数组”都是表单。也就是说,“呈现数组”这个概念范畴更广一点。

    在Drupal7中,对于各种创建的内容,基本上都采用“呈现数组”这种方式。在本章的第一个实例代码中,页面回调函数中,我们就采用了“呈现数组”。使用这种方式的好处时,以page为例,我们可以方便的修改其它模块创建的页面内容,就像我们修改其它模块创建的表单一样方便,所不同的是这里使用hook_page_alter钩子函数。例如:

function mymodule_page_alter(&$page) {  

  // 把搜索表单放在页脚.  

  $page['footer']['search_form'] = $page['sidebar_first']['search_form'];  

  unset($page['sidebar_first']['search_form']);    

  //删除"powered by Drupal"区块 

  unset($page['footer']['system_powered-by']);

}


Drupal版本:

2创建相关文件

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

    我们不妨把模块的名字命名为contactus,在sites/all/modules/custom目录下创建一个contactus目录,接着创建三个文件,contactus.infocontactus.modulecontactus.pages.inc。我们向info文件添加以下内容:

 

name = 联系我们

description = 联系我们页面

core = 7.x

 

    接着,我们向contactus.module中添加hook_menu的实现:

<?php

 

/**

 * @file

 * 方便用户联系我们.

 */

 

/**

 * 实现钩子hook_menu().

 */

function contactus_menu() {

  $items = array();

  //联系我们菜单项

  $items['contactus'] = array(

    'tilte' => '联系我们',

    'page callback' => 'contactus_page',

    'type'     => MENU_CALLBACK,

    'access callback' =>TRUE,

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

  );

  //确认页面菜单项

  $items['contactus/confirm'] = array(

    'page callback' => 'contactus_confirm_page',

    'type'     => MENU_CALLBACK,

    'access callback' =>TRUE,

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

  );

  //致谢页面菜单项

  $items['contactus/thanks'] = array(

    'page callback' => 'contactus_thanks_page',

    'type'     => MENU_CALLBACK,

    'access callback' =>TRUE,

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

  );

  return $items;

}


Drupal版本:

3“联系我们”页面

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

   我们创建了三个菜单项,分别对应于联系我们、确认页面、致谢页面。接下来,我们首先为联系我们页面添加回调函数,向contactus.pages.inc中添加以下内容:

 

<?php

 

/**

 * @file

 * 各种页面的回调函数.

 */

 

/**

 * “联系我们”页面的回调函数

 */

function contactus_page(){

  //我们为这个页面设置标题

drupal_set_title('联系我们');


$render_array = array(

  '#markup' => '', 

);


//该页面的正文为一个表单,注意对于表单,这里需要使用drupal_render呈现一下。

$render_array['#markup'] .= drupal_render(drupal_get_form('contactus_form'));


//Drupal7的页面回调,返回的应该是一个数组

return $render_array;

}

 

Drupal需要对表单进行唯一的标识,这样当一个页面包含多个表单时,它就可以判定被提交的是哪一个表单,并且可以将表单与处理该表单的函数关联起来。为了唯一的标识表单,我们为每个表单分配了一个表单ID。在drupal_get_form()的调用中,所用的就是表单ID,如下所示:

drupal_get_form('contactus_form');

 

对于大多数的表单,其ID本身就是Drupal的一个函数名,因此符合Drupal中函数名的命名规则:“模块名字”+“表单描述”。例如,用户模块中的用户登录表单,它的ID为user_login,就符合这样的规则。

 

在表单的默认验证、提交、主题函数中,都用到了表单ID。另外,Drupal中表单ID和该表单最终生成的HTML版<form>标签中的ID属性还存在对应关系,我们很容易通过HTML版的ID来得到表单ID,对于我们的这个例子,我们可以使用firebug来查看表单的html,对于我们的这个表单,对应的html为:

<form id="contactus-form" accept-charset="UTF-8" method="post" action="/thinkindrupal/contactus">

 

这里我们看到,id="contactus-form",这是HTML版的ID,我们将连字符替换为下划线,就得到了表单ID,“contactus_form”。此外,在表单的HTML内部,我们还可以找到一个名为form_id的隐藏域,这里也存储了表单ID。通过这两个地方,我们可以反向的获取表单的ID,如果我们想使用form_alter修改一个已有表单时,这会非常有用。

 

    在我们的这个模块中,我们把表单命名为了contactus_form,它的描述性并不好,不过我们的这个模块中只有这么一个表单,所以这样命名也不会引起歧义,对于中文的开发者来说,如果找不到合适的英文单词,我们可以使用自己的拼音,然后加上注释,这样更可行一点,没有必要为了一个地道的英文名字而费上半天的功夫。

 

    现在就让我们看看contactus_form表单,接下来添加该表单的构建函数:

 

/**

 * “联系我们”表单的构建函数

 */

function contactus_form(){

  //添加我们自己的CSS,用来控制表单的样式

drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');


  //提示信息,默认为markup类型。

$form['tips'] = array(

'#prefix' =>'<div id="tips">',

'#markup' => t('<span class="form-required">*</span> 号为必填项。'),

'#suffix' =>'</div>',

);


//表单元素“姓名”

$form['name'] = array(

  //表单元素的#title属性,对应于实际输出中的label

'#title' => t('姓名'),

//表单元素的类型,这里为textfield

'#type' => 'textfield',

//这个表单元素是必填的

'#required' => TRUE,

//表单元素的默认值,这里使用了三位运算符和isset进行判定

'#default_value' => isset($_SESSION['contactus_form']['name']) ? $_SESSION['contactus_form']['name'] : "",

//表单元素的描述,

'#description' => t('例如:周星驰'),

);


  //表单元素“单位名称”

$form['company_name'] = array(

'#title' => t('单位名称'),

'#type' => 'textfield',

'#required' => TRUE,

'#default_value' => isset($_SESSION['contactus_form']['company_name']) ? $_SESSION['contactus_form']['company_name'] : "",

'#description' => t('例如:北京无名信息技术公司'),

);


  //表单元素“电子邮件”

$form['mail'] = array(

'#title' => t('电子邮件'),

'#type' => 'textfield',

'#required' => TRUE,

'#default_value' => isset($_SESSION['contactus_form']['mail']) ? $_SESSION['contactus_form']['mail'] : "",

'#description' => t('例如:info@example.com'),

);


//表单元素“电话号码”

$form['phone'] = array(

'#title' => t('电话号码'),

'#type' => 'textfield',

'#required' => TRUE,

'#default_value' => isset($_SESSION['contactus_form']['phone']) ? $_SESSION['contactus_form']['phone'] : "",

'#description' => t('例如:010-88888888'),

);


  //表单元素“邮件正文”

$form['contact'] = array(

'#title' => t('邮件正文'),

//表单元素的类型,这里为textarea

'#type' => 'textarea',

'#required' => TRUE,

'#default_value' => isset($_SESSION['contactus_form']['contact']) ? $_SESSION['contactus_form']['contact'] : "",

);


//访问来源的可选项

$visit_options = array(

'baidu' =>t('百度'),

'google' =>t('谷歌'),

'sohu' =>t('搜狐'),

'other' =>t('其它'),

);

//表单元素“访问来源”

$form['visit'] = array(

'#title' => t('访问来源'),

//表单元素的类型,这里为radios

'#type' => 'radios',

//单选按钮的可选项。

'#options' => $visit_options,

'#default_value' => isset($_SESSION['contactus_form']['visit']) ? $_SESSION['contactus_form']['visit'] : "",

//为了便于控制radios的外观,我们使用#prefix#suffix为其添加了一个带有IDdiv

'#prefix' => '<div id="visit-radios">',

'#suffix' => '</div>',

);


//表单元素“其它”,它依赖于表单元素“访问来源”

$form['other'] = array(

'#title' => t('其它'),

'#type' => 'textfield',

'#default_value' =>isset($_SESSION['contactus_form']['other'])?$_SESSION['contactus_form']['other']:"",

//这里的意思是,当表单元素“访问来源”的值为“other”时,这个表单元素才显示出来

  '#states' => array(

  'visible' => array(

  ':input[name="visit"]' => array('value' => 'other'),

),

),

); 

/*

//表单元素“确认”提交按钮

  $form['submit'] = array(

    '#type' => 'submit',

    '#value' => t('确认'),

  );

  */


  //表单元素“确认”提交按钮,这里采用了图片的形式

  $form['image_submit'] = array(

  //表单元素的类型,这里为image_button

    '#type' => 'image_button',


//图片按钮特有的#src属性,

    '#src' => drupal_get_path('module','contactus').'/images/button1-1.jpg',

    '#value' => 'image_sub',


//为表单元素添加两个属性,onmouseoutonmouseover,为了在鼠标移到按钮上时,显示不同的图片效果

    '#attributes' =>array(

      'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-1.jpg'",

      'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-2.jpg'",

    ),


    //为了便于控制image_button的外观,我们使用#prefix#suffix

    //为其添加了一个带有IDdiv

    '#prefix' => '<div id="image-submit">',

    '#suffix' => '</div>',

  );

 

  return $form;

}

 

    我们在这个表单中,用到了markuptextfieldradiosimage_button等常用的表单元素,关于这几个表单元素和其它表单元素的详细介绍,可参看后面的表单元素一节。另外,上面的代码中,有不少的注释,我们在这里也就不再过多地重复了。我们讲解一下,这里面的重点。

    我们启用这个模块,访问页面contactus。此时我们就可以看到我们创建的表单的实际效果了。如图所示:

图片1.png

Drupal版本:

4控制表单的外观

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

    如果你细心一点的话,你就会发现,我们的这个表单与默认的表单,在样式上面有所不同。是的,我们单独的为这个表单定义了自己样式。注意代码中的:

    drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');

    这一代码,表示向这个表单添加CSS文件contactus.css,当加载这个表单时,就会加载这个CSS文件。通过在CSS文件中定义样式规则,我们便可以控制80%的表单外观了。首先,通过CSS代码,我们实现了表单元素的label标签与input元素左右显示,默认情况下,是上下显示的。我们来看看对应的CSS代码:

#contactus-form .form-item{

  position:relative;

  left:10px;

  top:5px;

  margin-bottom:5px;

  overflow:hidden;

}

 

#contactus-form .form-item label{

float: left;

  position:relative;

width: 180px;

font-size:12px;

}

 

#contactus-form .form-item input{

width: 250px;

}

 

    在这里,我们固定了labelinput的宽度,并使label向左浮动,从而实现了左右排列的效果。contactus.css文件中的更多的CSS代码,我们就不再这里列出了,大家可以登录我的网站http://zhupou.cn,来下载所有的实例模块的完整代码。

 

    为了控制具体表单元素的外观,我么还用到了属性#prefix和#suffix。使用这两个属性,我们可以方便的为表单元素添加更具有表述性的ID:

        '#prefix' => '<div id="visit-radios">',

'#suffix' => '</div>',

 

    我们看一下,表单的最下面,我们这里使用了图片按钮,来替代默认的表单提交按钮元素。我这里的图片按钮看起来有点单薄,这是因为作者的美工效果实在不怎么样。如果换一个好一点的美工,加上上面所给的代码,一定能够实现非常酷的效果。另外注意的是,鼠标移到图片按钮上时,会变色,我们看看前后对比:

前:                           后:      

                图片1.png 图片2.png

    这是因为我们作了两个图片,通过JS,在鼠标移进和移出时变换图片,来实现的这一效果。对应代码:

'#attributes' =>array(

      'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-1.jpg'",

      'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-2.jpg'",

    ),

    最后,让我们看一下访问来源这个表单元素,当我们选择“其它”的时候,表单显示出来一个文本输入框,允许我们输入一个具体的访问来源:

图片3.png 

    其实这里,我们做了两个表单元素,一个是visit、一个是other,后一个是一个文本输入框。对于表单元素other,我们为其使用了'#states'属性:

'#states' => array(

'visible' => array(

':input[name="visit"]' => array('value' => 'other'),

),

),

 

    这段代码的意思是说,当表单元素visit的值等于'other'时,显示这个表单元素。我们在这里没有使用任何JS,仅仅添加了'#states',就实现了这一效果。实际上,如果我们看一下,生成的html代码,我们就会发现表单元素other是存在的,只不过被隐藏了起来,当我们点击访问来源并选择“其它”时,该元素显示了出来。在这里,Drupal替我们做了很多工作。

    

    此外,为了控制表单的外观,我们还可以为表单添加主题函数,我们也可以使用#theme属性为任意一个表单元素添加主题函数。在默认情况下,contactus_form对应的主题函数应该为theme_contactus_form。当然,对于使用#theme属性添加的主题函数,可以根据需要按照Drupal函数的命名规则加以命名。关于主题的更多知识,我们可以参看主题系统一章。


Drupal版本:

5添加验证函数和提交函数

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

    定义好表单以后,通常接下来的工作就是为表单添加验证函数,用来检查用户的输入,让我们在代码中添加验证函数对应的代码:

 

/**

 * contactus_form表单的验证函数

 */

function contactus_form_validate($form, &$form_state){

  //这里使用正则表达式,来验证邮箱的有效性,注意,这里的正则表达式,包围在"/...../"之间。

if(!preg_match("/^[\w\-\.]+@[\w\-\.]+(\.\w+)+$/", $form_state['values']['mail'])){

form_set_error('mail',t('您输入的电子邮件地址格式不正确'));

}

}

 

    在这里,对于用户的输入,我们只对电子邮件字段进行了检查。如果我们在电子邮件输入框中输入了非法的电子邮件,比如:

 

图片1.png 

 

那么当我们提交表单时,系统就会给出错误提示:

 

图片2.png 

    通过上面的例子,我们可以看出,form_set_error的第一个参数,对应于表单元素的ID,第二个参数为显示给用户的错误提示。

 

    在我们的这个默认的验证函数中,我们可以为所有的表单元素添加验证规则,因此在一般情况下,一个默认的验证函数就足够了。当然,我们可以使用#validate属性,为表单添加一组验证。我们在用户系统一章,讲到的用户登录表单,就用到了#validate属性。此外,对于单个的表单元素,我们也可以为其设置验证函数,这和表单的验证函数类似,不过需要使用表单元素的#element_validate属性。

 

如果我们在验证函数做了大量的处理,而你又想把结果保存下来,供后面的提交函数使用,此时有两种不同的实现方式。一是使用form_set_value(),二是使用$form_state。我们来看一下$form_state的这种方式,这种方式我用到过:

 

    由于$form_state在验证和提交函数中都是通过引用传递的,所以在验证函数中,可以将数值存储在这里,而在提交函数中,就可以使用它了。最好在$form_state中加上你模块的命名空间,这样有利于避免命名冲突。

 

    在实际的工作中,我就用到过这一点,在一个模块的验证函数中,我需要从远程获取一个图书的ID号,这个操作由于调用的是web service,所以比较耗费资源,而在提交处理函数中,我需要使用这个ID号。

 

//通过调用web service获取图书的唯一ID

$book_id = my_module_get_book_id_by_service($bsno);

 

//我们将在验证中获取的值保存起来,这样在提交函数中可以使用。

$form_state['my_module']['book_id'] = $book_id;

 

接着,在提交函数中,我这样访问该数据:

$book_id = $form_state['my_module']['book_id'];

 

 

    表单完全通过验证后,接下来我们需要对通过表单提交的数据进行处理。让我们为表单添加对应的提交处理函数。

 

/**

 * contactus_form表单的提交函数

 */

function contactus_form_submit($form, &$form_state){

  //把表单的值存放在会话中去,由于这里涉及到了两个不同的表单之间传值。

$_SESSION['contactus_form'] = $form_state['values'];

//表单重定向到确认页面

$form_state['redirect'] = 'contactus/confirm';

}

 

    在我们的