老葛
北京亚艾元软件有限责任公司
第一章 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
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过数据库抽象层, Drupal可以支持多个数据库,比如内置支持的MySQL、PostreSQL、SQLite,以及通过第三方模块支持的SqlServer、Oracle等等。除此以外,Drupal在数据库方面,还提供了进一步的支持,这就是使用Schema来描述数据库表结构,这对于那些需要创建自己的数据库表的模块,提供了极大的方便。这样,我们创建好Schema定义,Drupal就能够将其翻译成具体数据库的语法,比如MySQL的、PostreSQL的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在第2章中已经看到,当我们编写的模块需要创建一个或者多个数据库表来存储信息时,创建和维护表结构的指令都放在了模块的install文件中。
作者:老葛,北京亚艾元软件有限责任公司,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,
),
一个序列字段是用来保存自增数字的。例如,当添加一个节点时,node表中的nid字段将会自增。序列字段必须索引;通常会把它作为主键进行索引。
'nid' => array(
'description' => 'The primary identifier for a node.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
浮点数字是用来存储浮点数据类型的。对于浮点数字来说,tiny, small, medium, 和normal型浮点一般是没有区别的;另外,big型浮点用来声明双精度字段。Ubercart的uc_products表中weight字段用到了这一个类型。
'weight' => array(
'description' => 'Physical weight.',
'type' => 'float',
'not null' => TRUE,
'default' => 0.0,
),
数字数据类型允许你声明数字的精度和小数位数。精度指的是数字的有效数字位数。小数位数指的是小数点右边的数字位数。例如,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,
),
二进位大型对象数据类型用于存储二进制数据。二进位数据包括音乐,图片,或者视频。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.',
),
表.模式定义中的Type和Size键与本地数据库类型的对应关系
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 |
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
向数据库表添加一个字段。 |
|
向数据库表添加一个索引。 |
|
向数据库表添加一个主键。 |
|
向数据库表添加一个唯一键。 |
|
修改一个字段定义。 |
|
创建一个数据库表。 |
|
删除一个字段。 |
|
删除一个索引 |
|
删除数据库的主键。 |
|
删除一个表。 |
|
删除一个唯一键。 |
|
检查给定表中该字段是否存在。 |
|
根据指定的包含字段键的数组,返回包含字段名字的数组。 |
|
为字段设置默认值。 |
|
把一个字段设置为没有默认值。 |
|
按照给定的基表名字,查找对应的所有数据库表。 |
|
检查给定表中该索引是否存在。 |
|
重命名一个表。 |
|
检查该数据库表是否存在。 |
|
获取一个表的模式定义,或者整个数据库的模式定义。 |
|
返回一个模块的未处理过并且未修改过的模式(schema)。 |
|
创建模块中hook_schema里面定义的所有数据库表。 |
|
从一个表的模式定义中返回一列字段。该字段列表适用于SQL查询。 |
|
删除模块中hook_schema里面定义的所有数据库表。 |
|
基于数据库模式,向数据库中保存(插入或者更新)一个记录。 |
|
定义数据库模式的当前版本。 |
我们在前面定义了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_field和db_add_field。
作者:老葛,北京亚艾元软件有限责任公司,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上,INT、VARCHAR、 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': 包含索引的关联数组。它里面包含一个或多个字段的键名,用来表示数据库表的一个索引。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在你可能会想,花这么大的功夫,创建一个这么复杂的数组,来向Drupal描述我们的表结构,是不是有点得不偿失啊?当你熟悉了这些语法结构以后,大多数的代码都是拷贝来,拷贝去,实际上你并不需要记忆太多,这非常类似于我们写的八股文。其次,存在第三方的模块,帮我们干这些脏活,这就是Schema(模式)模块,它的下载地址为:http://drupal.org/project/schema。下载了模块,将其启用。导航到“管理 〉结构 〉Schema”,点击Inspect(检查)标签,在这里,我们可以看到所有数据库表的模式定义。如果你为你的数据库表准备好了SQL脚本,那么使用模式模块,它就可以帮你自动生成模式定义,接着将其复制粘贴到你的.install文件中就可以了。
提示 你一般很少需要从头编写一个模式定义。一般情况下,你可以拷贝已有的模式,以此为基础定义自己的模式。或者,你可以使用已有的表,使用模式模块的Inspect(检查)标签,让它帮你创建模式定义。
模式模块还允许你查看任意模块的模式。如图所示,在模式模块中显示了block_morelink模块的模式。
图 4-1.模式模块显示了block_morelink模块的模式。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在模式定义中声明的字段类型,将会映射成具体数据库中的本地字段类型。例如,一个size为tiny的整数字段将映射为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的类型可以为tiny、small、medium、big、normal。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Varchar,也就是变长字符字段;对于长度小于256字符的文本,通常使用这一字段类型。它的最大的字符长度,可以使用length键定义。MySQL中 varchar 字段的长度为0–255字符(MySQL 5.0.2 及更早版本)和0–65,535字符(MySQL 5.0.3及以后版本);而PostgreSQL中varchar字段的长度则可以更大一些。
例如,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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Char字段是定长字符字段。该字段的字符长度,可以使用length键定义。MySQL中char字段的长度为0–255字符。Drupal核心中,我并没有找到char类型的实例。Ubercart模块中的uc_countries表的country_iso_code_2、country_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。
作者:老葛,北京亚艾元软件有限责任公司,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的类型可以为tiny、small、medium、big、normal。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
数字型数据类型是用来存储数字的,它包括integer(整数)、serial(序列数)、 float(浮点数)、 和numeric(数字)类型。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在本书中,我们首先介绍Drupal的基本原理,接着对Drupal开发中,所涉及到的基本技术和常见技巧,进行详细的描述。在很多的技术图书中,都会包含与软件安装相关的章节,由于本书主要是讲解Drupal模块开发的,我们在这里不会讲解Drupal的安装及相关模块的配置,对于Drupal的安装,由于相对来讲,是很简单的,并且网络上有很多这样的文档,所以在这里就不会为安装配置浪费笔墨了。
在本章中,我们将为大家介绍什么是Drupal、Drupal相关技术、Drupal核心术语、引导指令流程、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又爱又恨的原因。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们首先来看一下,在Drupal中,都会用到哪些技术。
2.1 PHP
Drupal使用的编程语言是PHP(http://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的相关配置,可以参看相关的文档。
最初,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 在更广泛的领域中得以应用。
操作系统位于Drupal相关技术堆栈的最下面的一层,由于Drupal是基于PHP编写的,而PHP语言也具有跨操作系统的特点,所以只要操作系统能够支持PHP,我们就可以使用它来运行Drupal。Windows,Linux,Mac OS等等,在这些主流的操作系统上,都可以运行Drupal。操作系统层,主要负责网站相关的最底层的任务,比如网络连接,文件的权限。如果你是在linux下面安装运行Drupal,你经常会遇到文件夹权限不可写,这样的权限问题。
本书的作者,使用的是Vista操作系统,所用的环境是XAMPP,所有的代码都是在这个操作系统下面编写的,相信这些程序也能够应用于其它操作系统及相关环境下。
从Drupal系统中最终返回给浏览器的是HTML,而在HTML中,CSS是用来定义页面样式的,JavaScript则是浏览器客户端的脚本语言。需要注意的是,Drupal对JavaScript的支持,是通过jQuery实现的。jQuery是一个优秀的轻量级的JavaScript框架 (压缩后只有21k) ,它兼容CSS3,还兼容各种浏览器。作为一个Drupal开发者,我们今后肯定会涉及到这三种技术。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们从drupal.org上下载最新的Drupal7版本,把它结压缩后,放到本地的web文档目录下,这是我们就会看到Drupal核心文件夹下的目录结构了。
Drupal7核心的默认文件夹结构
通过了解Drupal默认安装的目录结构,我们能够了解一些最佳实践,比如下载的第3方模块和主题的放置位置,以及如何拥有不同的Drupal安装轮廓。我们来进一步的了解一下Drupal的文件夹目录:
3.1 includes
这个文件夹下面包含了Drupal通用的函数库,比如ajax、批处理、表单API、数据库API、本地化API等等。这些通用函数库,是Drupal程序运行的基石。
3.2 misc
这个文件夹下面包含了JavaScript文件,和其它各种图标和图片文件。其中JavaScript文件包括jquery.js、jquery.form.js、jquery.cookie.js、drupal.js、ajax.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自身程序运行过程中,调用不到这些脚本;这里面都是一些shell和Perl的实用脚本。
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的模板引擎和默认主题。这里的默认主题有bartik、garland、seven等等。你下载的第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中新增的一个文件。其余文件则是相关的文档文件。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过了解Drupal自身的文件夹结构,我们对Drupal有了初步的认识。我们还需要对Drupal中的常用概念,或者说专有术语,有更好的界定。这能够帮助我们更好的学习使用Drupal。常用的术语有模块、钩子、主题、节点、菜单、区块、字段、实体等等。Drupal相关术语不仅仅是这里所列的这么几个,还有更多的相关术语。有兴趣的读者,可以参看http://drupal.org/node/19828。
作者:老葛,北京亚艾元软件有限责任公司,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的模块,就像大自然一样,是在不断演化的,而且也存在优胜劣汰这样的自然法则。
作者:老 葛,北京亚艾元软件有限责任公司,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($hook) as $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,里面包含了用户模块对外提供的所有钩子。
作者:老葛,北京亚艾元软件有限责任公司,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代码干净、浏览器兼容性比较好,易于复用,缺点就是比较复杂,前期成本高。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
区块一般指的是放置在模板中的边栏、页首、或者页尾中,主内容外的内容片断。通过区块管理界面,可以启用或者禁用这些信息。举例来说,我们通常在站点的页脚
处,显示的“版权信息”,就可以处理成区块;常常显示在站点边栏的“热门内容”,每个站点的主导航链接,都可以处理成区块;比如“用户登录”和“我的帐
号”功能,可以结合在一起,处理成区块显示,这样匿名用户看到的就是“用户登录”表单,而注册用户看到的就是“我的帐号”链接。
我们可以控制单个区块的显示位置,比如显示在特定页面,显示给特定角色的用户,显示在特定节点类型的页面中,显示在特定的主题区域下。区块通常是放在区域中的,而区域的定义则位于主题的info文件中。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在Drupal中,菜单有两层含义:一种是处理请求的路由系统,它会将页面请求所提供的URL映射到Drupal内部的回调函数上,这是Drupal程序员所关心的;另一种就是页面上的导航,它负责组织站点的内容关系。
菜单具有层级的树形结构,一个菜单项下面可以有多个子菜单项,子菜单项下面又可以包含菜单项。需要注意的是Drupal菜单的层级,最多可以有9级,超过了9级,系统就不能正常工作了,而在实际的站点导航中,很难遇到包含9级的导航。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于你站点的每位访问者,无论他拥有你站点的一个账号,或者是匿名访问,在Drupal中,都会将其处理成用户。每个用户都具有一个ID,注册用户还具有用
户名、电子邮件等信息。用户本身是实体的一个具体实现,所以我们可以为用户添加更多的字段;此外,使用profile2模块,就可以定义不同的
profile类型,并将其与用户关联起来。
ID为0的用户为匿名用户,我们可以为匿名用户启用缓存。ID为1的用户,是Drupal站点的超级管理员,具有站点操作的各种权限。Drupal通过角色来
管理不同注册用户的权限。一个角色就代表着一组权限的集合。Drupal自带了3中角色:匿名用户、注册用户、管理员。根据站点的需求,可以添加更多的角
色,比如“站内编辑”。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
字段和实体,是Drupal7中新引入的两个概念。在Drupal7以前,人们使用CCK模块来扩展节点类型,为节点类型添加各种字段。这种方式渐渐的演变成为了主流方式,并最终在Drupal7中进入了Drupal内核。
以
前,人们尝试着,将所有的内容都统一到节点上去,比如区块,评论,分类,用户profile,都存在相应的第3方模块将其实现为相应的节点类型。在
Drupal7中,核心开发者将这方面的努力做了进一步的抽象,把核心中的节点、分类、评论、用户都抽象成为了实体。实体具有相同的增删改查,可以为实体
添加更多的字段。
第3方模块,Entity API正在成为Drupal7中的CCK模块,基于这个模块,我们可以方便的定义出来新的实体类型,比如在Ubercart和Commerce模块中,就基于这个模块将订单实现成为了一种新的实体类型。
作者:老葛,北京亚艾元软件有限责任公司,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、首先,我们启用了简洁URL,Web服务器收到请求后,会按照URL重写把用户看到的URL,转换为系统可以理解的URL,在这里就是将http://zhupou.cn/node/88 转换为了http://zhupou.cn/index.php?node/88。Apache,IIS,Nginx都支持URL重写。
2、PHP开始执行Drupal的index.php文件,Drupal获取到内部路径“node/88”.
3、Drupal启动完整地引导指令流程,完成资源的初始化,加载所有启用的模块后,将内部路径“node/88”映射到了节点模块的对应回调函数node_page_view上。
4、节点模块执行node_page_view函数,经过node_page_view -à node_show à node_view_multipleànode_viewànode_build_content这样的持续回调,它从数据库中读取ID为88的节点,并将返回的数据封装成一个drupal_render可以识别的数组。在node_view,node_build_content函数中,先后触发了以下钩子函数:hook_field_prepare_view,、hook_field_formatter_prepare_view、hook_entity_prepare_view、hook_field_formatter_view、hook_node_view、hook_entity_view、hook_node_view_alter、hook_entity_view_alter。
5、主题系统获取节点88的数据信息,并将其使用html代码进行封装,同时应用CSS。主题系统获取与节点88相关的其它页面元素数据信息,并分别将其使用html代码进行封装,同时应用CSS。
6、Drupal完成所有的处理后,把最终封装好的HTML、CSS数据传送给用户的浏览器。浏览器将这些数据显示成web页面,呈现给最终用户。
这里重点需要理解的就是Drupal的引导指令,和相关的钩子函数调用。让我们对这两点进一步的解释。
作者:老葛,北京亚艾元软件有限责任公司,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.inc、theme.inc、pager.inc、menu.inc、file.inc、form.inc、ajax.inc、token.inc等等。在这里将设置Drupal定制的错误处理器,并加载所有启用了的模块。同时还会初始化Drupal内部路径,初始化主题系统。最后Drupal调用hook_init钩子,这样在对请求正式处理以前,为相应模块提供一个交互的机会。
作者:老葛,北京亚艾元软件有限责任公司,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_alter、hook_entity_view_alter钩子的触发,则是使用drupal_alter完成的,代码如下:
drupal_alter(array('node_view', 'entity_view'), $build, $type);
而hook_field_prepare_view、hook_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();
我画了一张钩子执行顺序的流程图,希望能够方便大家理解构字的执行顺序。
钩子执行顺序图
总结
读完本章以后,你应该能够
理解Drupal是什么,
Drupal核心的文件结构、
Drupal常用术语,
Drupal处理http请求的大致流程,
引导指令流程,
钩子回调流程。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Thinkindrupal.com是老葛的个人网站,主要面向自己的各种读者群,为大家提供优质的Drupal中文技术资源。
Thinkindrupal.com主要解决Drupal的中文文档的问题,尽可能的去改进,完善,增加各种各样的Drupal中文文档,以此来帮助更多的人学习使用Drupal。
我们知道,Drupal不同于Java,PHP这样的平台性的技术,后面有专门的商业公司的支持。Drupal作为一个开源项目,像所有的其它开源项目一样,文档方面是比较缺乏的,对于一个非中文的项目,中文文档更缺乏。文档的建设始终是开源软件领域的冷板凳,Drupal也不例外。Thinkindrupal.com希望通过自己的努力,帮助更多的英文不好的朋友了解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问题数百个。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal可以用于构建各种类型的网站,比如论坛、电子商务、博客、新闻聚合、相册、视频等等。由于每种类型的网站之间,其具体功能相去甚远,如果仅仅依靠Drupal核心模块,那么是远远满足不了这么多需求的。但是Drupal在PHP语言的基础之上,封装了一套自己的API,基于Drupal的API,用户就可以根据自己网站的具体需求,开发出对应的模块,来满足自己的需求。以电子商务类型的网站为例,drupal社区的相关开发者,先后开发出来了ecomerce、ubercart、commerce模块,用来满足该类型网站的建站需求。
Drupal的模块大体分作两类,一种是Drupal核心模块,它又可分为核心必选模块和核心可选模块;另一种是第3方模块,就是Drupal核心以外的模块,在drupal.org上的模块下载中,可以找到各种各样的模块,比如常见的views、panels、ctools、rules模块。当我们要实现一个功能的时候,我们就要寻找相应的模块,我们首先想到的是Drupal核心模块是否能够满足我们的需求,其次是在drupal.org的模块库中寻找,有时可能会在别的网站上找到。通常我们总能找到一个现成的模块,或者基于多个基础模块的组合功能,恰好满足我们的需求。但是总存在这样的情况,现有的模块仅能满足我们80%的需求,或者没有现成的模块可用。这个时候,我们就不得不开发自己的模块了。
在本章中,我们将根据实际的需求,来开发一个自己的模块。通过这个实例,我们能够了解到Drupal模块的结构,常用的Drupal API,基本的编码规范。首先让我们了解一下这个模块的具体需求,我们知道,中文的网站,通常信息量比较大,页面全是链接,而链接放在一个又一个的区块当中,通常中文网站的区块有3部分组成,区块标题,更多链接,区块内容列表。而Drupal核
心的区块,只包含了标题和内容两个组成部分。为了在区块中,添加一个更多链接,可以采用多种办法,但是很多时候,我们需要把这个链接硬编码到模板文件中。
如果能够在区块的配置页面,能够配置更多链接,那岂不是更好?而此时我们又没有找到一个现成的模块可用,所以我们只好编写一个这样的模块了。
作者:老葛,北京亚艾元软件有限责任公司,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.info、block_morelink.module、block_morelink.install。
接着,让我们为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
让我们来看一下,这些内容的含义:第一条指令的意思是说,模块的名字为“区块更多链接”;第二条指令为我们声明了这个模块的描述,告诉用户这个模块是用来做什么的,name和description的值会显示在模块列表页面;第三条指令,告诉Drupal这个模块所适用的Drupal主版本,这里为7.x,Drupal主版本之间是不兼容的;第四条指令,表示这个模块依赖于block模块,如果block模块没有启用,那么我们的这个模块也就无法启用;第五条指令,用于告诉Drupal这个模块的配置页面链接,同样显示在模块列表页面。
图 模块启用后在模块列表页面上显示的内容
如果我们需要在info文件添加注释,那么可以使用“;”,语法如下:
;files[] = morelink.module
;files[] = morelink.admin.inc
;files[] = morelink.install
注意我们在上面的注释中,使用了files[]指令,它用来表示这个模块都包含哪些文件。在Drupal7中,实现了缓加载机制,用来提升Drupal的性能。最初设计的目标是,Drupal可以缓加载所有的PHP函数、类、接口。但是在Drupal7正式版发布时,只实现了类、接口的缓加载。所以files[]指令的具体含义,就是声明模块中有哪些文件里面包含php类、接口的代码。由于我们这个模块中,没有使用到PHP面向对象技术,也就没有包含任何PHP类、接口。所以我们完全可以把与这个指令相关的代码注释掉。
除此之外,常用的还有package、php指令。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开发者,时常应该记着把info、module、inc、php文件的文本格式设置为UTF-8,这样可以避免很多不必要的编码错误。
现在我们创建好了info文件,让我们为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文档。接着空了一行,后面跟着一段更长的描述,它用来向其它程序员说明这个模块是做什么的。
下面我们要做的就是编写模块的逻辑了。我们在前面讲过,我们这个模块目的是为区块添加一个“更多链接”属性。为此,我们首先需要让用户为区块输入这个“更多链接”,在什么地方输入呢?应该在区块的配置页面,以及区块的添加页面,在这两个页面允许用户输入“更多链接”。首先让我们以区块的配置页面为例,我们先来看看这个页面。
图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_menu、hook_perm、hook_help这样简单的钩子开始讲解,主要是为了给大家呈现一个解决问题完整过程,希望让读者在学习Drupal api的同时,能够掌握解决问题的方法,另外就是想突出一下表单API在Drupal模块开发中的重要性,只有掌握了表单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。我们在这里知道了这个表单的ID,block_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-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就可以了。所以我们暂时先不考虑这个验证函数。而在提交函数中,我们则需要把用户输入的信息保存到数据库中,显然我们现在还没有准备好用来存储数据的数据库表。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
模块中存储所用数据的常用方式,就是为这些数据创建一个单独的数据库表。当我们决定为模块创建数据库表结构时,应该问问自己:我们需要存储哪些数据?如果我们要对这个表进行查询,那么会用到哪些字段和索引?最后,还要考虑一下,将来对这个模块,有没有可能会作些扩展?
首先让我们分析一下需要存储的数据。除了上面新增表单元素里面的信息外,我们还需要保存区块的ID信息,这样我们才能知道这个更多链接信息是属于哪个区块的。使用一个mysql的管理工具,比如phpmyadmin,我们打开Drupal的数据库结构,我们发现,在Drupal中,区块的ID是由模块名和delta值共同组成的。这样对于一个区块,我们需要保存4个值,分别为module、delta、url、title。另外我们需要为我们的数据库表起个名字,为了简单起见,就叫做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注入。
作者:老葛,北京亚艾元软件有限责任公司,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_preprocess、template_preprocess_block、template_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);
}
在这个函数中定义了变量block、block_zebra、block_id、content、classes_array、block_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>
这样,我们配置一下搜索表单区块,输入一个测试用的更多链接url、title。保存区块,回到区块的显示页面,我们就会看到一个更多链接。
图2-3搜索表单多了一个更多链接
模块写到这里,功能基本上就完成了,如果是一个实际的项目,现在就可以将其应用于在线站点了。但是作为模块开发中的一个练习来讲,我们还需要进一步的来完善这个模块,使其具有较强的可配置性和可定制性。
作者:老葛,北京亚艾元软件有限责任公司,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中返回的是一个数组,一个这样的钩子函数中可以注册多个主题函数。
现在编辑bartik的template文件,我们在这个文件的最下面追加以下函数:
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。
作者:老葛,北京亚艾元软件有限责任公司,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/block和admin/config/block/morelink。菜单项的值是一个数组,里面包含的键和值,描述了Drupal在处理该路径下的回调函数时可以做些什么。有关菜单方面的更多详细,可参看菜单系统一章。
Drupal的配置页面有多个类别,比如内容、用户、系统,都出现在主配置页面上。我们的配置是关于区块的,并没有一个类别特别适合我们。所以在这里,我们创建一个名为“Block”的新类别。
图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给出了的当前表单的样子。
图2-5 block_morelink的配置页面
在前面的例子中,修改配置并点击“保存配置”按钮,就可以正常工作。下面部分将描述如何实现这一点。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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
尽管贡献自己的代码在中国这样的环境中,并不是很流行,但是环境还是在逐步的改善,通过向Drupal社区贡献自己的模块,如果你的模块被其它用户使用的话,也是可以为你带来很多潜在的机会的。开发好了block_morelink模块以后,我们可以在drupal.org上创建一个项目,然后将这个模块通过GIT上传上去,这样我们就将这个模块分享出去了。项目地址:http://drupal.org/project/block_morelink/。注意drupal.org上的模块,需要遵守GPL协议。
图2-6 drupal.org上该模块项目页面
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
总结
读完本章以后,我们应该掌握以下几点:
从头创建一个Drupal模块。
使用Drupal的表单API来创建简单的表单。
使用hook_form_FORM_ID_alter来修改其它表单。
了解Drupal的主题覆写机制
了解Drupal中的预处理函数
使用hook_schema创建数据库表
使用hook_menu建立简单的回调映射。
使用variable_get与variable_set来读取和存储配置信息。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
菜单就是网站的导航链接,Drupal自带的菜单模块,允许我们添加、编辑、删除菜单和菜单项。这里的菜单项,就是通常所说的导航链接,通常菜单中的导航链接是有层级关系的。Drupal7自带了四个菜单,主菜单、管理菜单、导航菜单、用户菜单。Drupal的菜单系统,除了包含前面菜单模块所提供的功能以外,还蕴含着其它含义。为了更好的理解Drupal的菜单系统,我们可以从它提供的功能入手,菜单系统提供了三种功能:1、回调映射,2、访问控制,3、菜单定制。菜单系统的基本代码位于includes/menu.inc中,主要包含了前两点功能;而可选代码则位于modules/menu,这就是菜单模块提供的普通用户可见的菜单功能。
在本章中,我们将主要学习菜单系统中开发相关的知识,比如如何创建一个菜单项,如何通过访问控制来保护菜单项,学习如何使用菜单通配符,以及内置的各种菜单项类型。在本章的最后,给出了如何修改、添加、和删除已有的菜单项,这样你就可以灵活的控制Drupal菜单了。
作者:老葛,北京亚艾元软件有限责任公司,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所示的输出。
图 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所示的结果(如果你没有得到这一结果,那你清空一下缓存)。
图 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-13.页面参数和URL参数
在页面回调参数的数组中,键被忽略了,只有值才有意义,所以你不能使用键来对应回调函数中的参数;在这里,是按照顺序走。回调参数通常是变量,并常用在动态菜单项中。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
有时候,Drupal的术语是异常晦涩难懂的,同样的名字,在Drupal中有着不同的含义,拿标签tabs来说,在Drupal中,我们把它叫做本地任务,对应的菜单类型为MENU_LOCAL_TASK或者MENU_DEFAULT_LOCAL_TASK。本地任务的标题通常是一个简短的动词,比如“添加”或者“列出”。它通常作用在一些对象上,比如节点,或者用户。我们可以把一个本地任务理解为一个关于菜单项的语义声明,通常显示为一个标签(tab)----这和<strong>标签类似,后者也是一个语义声明,通常用来显示加粗的文本。
为了显示标签,本地任务必须有一个父菜单项。一个常用的做法是将一个回调指定到一个根路径上,比如my,然后将本地任务指定到扩展了该路径的子路径上,比如my/orders、my/comments、my/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所示的效果。
图 3-14.采用了本地任务的个人主页
注意,页面的标题来自于父回调函数,而不是来自于默认的本地任务。如果你想使用一个不同的标题,那么可以使用drupal_set_title()来单独设置它。
作者:老葛,北京亚艾元软件有限责任公司,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目录下查找这个文件,并显示无法打开相应文件的错误信息。
图 3-15.注释掉 'file path'的定义信息,就会报错。
作者:老葛,北京亚艾元软件有限责任公司,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()。
图 3-16.用户菜单的链接已被禁用
此时,在用户菜单的管理界面,我们选择启用,并保存设置,我们发现两个菜单项仍然是禁用的,也就是说hook_menu_link_alter()中对菜单项所做的修改,是无法在用户界面中再进行覆写的。
作者:老葛,北京亚艾元软件有限责任公司,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;
}
作者:老葛,北京亚艾元软件有限责任公司,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,就会得到这样的结果:
图 3-17.带有通配符的菜单项
作者:老葛,北京亚艾元软件有限责任公司,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所示的结果。
图 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之间的区别。
作者:老葛,北京亚艾元软件有限责任公司,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所示的结果。
图 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()。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
菜单路径中的通配符,不影响将URL中的额外部分作为参数传递给页面回调,这是因为通配符只匹配到下一个/字符。继续使用我们的wildcard/%路径作为例子,对于URL wildcard/123/b/c/d,通配符所匹配的字符串就是123,而对于路径中的其余部分(b/c/d),它们将分别作为参数传递给页面回调。
图 3-18.通配符的值和URL中的参数部分都传递了过来
作者:老葛,北京亚艾元软件有限责任公司,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)。
作者:老葛,北京亚艾元软件有限责任公司,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类别下的对应信息提取出来。
作者:老葛,北京亚艾元软件有限责任公司,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核心中,好像再也找不到实际的例子了。而这种方式,在实际的站点开发中,很少被用到。
作者:老葛,北京亚艾元软件有限责任公司,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_ITEM、MENU_CALLBACK、MENU_SUGGESTED_ITEM、MENU_LOCAL_ACTION、MENU_LOCAL_TASK、MENU_DEFAULT_LOCAL_TASK。 如果忽略了"type",那么会使用默认的MENU_NORMAL_ITEM。
"options":在根据这个菜单项生成链接时,传递给l()函数的选项数组。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
hook_menu_link_insert($link)
这个钩子用于通知模块,菜单项已经创建。
hook_menu_link_update($link)
这个钩子用于通知模块,菜单项已经更新。
hook_menu_link_delete($link)
这个钩子用于通知模块,菜单项已被删除。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
•MENU_NORMAL_ITEM: 普通的菜单项,显示在菜单树中,管理员可以对其进行管理。
•MENU_CALLBACK: 回调函数简单的映射到一个路径上,不显示在菜单树中,当路径被访问时,调用对应的回调函数。
•MENU_SUGGESTED_ITEM: 模块可以“建议”管理员启用的菜单项。
•MENU_LOCAL_ACTION:本地动作是用来描述作用于父菜单项的动作,比如添加一个用户或者添加一个区块,它们在你的主题中呈现为动作链接。
•MENU_LOCAL_TASK: 本地任务通常显示为标签,用于描述数据的不同显示。
•MENU_DEFAULT_LOCAL_TASK: 每组本地任务中,必须提供一个默认的任务,它与父菜单项显示相同的内容。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
当读完这一章后,你应该可以:
使用hook_menu定义自己的菜单项
理解访问控制的工作原理
理解如何在路径中使用通配符
创建带有标签(本地任务)的页面
通过代码来修改已有的菜单项
作者:老葛,北京亚艾元软件有限责任公司,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所示。
图3-1 访问菜单项menu_abc后,Drupal显示的页面内容
需要注意的要点是,这里我们定义了一个路径,并将其映射到了一个函数上。该路径是一个Drupal路径。我们使用它作为$items数组的键。你还会注意到这个路径的名字和模块的名字是一样的,这里主要是用来保证有一个干净的URL命名空间。实际上,你可以在hook_menu定义各种有效的路径。
让我们来学习一下,这个菜单项数组里面所包含的每一项的含义。
'title':是用来定义菜单项的标题的,这里定义为'菜单ABC',当在浏览器中显示该页面时,它会自动用作页面标题。如果我们需要在后面的回调函数中覆写页面标题,那么可以使用drupal_set_title()函数。此外,我们没有在这里使用t()函数。这是因为对于菜单项的title来说,系统会自动地调用t()。
'description':是这个菜单项的简单描述,这里定义为'一个简单的菜单项.',当我们把鼠标移到右边导航区块的对应链接时,这一文本会显示出来,如图3-2所示。
图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。
作者:老葛,北京亚艾元软件有限责任公司,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,点击“清空所有缓存”按钮。这样我们就能看到菜单项位置的变化了,
图3-3 调整菜单项重量后的导航区块
我们调整重量后的效果,如图3-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;
}
清空缓存数据,得到如图所示的效果
图3-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;
}
作者:老葛,北京亚艾元软件有限责任公司,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文件中了。
作者:老葛,北京亚艾元软件有限责任公司,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'表示这个权限的名字。
图3-5 我们定义的权限,显示在了权限列表页
在前面的代码中,根据函数user_access('access abc')的返回结果,来判定是否允许用户访问的。现在,菜单系统就相当于一个门卫,hook_permission就相当于各种出入证,当用户访问特定路径时,需要提供相应的出入证明,没有有效的身份证明,就会被负责任的门卫遣返回家。
图3-6 匿名用户访问该页面,得到的提示
user_access()函数是默认的访问回调。如果你没有定义访问回调的话,那么访问参数将被菜单系统传递给user_access()。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们知道,Drupal是支持多语言的,它使用t(),st()函数来翻译字符串。所以你可能会想,菜单项中的title键应该是这样定义的:
'title' =>t( '菜单ABC'),
然而,这样定义就错了。菜单字符串是以原始字符串的形式存储在menu_router表中的,而菜单项的翻译则被推迟到了运行时。Drupal会自动的调用t()函数,用来翻译菜单项的标题。这个t()函数,就是默认的标题回调函数(title callback)。我们接下来会看到,如何将默认的t()函数修改为自定义的函数,以及如何向标题回调函数传递参数。
作者:老葛,北京亚艾元软件有限责任公司,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所示,通过使用一个自定义的标题回调,就可以实现,在运行时设置菜单项标题了。
图 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所示。
图 3-8. 将菜单项的标题和页面标题独立开来
作者:老葛,北京亚艾元软件有限责任公司,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所示。
图 3-9.嵌套菜单
Drupal还在页面的正文上面,正确的设置了面包屑,用来表示页面之间的嵌套关系。当然,根据设计的要求,可在主题层将菜单或面包屑定义成所要的各种样式。
图 3-10. 子菜单项页面和及其面包屑
为了关联另一个数据库表,我们可以使用方法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');
?>
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal提供了一套表单API,用来生成、验证和处理HTML表单。表单API将表单抽象为一个关联数组,里面包含了各种属性和对应的值。在生成表单页面时,呈现引擎会在适当的时候将数组呈现出来。表单API为我们带来了很多好处,由于我们将表单表示成为了结构化的数组,所以我们可以添加、删除、重新排序、和修改表单。当我们想用一种干净的方式,来对其它模块创建的表单进行修改时,这会特别方便。此外,表单API还对表单操作进行了保护,从而能够有效的防止表单注入攻击。对于任意的表单,我们可以使用表单API为其添加附加的验证和处理函数。
当然,表单API再给我们带来很多便利和灵活性的同时,也给我们带来了高昂的学习成本,除此以外,定制表单的外观,通常也是非常琐碎和费时的工作。
我们在前面已经接触过Drupal的表单了,并且用到了hook_form_alter、hook_form_FORM_ID_alter。对于表单、表单元素应该有了感性的认识。如果以前,就使用过Drupal6下面的表单API写过模块,那么接下来的内容,就不难理解了。
在本章中,我们将迎难而上。首先通过两个实际的例子,来学习Drupal表单API。最后,我们列出来了Drupal核心自带的各种表单元素,及实例代码,并对呈现(render)API做了简单介绍。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
让我们来看第一个实例,我们先介绍一下,实例的背景。我们想建立一个联系我们的表单页面,我们知道有两种方法可用,一是使用Drupal核心自带的contact模块,另一个就是使用webform模块,通常这两个模块可以满足大多数的情况。但是假如,客户希望用户提交联系我们表单的时候,先确认一下,确认无误以后再提交。这在实际中,也是常见的需求。此时,contact模块和webform模块就不大适用了。每个客户对于表单元素的需求也各不相同,所以此时我们可以选择通过定制自己的模块来实现。
假定客户的具体需求是这样的,在联系表单页面,用户可以输入姓名、单位名称、电子邮件、电话号码,邮件正文、访问来源等信息,用户填写了这些信息后,提交表单,进入确认页面;在确认页面,用户可以检查填写的信息,如果信息有误,可以返回来修改,如果信息无误,正式提交;正式提交后,用户看到一个致谢页面,用来“感谢用户的来信”。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal7中,不再使用“AHAH”术语了,而是采用更通用的大家已经习以为常的“Ajax”。有时候,错误的表述被一而再,再而三的使用以后,也就成为了标准,Ajax就是这样。在7里面,表单API对AJAX提供了进一步的支持,我们可以方便的创建Ajax表单,实现动态表单的效果,而不需要借助于第三方模块,这比Drupal6进步了很多。
现在就让我们通过一个实例,来实际学习一下Ajax表单吧。我们在构建各种站点时,经常会遇到让用户选择他所在的地区。比如他的出生地,他的户口所在地,他的常住地址。当一个销售商品时,我们需要客户的配送地址。中国的地址信息,通常包含省、市、县这一信息,为了让网站更加友好,通常将省市县做成联动的表单。我们的这个例子,就是在Drupal中实现省市县三级联动。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先,我们先做一些准备工作。Drupal7中,分类术语被处理成了实体,与以前的版本相比,Drupal7下面的分类更好用了。我们不妨把省市县信息存储成为分类,这样以后用起来也更加方便。首先我们创建一个“地区”词汇表,如图所示。
在里面添加一些省市县测试信息。
注意,这里面,长垣县在2011年6月以前,是隶属于新乡市的,2011年6月1日改为省辖县(市)了,不过这里还是把它放在了新乡市下面,长垣是我老家。题外话。北京市由于是直辖市,所以这里给了一个中间层“区/县”。
实际的省市县信息,可以借助于第三方模块,比如feeds、migrate,导入到Drupal的分类系统中去。这里就不再详细介绍了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,就是创建我们的模块,不妨把这个模块叫做“shengshixian”,在sites/all/modules/custom目录下创建一个shengshixian目录,接着创建两个文件,shengshixian.info、shengshixian.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一章。对于省市县对应的三个表单元素,这里都选用了下拉选择框。
作者:老葛,北京亚艾元软件有限责任公司,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时,我们看到了一个普通的表单。如图所示:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
此时,省份的可选项有“北京市”、“河南省”。市的可选项为空,县的可选项为空。也就是初次访问这个页面时,$default_sheng、$default_shi、$default_xian的值都为空。
接着,当我们在表单页面选择“河南省”的时候,系统进行了Ajax调用,如图所示:
我们看到这里出现了“请稍等”的提示,表明系统进行了Ajax调用。聪明的读者会想到,Ajax回调应该就是hengshixian_sheng_callback了,这样理解是对的,但是还要再补充一点,那就是Drupal在后端重新构建了整个表单,在构建完整个表单后,根据hengshixian_sheng_callback回调函数,向浏览器端只返回变化的元素$form['shi'],浏览器端根据服务器端的返回值,重新设置了 “市”这一表单元素。
在重新构建整个表单的过程中,此时$default_sheng的值,就是“河南省”的tid了,已经不再为空,所以此时“市”的可选项就不再为空了。
当我们选择“新乡市”,系统再次进行了Ajax调用,如图所示:
此时,Drupal在后端再次重新构建了整个表单,在构建完整个表单后,根据shengshixian_shi_callback回调函数,向浏览器端返回更新过的元素$form['xian'],浏览器端根据服务器端的返回值,重新设置了 “县”这一表单元素。
在重新构建整个表单的这次过程中,此时$default_sheng的值,就是“河南省”的tid了,此时“市”的可选项不为空。此时$default_shi的值,就是“新乡市”的tid了,所以此时“县”的可选项就不再为空了。如图所示:
当我们做完这样的流程分析以后,我们再来理解这段代码:
'#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。
作者:老葛,北京亚艾元软件有限责任公司,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)函数可以查看元素类型的默认属性。为方便大家查阅,这里按照字母顺序排列。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
单个复选框。
$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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一组复选框。属性#options是一个关联数组,它的键是复选框的#return_value,对应的值则是用来显示给用户的。#options数组的键不能为0,假如为0了,那么系统就无法判断是否选中了这个选项。
$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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个html容器,用来封装子元素。把子元素放在一个<div>中,并可以为这个div设置类或ID属性。
$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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
日期元素,它是一个由3个下拉选择框联合而成的元素。如果没有提供默认值,#default_value则默认为今天的日期。#default_value和#return_value的格式是一个包含三个元素的关联数组,对应的键为:'year'、month'、'day'。例如:
array('year' => 2011, 'month' => 7, 'day' => 26)
$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中)。当你在表单中定义日期元素时,通过定义这两个属性,就可以使用你自己的代码来替代默认设置了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
字段集元素是用来对其它表单元素进行归类分组的。可将其声明为可伸缩的,这样当用户查看表单并点击字段集标题时,由Drupal自动提供的JavaScript能够动态的打开和关闭字段集。
$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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
创建一个单行的文本字段,用来存储唯一的机读名字。对这个字段会进行验证,以确保它是唯一的,并且不包含非法字符。所有的非法字符将会通过JavaScript替换为有效字符。注意这个元素对中文的支持不是很友好。
它还包含一个非标准的元素属性#machine_name,它是一个关联数组,可包含以下键exists、source、label、replace_pattern、replace。exists是一个回调函数,用来检查机读名字是否存在;source指的是表单元素的#array_parents包含的用户可读名字,可用作机读名字的源,默认为array('name');label,机读名字的标签文本,默认为“机读名字”;replace_pattern,正则表达式,用来匹配机读名字中不允许的字符,默认为'[^a-z0-9_]+';replace,在机读名字中,用来替换非法字符的字符,默认为'_'。
$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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
生成一个表格形式的单选按钮或者复选框,#headers属性用来设置列,#options属性用来设置行。#multiple属性用来决定是用单选按钮还是复选框,如果#js_select属性设置为TRUE,那么还会添加一个基于JavaScript的全选按钮。
admin/content页面的节点内容列表,就用到了这个元素,如图所示:
示例代码,来自(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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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的设置将不起作用。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,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。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
把所有的子字段集显示为垂直的标签。如图所示:
示例代码,来自(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。
作者:老葛,北京亚艾元软件有限责任公司,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']);
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们不妨把模块的名字命名为contactus,在sites/all/modules/custom目录下创建一个contactus目录,接着创建三个文件,contactus.info、contactus.module、contactus.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;
}
作者:老葛,北京亚艾元软件有限责任公司,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为其添加了一个带有ID的div
'#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',
//为表单元素添加两个属性,onmouseout、onmouseover,为了在鼠标移到按钮上时,显示不同的图片效果
'#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
//为其添加了一个带有ID的div
'#prefix' => '<div id="image-submit">',
'#suffix' => '</div>',
);
return $form;
}
我们在这个表单中,用到了markup、textfield、radios、image_button等常用的表单元素,关于这几个表单元素和其它表单元素的详细介绍,可参看后面的表单元素一节。另外,上面的代码中,有不少的注释,我们在这里也就不再过多地重复了。我们讲解一下,这里面的重点。
我们启用这个模块,访问页面contactus。此时我们就可以看到我们创建的表单的实际效果了。如图所示:
作者:老葛,北京亚艾元软件有限责任公司,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;
}
在这里,我们固定了label和input的宽度,并使label向左浮动,从而实现了左右排列的效果。contactus.css文件中的更多的CSS代码,我们就不再这里列出了,大家可以登录我的网站http://zhupou.cn,来下载所有的实例模块的完整代码。
为了控制具体表单元素的外观,我么还用到了属性#prefix和#suffix。使用这两个属性,我们可以方便的为表单元素添加更具有表述性的ID:
'#prefix' => '<div id="visit-radios">',
'#suffix' => '</div>',
我们看一下,表单的最下面,我们这里使用了图片按钮,来替代默认的表单提交按钮元素。我这里的图片按钮看起来有点单薄,这是因为作者的美工效果实在不怎么样。如果换一个好一点的美工,加上上面所给的代码,一定能够实现非常酷的效果。另外注意的是,鼠标移到图片按钮上时,会变色,我们看看前后对比:
前: 后:
这是因为我们作了两个图片,通过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'",
),
最后,让我们看一下访问来源这个表单元素,当我们选择“其它”的时候,表单显示出来一个文本输入框,允许我们输入一个具体的访问来源:
其实这里,我们做了两个表单元素,一个是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函数的命名规则加以命名。关于主题的更多知识,我们可以参看主题系统一章。
作者:老葛,北京亚艾元软件有限责任公司,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('您输入的电子邮件地址格式不正确'));
}
}
在这里,对于用户的输入,我们只对电子邮件字段进行了检查。如果我们在电子邮件输入框中输入了非法的电子邮件,比如:
那么当我们提交表单时,系统就会给出错误提示:
通过上面的例子,我们可以看出,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';
}
在我们的这个处理函数,逻辑相当简单,我们仅仅把表单的提交值,保存到了会话信息中。然后将表单重定向到了确认页面。如果我们不修改$form_state['redirect']的话,表单提交后,仍然会回到当前表单所在的页面。另外,这里最好不要使用drupal_goto,尽量使用$form_state['redirect']的方式。如果存在多个提交处理函数的话,你使用了drupal_goto,那么接下来的提交处理函数可能就被跳过了。如果我们有多个提交处理函数,每个函数中都设置了$form_state['redirect'],那么最后起作用的应该是最后设置的那个。
在两个表单页面之间传值,可以通过URL,也可以先将值保存到数据库中,然后再读取。我们这里用到的会话方式,本质上就是使用的后一种方式。刚学Drupal的时候,对这一点还不是很熟悉,总以为不应该向会话信息中保存太多信息,Java中就是这样,但是Drupal与Java不同,这里的会话信息会保存在数据库中,而不是停留在内存中供下次调用。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下确认页面的回调代码:
/**
* “确认”页面的回调函数
*/
function contactus_confirm_page(){
//我们为这个页面设置标题
drupal_set_title('联系我们');
//这里首先作了判断,如果会话中没有设置contactus_form,返回contactus
if(empty($_SESSION['contactus_form'])){
drupal_goto('contactus');
}else{
}
$render_array = array(
'#markup' => '',
);
//该页面的正文为一个表单,注意对于表单,这里需要使用drupal_render呈现一下。
$render_array['#markup'] .= drupal_render(drupal_get_form('contactus_confirm_form'));
//Drupal7的页面回调,返回的应该是一个数组
return $render_array;
}
这段代码和前面的类似,只是在函数的前面作了一下检查,检查会话中没有设置contactus_form,如果没有,则返回到“联系我们”页面。如果设置了,则会呈现确认表单,我们来看一下确认表单的定义:
/**
* “确认”表单的构建函数
*/
function contactus_confirm_form(){
//添加我们自己的CSS,用来控制表单的样式
drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');
//drupal_set_message(print_r($_SESSION['contactus_form']));
//表单元素“姓名”
$form['name'] = array(
'#title' => t('姓名'),
//表单元素的类型,这里为item
'#type' => 'item',
//'#default_value' => $_SESSION['contactus_form']['name'],
//表单元素的#markup,在Drupal6下面,我用的是#value,在7下面就无法工作,改为了#default_value,还是不行,
//最后改为#markup,才可以了
'#markup' => isset($_SESSION['contactus_form']['name'])?$_SESSION['contactus_form']['name']:"",
);
//表单元素“单位名称”
$form['company_name'] = array(
'#title' => t('单位名称'),
'#type' => 'item',
//这是我在调试的时候,使用#value、#default_value、#description分别测试时的代码,这里保留了。
//'#value' => $_SESSION['contactus_form']['company_name'],
//'#value' => '123456',
'#markup' => isset($_SESSION['contactus_form']['company_name'])?$_SESSION['contactus_form']['company_name']:"",
//'#description' => '123456',
);
//表单元素“电子邮件”
$form['mail'] = array(
'#title' => t('电子邮件'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['mail'])?$_SESSION['contactus_form']['mail']:"",
);
//表单元素“电话号码”
$form['phone'] = array(
'#title' => t('电话号码'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['phone'])?$_SESSION['contactus_form']['phone']:"",
);
//表单元素“邮件正文”
$form['contact'] = array(
'#title' => t('邮件正文'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['contact'])?$_SESSION['contactus_form']['contact']:"",
);
//表单元素“访问来源”
$form['visit'] = array(
'#title' => t('访问来源'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['visit'])?$_SESSION['contactus_form']['visit']:"",
);
//如果访问来源,我们选择了“其它”,此时使用other表单元素的值来替换$form['visit']['#markup']。
if( isset($_SESSION['contactus_form']['visit']) && $_SESSION['contactus_form']['visit'] == 'other'){
$form['visit']['#markup'] = isset($_SESSION['contactus_form']['other'])?$_SESSION['contactus_form']['other']:"";
}
/*
//表单元素“返回”按钮
$form['back'] = array(
'#type' => 'submit',
'#value' => t('返回'),
'#submit' => array('contactus_confirm_form_back'),
);
//表单元素“提交”按钮
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('提交'),
);
*/
//表单元素“返回”图片按钮
$form['image_back'] = array(
'#type' => 'image_button',
'#src' => drupal_get_path('module','contactus').'/images/button2-1.jpg',
//使用这个按钮提交时,对应的提交处理函数为contactus_confirm_form_back
'#submit' => array('contactus_confirm_form_back'),
'#executes_submit_callback' => TRUE,
//为表单元素添加两个属性,onmouseout、onmouseover,为了在鼠标移到按钮上时,显示不同的图片效果
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button2-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button2-2.jpg'",
),
);
//表单元素“提交”图片按钮
$form['image_submit'] = array(
'#type' => 'image_button',
'#src' => drupal_get_path('module','contactus').'/images/button3-1.jpg',
'#executes_submit_callback' => TRUE,
//使用这个按钮提交时,对应的提交处理函数为contactus_confirm_form_submit
'#submit' => array('contactus_confirm_form_submit'),
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button3-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button3-2.jpg'",
),
);
return $form;
}
这个确认表单,其实比前面的第一表单还要简单一些,这里主要就用到了item元素。而图片按钮的用法则和前面一致。对于item元素,我最初使用'#value'属性,后来改为'#default_value'属性,但总是显示不出来值,最后才改用'#markup'。注释里面讲到了这一点,这里再强调一下,因为在Drupal6里面,我用的是'#value'属性,能够正常工作,在Drupal7下面却显示不出来,所以这里给以前熟悉Drupal6的开发者提个醒,避免出现同样的错误。
对于这个确认页面,我们仍然为其添加了CSS文件,用来控制样式,当我们提交表单后,得到一个如下图所示的确认表单。
当我们点击“返回”按钮,就会回到前面的表单页面,当我们点击“提交”按钮时,就会正式的提交填写的数据。
这个表单,我们没有定义验证函数,因为这里没有输入框。对于“返回”按钮和“提交”按钮,我们分别为其使用'#submit'属性为其定义了对应的提交处理函数。我们接下来看看这两个处理函数:
/**
* 返回按钮对应的提交函数
*/
function contactus_confirm_form_back($form, &$form_state){
//简单的重定向到contactus页面
$form_state['redirect'] = 'contactus';
}
/**
* 提交按钮对应的提交函数
*/
function contactus_confirm_form_submit($form, &$form_state){
$values = NULL;
//从会话中获取用户最初提交的值,并将$_SESSION['contactus_form']置为空。
if(empty($_SESSION['contactus_form'])){
drupal_goto('contact');
}else{
$values = $_SESSION['contactus_form'];
unset($_SESSION['contactus_form']);
}
//收件人地址,这里为作者的邮箱
$to = 'g089h515r806@gmail.com';
//用户填写的邮箱地址
$from = $values['mail'];
//发送邮件
drupal_mail('contactus', 'contact', $to, language_default(), $values, $from);
//简单的重定向到致谢页面
$form_state['redirect'] = 'contactus/thanks';
}
“返回”按钮的提交处理函数contactus_confirm_form_back的逻辑非常简单,就是一个重定向。“提交”按钮的处理函数contactus_confirm_form_submit,则包含获取用户最初提交的数据、发送邮件、重定向页面等三个组成部分。这个提交处理函数,是我们整个模块中最复杂的一个了,很多实际的提交处理函数可能比这个还要复杂一点。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这里我们用到了Drupal的邮件发送,drupal_mail是一个API函数,专门用来发送邮件的。我们这里做一下简单的介绍:
drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE)
$module表示hook_mail()所触发的模块的名字,也就是我们将调用{$module}_mail();
$key是hook_mail中定义的键。
$to表示收件人地址。
$language表示合成邮件所用的语言对象,默认为当前语言。
$params,表示用于构建电子邮件的可选参数
$from,表示发件人地址。
$send,表示直接发送邮件。
下面是我们的hook_mail的具体实现:
/**
* 实现钩子hook_mail().
*/
function contactus_mail($key, &$message, $params){
$language = $message['language'];
switch ($key) {
case 'contact':
//邮件的标题
$message['subject'] = '联系我们';
//邮件正文,这里面包含:姓名、单位名称、电子邮件、电话号码、邮件正文、访问来源
$message['body'][] = '姓名:'.$params['name'];
$message['body'][] = '单位名称:'.$params['company_name'];
$message['body'][] = '电子邮件:'.$params['mail'];
$message['body'][] = '电话号码:'.$params['phone'];
$message['body'][] = '邮件正文:'.$params['contact'];
//对于访问来源,如果visit的值我们选择了“其它”,那么此时我们取$params['other'],否则取$params['visit']
$visit = "";
if($params['visit'] == 'other'){
$visit = $params['other'];
}else{
$visit = $params['visit'];
}
$message['body'][] = '访问来源:'. $visit;
}
}
这里$message['body']表示邮件的正文部分,这里把用户提交的信息全部加了进来。这里的$message是一个数组,除了上面用到的subject、body以外,还可以用的键有id、to、from、headers。$key表示邮件的标识。$params则是传递过来用于构建邮件的关联数组。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
最后让我们看一下,“致谢”页面的回调代码,这里仅仅用来显示一段感谢语:
/**
* “致谢”页面的回调函数
*/
function contactus_thanks_page(){
//我们为这个页面设置标题
drupal_set_title('联系我们');
$render_array = array(
'#markup' => '',
);
//Drupal7的页面回调,返回的应该是一个数组
$render_array['#markup'] .= '<div id="contactus-thanks">';
$render_array['#markup'] .= t('感谢您的来信,我们会在第一时间给您回复。');
$render_array['#markup'] .= '</div>';
return $render_array;
}
通过这个例子,希望大家能够熟悉表单的定义,验证函数、提交函数的编写,以及了解两部表单的实现方法。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal的成功,离不开社区的各种用户的支持;同时,作为一个搭建社区类网站的软件,Drupal程序本身对用户系统提供了完备的支持。使用Drupal可以方便的搭建一个社交网络式的站点、一个微博类型的站点、一个社区型电子商务站点,等等。在本章节中,我们首先学习Drupal中用户的结构定义,接着学习有关用户的钩子函数和常见API函数。最后我们讲解有关Drupal用户统一登录的相关技术。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
登录用户,必须启用cookie;匿名用户则不需要cookie。一个关闭了cookie的用户,仍然可以以匿名的身份与Drupal进行交互。
在引导指令流程的会话阶段,Drupal创建了一个全局$user对象,用来作为当前用户的标识。如果用户没有登录(这样就没有建立一个会话cookie),那么它将被当作匿名用户对待。创建匿名用户的代码如下所示(位于bootstrap.inc):
function drupal_anonymous_user() {
$user = new stdClass();
$user->uid = 0;
$user->hostname = ip_address();
$user->roles = array();
$user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
$user->cache = 0;
return $user;
}
另一方面,如果用户当前登录了,那么可以通过使用用户的ID,通过关联表users和sessions,来创建对象$user。两个表中的所有字段都放到了对象$user中。
注意:用户的ID是在用户注册时或者管理员创建用户时所分配的一个序列数。这个ID是users表中的主键。
我们可以向主题中的page.tpl.php添加以下代码:
<?php debug($user); ?>
这样我们就可以很容易的查看$user对象的内部结构了。下面是用户1的$user对象的一个实例结构:
stdClass::__set_state(array(
'uid' => '1',
'name' => 'admin',
'pass' => '$S$Cv6KBmupqlDMHO/yQ4GadB.JGaRlASPQ3rtJ1SS0f.GiSvJO5BRM',
'mail' => 'g089h515r806@gmail.com',
'theme' => '',
'signature' => '',
'signature_format' => NULL,
'created' => '1297216111',
'access' => '1310787362',
'login' => '1308985143',
'status' => '1',
'timezone' => NULL,
'language' => '',
'picture' => '0',
'init' => 'g089h515r806@gmail.com',
'data' => false,
'sid' => 'Bjfjz8wSsOkQ1d7BeYfhzmerrpVfR2VM3MRjQuuciKU',
'ssid' => '',
'hostname' => '127.0.0.1',
'timestamp' => '1310787484',
'cache' => '0',
'session' => 'batches|a:1:{i:2;b:1;}updates_remaining|a:0:{}',
'roles' =>
array (
2 => 'authenticated user',
3 => 'administrator',
),
))
在上面显示的$user对象中,斜体字段意味着数据来自于sessions表。表6-1解释了$user对象的组成部分:
表6-1 $user的组成部分
组成 描述
来自于表users
uid 用户ID。它是表users的主键,在Drupal系统中是唯一的。
name 用户的用户名,在用户登录时使用。
pass 用户的SHA哈希密码,在用户登录时进行对比。由于没有保存用户的原始明文密码,所以密码只能被重置,不能被恢复。
mail 用户当前的email地址。
theme 用户的默认主题。
signature 用户在他/她的账号页面所输入的签名。只有当评论模块(comment module)启用时,当用户添加一个评论时,才会看到签名。
signature_format 签名的文本格式。
created 用户账号创建时的Unix时间戳。
access 用户最近一次访问的Unix时间戳。
login 用户最近一次成功登录的Unix时间戳。
status 1表示正常激活的用户,0表示被拒绝访问的用户。
timezone 用户时区与GMT之间的差异,以秒为单位。
language 用户的默认语言。只有在站点上启用了多语言,并且用户通过编辑账号选择了一个优先语言时,才不为空。
picture 用户头像图片的文件ID。
init 用户注册时提供的初始email地址。
data 这个字段在Drupal7中,不建议使用。请使用Field API来保存需要存储的数据。
来自于表user_roles
roles 分配给当前用户的角色。
来自于表session
sid 通过PHP分配给当前用户会话的会话ID。
Ssid 安全会话ID。
hostname 用户浏览当前页面时所使用的IP地址。
timestamp 一个Unix时间戳,表示用户的浏览器最近接收一个完整页面的时间。
cache 一个用于per-user caching(参看 includes/cache.inc)的时间戳
session $_SESSION的序列化内容。在用户会话期间,模块可以向这里存储任意数据。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
实际情况不同,解决Drupal与其它站点、系统之间的用户同步的方式也不尽相同。Drupal有多个第三方模块,用来解决这样的问题。我们对常用的这些第三方模块加以介绍,方便大家根据自己的实际情况,选择对应的解决方案。
Single sign on(单点登陆)模块
• 项目地址:http://drupal.org/project/sso
• 适用情景:同一台服务器,多个Drupal安装,用户共享,域名不同
• 实现机制:通过建立子站点与主站点之间的会话信息的对应关系,重新实现Drupal的会话机制,来实现单点登陆
Bakery单点登录系统
• 项目地址:http://drupal.org/project/bakery
• 适用情景:Drupal.org官方网站的解决方案,不同的服务器,主域名相同,子域名不同,比如example.com、news.example.com、forum.example.com。
• 实现机制:由于同一个主域名下,cookie可以共享,通过站点之间调整并分发cookie实现。
CAS
• 项目地址: http://drupal.org/project/cas
• 适用情景: JAVA CAS所适用的各种场景,适用范围广。可用于多种情况。
• 实现机制:利用phpCAS库函数,将Drupal与Java的CAS系统相集成,实现统一用户登录。
•
LDAP integration
• 项目地址:http://drupal.org/project/ldap_integration
• 适用情景:符合轻量级目录访问协议的各种情景
• 实现机制:将Drupal与LDAP服务器相集成,利用LDAP服务器解决用户登录。它包含三个子模块ldapauth、ldapgroups、ldapdata。ldapauth可以针对多个LDAP服务器进行认证;ldapgroups可以把ldap group处理为Drupal角色;ldapdata为Drupal访问ldap数据提供读写权限
Services 模块
• 项目地址:http://drupal.org/project/services
• 适用情景:从其它系统访问Drupal中的用户
• 实现机制:Services 模块,对用户进行了封装,对外提供了webservice服务,供其它系统调用。它提供了多种接口方式,比如XMLRPC、 JSON、JSON-RPC、 REST、 SOAP、 AMF等等。Services 模块不是专门用于用户登录的,我们只是借助于它将Drupal的用户信息封装成web service服务。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在前面列举了多种解决方案,但是在实践中,当我们用到统一用户登录的时候,我们可以首先考虑一下,Drupal内置的统一用户登录是否适用。是的,Drupal自带了一种统一用户登录解决方案。
我们首先来看一下,内置的解决方案适用的条件,它包含三点:
• 这些多个站点的域名必须一致,比如 www.example.com、 forums.example.com 、subsite.example.com
• 必须使用 MySQL.
• 这些站点必须在同一服务器上.
这种内置的统一用户登录机制,充分利用了Drupal的数据库表前缀机制,Drupal本身的跨表查询的机制、MySQL的跨数据库查询能力,还有通过设置cookie domain可以共享多个站点的session会话信息。
我们现在来实际操作一下,假定我们的主站点为thinkindrupal.com,同时还包含两个子站点answer.thinkindrupal.com、forum.thinkindrupal.com。我们要在本地搭建环境,所以首先我们需要修改我们的hosts文件,我这里是vista操作系统,使用管理员身份打开hosts文件,然后向里面添加以下信息:
127.0.0.1 thinkindrupal.com
127.0.0.1 answer.thinkindrupal.com
127.0.0.1 forum.thinkindrupal.com
接着,我们在一个空的web根目录,在xampp环境下就是htdocs目录,放置一个Drupal安装,然后在sites目录下,创建3个文件夹,thinkindrupal.com、answer.thinkindrupal.com、forum.thinkindrupal.com。目录结构如图所示:
接着,我们分别按照正常方式安装thinkindrupal.com、answer.thinkindrupal.com、forum.thinkindrupal.com三个站点。这里假定它们对应的数据库分别为tid、tid_answer、tid_forum。没有的话,我们分别为其创建好就可以了。
下面是没有共享用户时的settings.php文件中的数据库配置:
thinkindrupal.com :
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
answer.thinkindrupal.com:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_answer',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
forum.thinkindrupal.com
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_forum',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
在安装Drupal时,我们分别将三个站点用户1的用户名设置为了admin、answer、forum。为了让answer、forum两个子站点能够共享主站的用户信息,让我们分别修改它们的settings.php文件,修改后的配置如下所示:
answer.thinkindrupal.com:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_answer',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => array(
'default' => '',
'users' => 'tid.',
'sessions' => 'tid.',
'role' => 'tid.',
'authmap' => 'tid.',
),
);
forum.thinkindrupal.com
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'tid_forum',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => array(
'default' => '',
'users' => 'tid.',
'sessions' => 'tid.',
'role' => 'tid.',
'authmap' => 'tid.',
),
);
注意,我们在这里设置了prefix,将users、sessions、role、authmap表挂在了主站上。对于其它表,则采用本安装所使用的数据库。现在我们就可以使用admin用户来登录answer.thinkindrupal.com、forum.thinkindrupal.com两个站点了。
此时,如果我们登录了三个站点后,比如从forum.thinkindrupal.com上登出,但是我们访问thinkindrupal.com站点时,仍然是登录的状态。现在美中不足的就是,登录和登出不是同步的。为了能够同步的登录和登出,我们还需要设置settings.php文件中$cookie_domain。我们将3个settings.php文件中的$cookie_domain都设置为:
$cookie_domain = '.thinkindrupal.com';
注意最前面的“.”号是必须要有的。此时当我们从thinkindrupal.com登录后,访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,仍然处于登录状态。如果我们从thinkindrupal.com登出,那么访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,状态就变成了匿名用户。
初次配起来,可能稍微有点复杂,但它却能解决我们的大问题,这就是我们想要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习:
我们了解了$user对象的组成结构
了解与用户相关的钩子函数
能够熟悉使用常见的用户钩子函数
了解统一用户登录,能够与异构系统整合用户
了解统一用户登录的常见解决方案
熟悉内置的单点登录
此时,如果我们登录了三个站点后,比如从forum.thinkindrupal.com上登出,但是我们访问thinkindrupal.com站点时,仍然是登录的状态。现在美中不足的就是,登录和登出不是同步的。为了能够同步的登录和登出,我们还需要设置settings.php文件中$cookie_domain。我们将3个settings.php文件中的$cookie_domain都设置为:
$cookie_domain = '.thinkindrupal.com';
注意最前面的“.”号是必须要有的。此时当我们从thinkindrupal.com登录后,访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,仍然处于登录状态。如果我们从thinkindrupal.com登出,那么访问answer.thinkindrupal.com、forum.thinkindrupal.com两个站点,状态就变成了匿名用户。
初次配起来,可能稍微有点复杂,但它却能解决我们的大问题,这就是我们想要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在请求期间,测试用户是否登录的标准方式,是检查$user->uid是否为0。Drupal有个名为user_is_logged_in()的函数可用来检查登录用户(还有一个相应的user_is_anonymous()函数用来检查匿名用户):
if (user_is_logged_in()) {
$output = t('User is logged in.');
else {
$output = t('User is an anonymous user.');
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在Drupal对用户进行各种相关操作时,为其它模块提供了相应的钩子函数,方便与Drupal系统交互。我们来看看Drupal中用户模块都提供了哪些钩子函数,这些信息可参看user.api.php文件。
名字 |
描述 |
取消用户帐户时的钩子函数。 |
|
修改取消用户帐户方法。 |
|
获取一列用户设置或者资料信息类别。 |
|
删除用户时的钩子函数。 |
|
创建用户帐户时的钩子函数。 |
|
在从数据库中加载用户对象时的钩子函数。 |
|
用户刚登录时的钩子函数。 |
|
用户刚登出时的钩子函数。 |
|
添加用户批量操作。 |
|
用户帐户将被创建或者将被更新时的钩子函数。 |
|
删除用户角色时,调用的钩子函数。 |
|
添加用户角色时,调用的钩子函数。 |
|
将要保存用户角色时,调用的钩子函数。 |
|
用户角色已被更新时,调用的钩子函数。 |
|
用户帐户已被更新时,调用的钩子函数。 |
|
用户帐户信息正被显示时,调用的钩子函数。 |
|
用户已构建;用户使用这个钩子可以修改其结构化的内容。 |
此外,系统模块还提供了一个与用户相关的钩子函数,这就是hook_username_alter(&$name, $account),它是用来修改用户名的,这句话的具体含义,我们在后面通过实例来讲解。
我们从用户的钩子函数中,选择较为通用的与增删改查相关的钩子,比如delete、insert、load、presave、update、view、view_alter。我们检查Drupal系统中所有的钩子函数,把与之相关的加以归纳,总结出如下所示的表。
delete |
insert |
load |
presave |
update |
view |
view_alter |
|
comment |
X |
X |
X |
X |
X |
X |
X |
entity |
X |
X |
X |
X |
X |
X |
X |
field |
X |
X |
X |
X |
X |
||
file |
X |
X |
X |
X |
X |
||
filter_format |
X |
X |
X |
||||
image_style |
X |
||||||
menu |
X |
X |
X |
||||
menu_link |
X |
X |
X |
||||
node |
X |
X |
X |
X |
X |
X |
X |
node_type |
X |
X |
X |
||||
path |
X |
X |
X |
||||
taxonomy_term |
X |
X |
X |
X |
X |
X |
|
taxonomy_vocabulary |
X |
X |
X |
X |
X |
||
user |
X |
X |
X |
X |
X |
X |
X |
user_role |
X |
X |
X |
X |
最左边的一列,表示Drupal中的各种对象,最上面的一行,是与增删改查相关的钩子,格子中的内容表示对应钩子是否存在,X表示存在,空白则表示不存在。我们可以看到,comment、entity、node、taxonomy_term、user最为接近,可以划分为一组;field、file、taxonomy_vocabulary、user_role比较接近;而filter_format、menu、menu_link、node_type、path,主要实现了delete、insert、update,也可以划分为一组。
在Drupal中,User本身是实体的一个实现,我们在后面的学习中,将会陆续的看到,评论、分类术语、节点也都属于实体。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们来看一个来自于实践的实例,有这样的一个网站需求,它用来展示一个中学各个班级的相关信息。注册用户为班级的班主任,每个班主任可以创建一个班级。在这里,班主任就是Drupal中的用户,班级就可以处理成为Drupal中的节点,一个用户只可以创建一个班级节点,这有相应的第三方模块可以控制,我们在这里不讨论这个。在这里,我们关注一个与我们的用户系统相关的需求,当班主任注册自己的帐户时,他希望能够同时输入班级的名字,在创建自己的帐户时,同时创建了班级节点;当班主任登录时,检查它所填写的班级信息的完整性,如果部分信息没有填完,将其重定向到班级的编辑页面,并提示他完善班级信息。
为此,我们首先需要创建一个班级节点类型。导航到admin/structure/types,点击“添加内容类型”链接,在这里我们输入与班级相关的信息:
注意这里面,机读名字的输入,它有点小技巧,首先在名称里面输入拼音,然后点击右边的编辑链接,然后再把名称里面的拼音改为中文。这是由于Drupal对中文的支持不大友好,不过这并不影响使用。
除非我们把系统中的注册用户用作班主任,否则我们还需要创建一个“班主任”用户角色,导航到admin/people/permissions/roles,在这里我们添加一个“班主任”角色。接着编辑班主任的权限,授予它创建编辑班级节点的权限,也就是“班级: 创建新内容”、“班级: 编辑自己内容”。
这样我们就完成了前期的准备工作。让我们来创建这个模块,不妨把我们的模块命名为“class_teacher”,中文名字就叫作“班主任”。和前面一样,我们先准备好文件夹class_teacher,然后向里面添加class_teacher.info、 class_teacher.module文件。
我们向info文件中,添加以下内容,注意文件本身的编码格式:
name = 班主任
description = 在注册班主任用户时,为其添加一个班级节点
core = 7.x
接着,我们向module文件中添加实际的逻辑代码,我们首先实现在用户注册表单上显示一个文本框字段“班级名”,以供用户注册时输入使用。在里面输出以下代码:
<?php
/**
* @file
* 在注册班主任用户时,为其添加一个班级节点.
*/
/**
* 实现钩子hook_form_FORM_ID_alter().
*/
function class_teacher_form_user_register_form_alter(&$form, &$form_state) { $form['class_name'] = array(
'#type' => 'textfield',
'#title' => t('班级名'),
'#maxlength' => 255,
'#description' => t('请输出您所管理的班级名.') ,
'#weight' => 1,
);
$form['#submit'][] = 'class_teacher_user_register_submit';
}
在这里面,我们再次使用了hook_form_FORM_ID_alter钩子函数,注意这里面用户注册所用的表单ID为user_register_form。在上面的代码中,我们为注册表单添加了一个表单元素“班级名”,同时添加了一个提交处理函数。
这样,当我们启用了这个模块后,换一个浏览器,使用匿名用户访问注册页面,此时我们就可以看到我们新加的“班级名”字段了。如图所示:
接下来,我们添加处理函数的相关代码:
/**
* 为注册表单新增的一个处理函数,用来处理班级名和其它.
*/
function class_teacher_user_register_submit($form, &$form_state){
//全局用户,也就是当前新注册用户
global $user;
//班级名的值
$class_name = $form_state['values']['class_name'];
//新建一个节点对象
$node = new stdClass();
//为节点对象赋值
$node->title = $class_name;
$node->uid = $user->uid;
$node->type = 'banji';
//保存节点对象
node_save($node);
//将用户的角色设置为班主任,这里班主任的role id为4
$roles = array(
2 => 'authenticated user',
4 => '班主任',
);
//保存用户对象
user_save($user,array('roles' => $roles));
}
这段代码有详细的注释,它的作用就是保存班级节点,设置新建用户的角色。注意,如果我们将设置用户角色的代码修改为:
//将用户的角色设置为班主任,这里班主任的role id为4
$user->roles = array(
2 => 'authenticated user',
4 => '班主任',
);
//保存用户对象
user_save($user);
此时我们并不能有效地设置用户的角色。这里需要注意一下,这段不工作的代码,是很多时候第一时间所想到的,我最初也这样写。这应该和user_save本身的机制有关系。我们在这里不做更深的研究,有兴趣的读者可以自己研究一下,为什么不工作。
如果你一直在使用本教程里面的模块进行练习,并且在同一站点,那么我们此时需要禁用我们在菜单系统一章用到个人中心模块“my”,这个模块对于改造用户的个人主页,非常有帮助的,但是在这里,与我们的这个模块存在冲突。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们现在想要在用户的主页显示班级信息,这样用户登录进来就能看到自己的班级。向module文件中添加以下代码:
/**
* 实现钩子hook_user_view().
*/
function class_teacher_user_view($account, $view_mode, $langcode){
//如果用户具有创建班级节点的权限,在这里也就是用户的角色为班主任
if (user_access('create banji content', $account)) {
//drupal_set_message('123456');
//首先是读取用户创建的班级节点。
$node = db_select('node', 'n')
->fields('n')
->condition('n.uid', $account->uid)
->condition('n.type','banji')
->execute()
->fetchObject();
//根据节点是否存在,为$markup赋值。
$markup = "";
if(!empty($node)){
$markup = l($node->title, "node/$node->nid", array('attributes' => array('target' => '_blank')));
}else{
$markup = l(t('添加班级'), "node/add/banji", array('attributes' => array('target' => '_blank')));
}
//添加有关班级的信息,注意这里的信息类型为user_profile_item
$account->content['summary']['banji'] = array(
'#type' => 'user_profile_item',
'#title' => t('班级'),
'#markup' => $markup,
'#attributes' => array('class' => array('banji')),
);
}
}
上面的代码中,有详细的中文注释,这里就不再过多解释了,需要注意的是'create banji content'这个权限,我是通过查询数据库最后才确认的,中间是内容类型的机读名字。我们向用户帐户的内容里面,添加了有关班级的信息。这里还需要注意的是信息的结构是一个关联数组,我们这里用到了它的四个键'#type'、'#title'、'#markup'、'#attributes'。user_profile_item类型的输出,由modules\user下面的user-profile-item.tpl.php模板文件负责。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下,当用户登录时,检查他是否创建了班级节点,如果没有创建,提示它创建,如果创建了但是信息不完善,提示他完善班级的资料信息。向module文件中添加以下代码:
/**
* 实现钩子hook_user_login().
*/
function class_teacher_user_login(&$edit, $account){
//首先是读取用户创建的班级节点。
$node = db_select('node', 'n')
->fields('n')
->condition('n.uid', $account->uid)
->condition('n.type','banji')
->execute()
->fetchObject();
if(!empty($node)){
//这里假定节点的默认语言为en,我们以body字段为判断依据,可根据实际调整
//debug($node);
$node = node_load($node->nid);
if(empty($node->body['en'][0]['value'])){
drupal_set_message(t('请完善班级信息'));
//重定向到节点的编辑页面
//drupal_goto('node/'.$node->nid.'/edit');
$url = url('node/'.$node->nid.'/edit');
header('Location: ' . $url);
exit();
}
}else{
drupal_set_message(t('请添加班级信息'));
//重定向到节点的创建页面
//drupal_goto('node/add/banji');
$url = url('node/add/banji');
header('Location: ' . $url);
exit();
}
}
在上面的代码中,我最初使用drupal_goto来进行重定向,在这种情况下,在用户登录页面登录时,可以正常工作,但是如果使用了登录表单区块,重定向就存在问题了,此时将其修改为了原始的header('Location: ' . $url);形式,这样在两种情况下都能正常工作。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
最后,让我们在这个模块中,再增加一个需求,那就是为用户增加一个真实姓名,当显示用户名字时,我们使用真实姓名来代替默认的username。
我们导航到admin/config/people/accounts,点击“管理字段”,进入页面,我们添加新的字段“真实姓名”。
接着点击保存按钮,在接下来的两个表单中,我们不做任何修改,直接保存即可。在Drupal7中,尽可能的使用字段的形式来扩充用户对象,而不是使用已经废弃的profile模块。Drupal7核心自带的profile模块仅仅是用来升级Drupal6,除此以外,再无别的用处,这个模块将在Drupal8中从核心中移出去。
完成这一准备工作以后,让我们向module文件中添加以下代码:
/**
* 实现钩子hook_username_alter().
*/
function class_teacher_username_alter(&$name, $account){
//debug($account);
$user = user_load($account->uid);
// 使用用户的真实姓名来代替默认的username.
if (isset($user->field_real_name['und'][0]['value'])) {
$name = $user->field_real_name['und'][0]['value'];
}
}
这样当我们访问我们的个人主页时,username就被替换为了现在的真实姓名,如图所示:
现在,对于钩子函数hook_username_alter(),我们就能够更好的理解它的用途了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果我们的站点非常简单,那么使用Drupal自带的用户系统就可以了。对于稍微复杂一点的站点,比如我们的站点使用Discuz作为论坛,此时就有了Drupal与Discuz用户集成的需求。除了与常见的Discuz集成以外,在实际中,还存在与.net系统,Java网站系统的用户集成。Drupal的用户系统,为我们提供了这样的机制,能够方便的与各种异构系统相集成。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们首先来看一个简单的实例。如果一个单位使用了Drupal6搭建了自己的网站,觉得比较好用,在另一个新建网站上使用了Drupal7,然后提出了这样的需求,那就是Drupal6站点的用户能够直接登录到Drupal7上,这里假定两个网站都放在同一个服务器上。
我们把这个模块的名字命名为d6user,表示Drupal 6的用户集成,先创建两个文件d6user.info, d6user.module。接着我们向info文件中添加以下代码:
name = Drupal 6 User
description = Drupal 6 老网站用户的同步登录.
core = 7.x
注意文件的编码格式,UTF-8。接着我们向module文件中添加逻辑代码:
<?php
/**
* @file
* 当登录Drupal7站点时,检查是否在drupal6站点存在这个帐户,
* 如果存在,将其保存到新的站点.
*/
/**
* 实现钩子hook_form_FORMID_alter().
*/
function d6user_form_alter(&$form, &$form_state, $form_id){
//这里我们对于表单user_login,user_login_block同时添加了一个验证器
if($form_id == 'user_login' || $form_id == 'user_login_block'){
$form['#validate'] = d6user_login_default_validators();
}
}
我们需要在用户登录时,提前检查Drupal6站点是否存在该用户,所以我们需要修改user_login、user_login_block表单,把它的验证函数集合,替换成我们自己的。我们先来看一下Drupal核心为登录定义的验证器函数:
function user_login_default_validators() {
return array('user_login_name_validate', 'user_login_authenticate_validate', 'user_login_final_validate');
}
我们看到,这里定义了3个验证器函数:
user_login_name_validate:用来检查是否设置了用户名,并且它对应用户的状态是否为0,如果属于这种情况,则为表单返回错误信息;
user_login_authenticate_validate:首先会检查连续尝试登录未成功的次数,如果超过了最大次数,则返回;反之,会根据用户名、密码、以及状态为1,来对users表进行查询,如果返回结果的不为空,为user_login_name_validate设置对应的标记$form_state['uid'];
user_login_final_validate:会根据$form_state['uid']是否存在进行判断,如果不存在,则为表单返回错误信息,反之登录用户。
对于大多数的外部验证系统,只需要修改第二个验证器就可以了。在我们的这个例子当中,我们将三个验证器全部保留了下来,并在第一个验证器函数后面追加我们自定义的验证器函数。代码如下:
/**
* 我们自己的登录验证函数集合,.
*/
function d6user_login_default_validators(){
//注意这里面保留了Drupal自带的验证器,只是在中间加上了一个自定义的验证器。这些验证器的执行存在先后顺序的
return array('user_login_name_validate', 'd6user_user_form_validate', 'user_login_authenticate_validate', 'user_login_final_validate');
}
最后,我们看一下d6user_user_form_validate函数的代码:
/**
* 我们自己定义的登录验证函数,它在user_login_authenticate_validate前面执行.
*/
function d6user_user_form_validate($form, &$form_state){
//form_set_error('name',t('用户名,密码不匹配.')); 这里保留了调试信息
$name = $form_state['values']['name'];
$pass = $form_state['values']['pass'];
if (!empty($name) && !empty($pass)) {
//drupal_set_message('123');
// 这里保留了调试信息,用户调试代码,方便大家测试
$account = db_query("SELECT * FROM {users} WHERE name = :name ", array(':name' => $name))->fetchObject();
//如果用户名存在,则返回.
if ($account) {
return;
}else{
// $sql = "SELECT * FROM {users} WHERE name = :name AND pass = :pass";
//drupal_set_message('123456');
//我们向Drupal6站点的用户表进行查询,检查该用户是否存在。
db_set_active('d6user');
$account = db_query("SELECT * FROM users WHERE name = :name AND pass = :pass", array(':name' => $name, ':pass' => md5($pass)))->fetchObject();
db_set_active('default');
if($account){
// drupal_set_message('1234567');
//此时,用户帐号在Drupal6中存在,并且用户名密码正确
$userinfo = array(
'name' => $name,
'pass' => $pass,
'mail' => $account->mail,
'init' => $name,
'status' => 1,
'access' => REQUEST_TIME
);
//我们将查询到的信息保存到Drupal7的用户表中
$account = user_save(drupal_anonymous_user(), $userinfo);
}
}
}
}
上述代码中,有详细的注释,这里就不再过多解释。我们需要做的是,修改settings.php文件,让Drupal7站点能够直接访问Drupal6站点的数据库。有关Drupal连接多个数据库,可参看数据库API一章。修改的代码如下:
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'thinkindrupal',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',)
;
$databases['d6user']['default'] = array(
'driver' => 'mysql',
'database' => 'drupal6',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
这样,当用户登录时,如果该用户名在Drupal7站点上不存在,那么我们会检查它是否存在于Drupal6站点上,如果存在,自动将其保存到Drupal7的系统中去。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
区块就是位于主内容区域外的一段小的文本,通常可以放在左边栏、右边栏、页首、页尾等这样的边边角角的位置。其实我们对区块是不陌生的,我们在第二章学习模块开发的时候,开发的模块就是用来扩展区块的属性的。只要我们访问过Drupal站点,其实就见识过区块。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
区块包含一个标题和一个描述,主要用于广告、html片段等辅助功能,它一般不用作主内容;因此,区块不是节点,它与节点有着不同的规则。区块也不是Drupal7中的实体。
区域是站点上用来放置区块的部分。区域的创建和显示是由主题(位于主题的.info文件中)负责的,而不是通过区块API来定义。如果一个区块,没有为其指定区域,那么它将无法显示出来。
当然,这里说的是一般情况,在实际中,当启用了一些第三方模块以后,区块本身就是节点,区块本身也是一个实体。此外,我们还可以直接将区块输出在任意模板的任意位置上。这和一般情况大不相同,也就是有例外的时候。
因此,有时候,我们真的很难界定什么才是区块。Drupal7中,一个小小的区块就演化出来了这么多可能。那么我们就直观的先看看Drupal内部自带的区块。
我们有三种方式来创建区块,一种方式是通过后台来添加区块,这种方式通常适用于添加静态的html区块;另一种常见的方式是使用views,对于Drupal中动态的各种区块,我们基本上都可以使用views列出,这应该是实际当中最常见的方式了,因为用的特别多,所以我们把它单列了出来;第三种方式,就是通过实现区块API,用模块代码的方式创建区块。
对于动态的区块,如果是内容列表的形式,那么首先选择views;其次是选用模块代码的方式;当然,如果你想图省事,也可以通过后台创建,不过你需要启用PHP filter模块,并且在区块的正文中,添加对应的PHP代码,这是模块方式和后台方式的混合体。当然,在后台的配置界面使用PHP代码,有各种潜在的危险性,尽可能需要避免。
如果我们开发一个实际的站点,需要定制一些杂七杂八的代码,比如包含多个动态的小区块,而这些区块也不方便使用views实现。此时我们可以为该站点开发一个专有的模块,把所有的这些没有特别关系的代码放在一起。比如,我为中华书局开发网上书店,我就专门创建了一个名为zhbc的模块,里面放置了很多特定于该站点的代码,其中就包含许多动态区块。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
当区块配置表单被提交后,区块系统并不会自动的保存我们新增的设置,此时我们还需要实现钩子hook_block_save,使用它来保存我们新增表单字段的值:
/**
* 实现钩子hook_block_save().
*/
function discuz_topics_block_save($delta = '', $edit = array()) {
// 当区块的delta就是我们前面定义的topics时
if ($delta == 'topics') {
variable_set('discuz_topics_num', $edit['discuz_topics_num']);
variable_set('discuz_topics_base_url', $edit['discuz_topics_base_url']);
}
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过使用Drupal函数variable_set(),我们将帖子数量和基路径的配置保存了下来。最后添加hook_block_view的实现,当区块显示时,返回Discuz最新帖子:
/**
* 实现钩子hook_block_view().
*/
function discuz_topics_block_view($delta = '') {
$block = array();
switch ($delta) {
case 'topics':
$block['subject'] = t('论坛最新帖子');
$block['content'] = discuz_topics_get_recent_topics();
break;
}
return $block;
}
在这里我们使用$block['subject']设置区块的默认标题,使用$block['content']设置区块的内容,我们将区块内容的显示委托给了discuz_topics_get_recent_topics函数,这样使得hook_block_view钩子中的代码逻辑更加清晰一点。现在让我们看看这个函数:
/**
* 区块内容的回调函数.
*/
function discuz_topics_get_recent_topics(){
$output = "";
$num = variable_get('discuz_topics_num', 5);
$base_url=variable_get('discuz_topics_base_url','http://localhost/discuz');
$query = Database::getConnection('default', 'discuz')
->select('posts', 'p')
->fields('p', array('tid', 'subject'))
->condition('p.first', 1, '=')
->range(0, $num)
->orderBy('p.pid','DESC');
$result = $query->execute();
$output .= "<ul class='discuz-topics'>";
foreach ($result as $record) {
$output .= '<li>';
$output .= l($record->subject,$base_url.'/viewthread.php?tid='.$record->tid);
$output .= '</li>';
//drupal_set_message($record->subject);
}
$output .= "</ul>";
return $output;
}
这里,我们通过对Discuz数据库进行查询,来获取里面的最新帖子,将帖子标题显示为链接形式,这样点击这个链接,就会自动地进入对应的论坛帖子页面了。将区块启用后,效果如图所示:
图“论坛最新帖子”区块实际效果图
在一个模块中,我们可以定义多个区块,只需要使用delta标识判断即可,我们在这里只用这么一个作为例子。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在前面提到过,不需要定义模块,只需要使用PHP filter模块,在自定义区块的内容里面,使用PHP代码,也可以实现动态的区块。我们以上面的例子作为对比,采用PHP代码的方式,实现同样的功能。
为此我们需要在区块管理界面添加一个自定义区块,配置信息如图所示:
PHP code形式的实现
在区块正文的文本格式中,我们选择了“PHP code”,同时在正文中输入了以下内容:
<?php
$output = "";
$num = 5;
$base_url = 'http://localhost/discuz';
$query = Database::getConnection('default', 'discuz')
->select('posts', 'p')
->fields('p', array('tid', 'subject'))
->condition('p.first', 1, '=')
->range(0, $num)
->orderBy('p.pid','DESC');
$result = $query->execute();
$output .= "<ul class='discuz-topics'>";
foreach ($result as $record) {
$output .= '<li>';
$output.=l($record->subject,$base_url.'/viewthread.php?tid='.$record->tid);
$output .= '</li>';
}
$output .= "</ul>";
print $output;
?>
这段代码和前面的discuz_topics_get_recent_topics函数中的代码基本一致,只是稍微做了修改。保存这个区块后,显示的效果如图所示:
PHP code形式的区块
这和我们在前面通过模块的形式,实现的功能是完全一样的,所不同的是,我们在这里没有实现任何钩子函数,省了很多功夫。这种方式对于我们这些开发者来说,可以省事不少。我记得以前,我还在公司的时候,做Drupal项目,我和同事合作,我看到他做了一个动态的区块,结果我在模块代码中怎么找都找不到,最后才发现,他放在了自定义区块中。
使用这种方式,在省事的同时,也带来了多种潜在的问题。比如说,程序员的习惯不同,如果一个开发者采用了这种形式,开发了一个动态区块,后来他不再维护这个项目,换了另外的程序员,这种PHP code的形式,就不利于后者理解这段代码的逻辑。此外,对于站点的其它管理员来说,如果他不小心编辑了这个区块,假如在他看来没有做任何修改,只是网站启用了CKeditor标签,不经意之间就会带来PHP语法错误。PHP语法错误的后果,和HTML语法错误的后果完全两回事的。后者仅仅会给个警告,而前者则会使整个网站挂掉。
推荐大家少采用这种方式,即便是采用了,也尽可能的在方便的时候,将其转换为模块的形式。其实模块的形式,熟悉了之后,也复杂不到哪里去。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
为了帮助大家进一步的了解区块系统,建议大家有空的时候研究使用一下这么几个第三方模块。
模块地址:http://drupal.org/project/bean。bean是“block is entity not a node”的缩写。在这里区块是一个实体,而不是节点。
模块地址:http://drupal.org/project/boxes。区块在这里不再是内容了,而仅仅是配置,因此可以使用features模块将其导入导出。
模块地址:http://drupal.org/project/nodeblock。这个模块允许使用一个内容类型来处理区块。
模块地址:http://drupal.org/project/multiblock。这个模块可以将一个区块同时显示在不同的区域中。
模块地址:http://drupal.org/project/cck_blocks。这个模块可以将一个实体的特定字段显示成为区块。这个模块刚好和bean模块反了过来。
研究学习了这几个模块以后,相信你对区块系统的认识会更加模糊了,是的更加模糊了。什么是区块?区块是什么?不同的人会有不同的答案。对于这样的问题,我们没有必要深究,只需要能够灵活的驾驭这些模块,让其帮助我们解决实际问题就可以了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在本章中,你学到了以下几点:
区块是什么以及它们与节点的区别
区块的可见性和位置设置是如何工作的
如何定义一个区块
区块相关的扩展阅读
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
无论是自定义的区块,还是通过编写模块实现的区块,我们都可以在后台对其进行配置,导航到“首页 » 管理 » 结构 » 区块”,在这里找到“Powered by Drupal”区块,点击它的配置链接,我们就可以看到一个典型的区块配置页面,如图所示:
区块配置选项截图
在这里,可配置的有三项:区块标题、区域、可见性。自定义区块还包括区块描述、区块内容两部分。在区块标题中,可以在这里输入想要显示的标题,如果不想输出区块标题,可以输入<none>,留空则表示使用默认标题。区域设置里面可以配置这个区块在主题中所属的区域,如果留空则表示在该主题下面不显示。可见性设置,又分为基于页面路径的可见性设置、基于用户角色的可见性设置、基于内容类型的可见性设置、基于用户的可见性设置。
基于页面路径的可见性设置:在这里可以配置区块显示在哪些页面,或者不显示在哪些页面。此处可以使用通配符,支持路径别名。结合Pathauto模块和这里的选项,可以方便的控制区块的显示。
假如我们创建一个book节点类型,为所有该类型的节点设置别名为:book/item/nid的形式,book相关的列表页面使用books/tid的形式。那么假如我们想将一个区块只显示在book的节点页面和列表页面,那么我们可以选择“只在下列页面”,然后在下面的文本域中输入:
book/item/*
books*
如果启用PHP filter模块,还可以使用PHP代码来控制区块的可见性。在基于页面路径的可见性设置中,选择PHP代码方式。然后在下面的文本域中输入PHP代码。当显示一个页面时,Drupal将运行这里的php代码片段,来判定该区块是否显示。每段代码都应该返回TRUE或FALSE,来指示区块对于特定请求是否可见。
比如将区块显示给登录用户的PHP代码:
<?php
global $user;
return (bool) $user->uid;
?>
基于内容类型的可见性设置:这里可以选择区块显示在哪些内容类型的节点页面下。还拿前面的例子来说,我们想在book类型的节点页面显示该区块,那么只需要在这里选择book内容类型即可。
特定角色可见性设置:管理员可以选择,区块对哪些特定角色的用户可见。如果想将区块显示给登录用户,只需要在这里选择“注册用户”即可。这和前面所给PHP代码,功能上是一致的。
基于用户的可见性设置:管理员可以允许个人用户在他们的帐户设置中,自己设定特定区块的可见性。用户可以在个人账户的编辑页面,来修改区块的可见性。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我在前面提到,在Drupal的区块配置页面中,管理员可以选择将区块放置在哪个区域中。在区块的后台主页面,我们还可以对同一区域内的不同区块进行排序。区域是通过主题层的.info文件定义的,而不是通过区块API,而且不同的主题可以有不同的区域。关于创建区域的更多信息,可参看后续的主题系统一章。
区块的位置可以上下拖动
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在一个页面请求周期内,在构建页面呈现数组时,Drupal允许模块通过实现钩子函数hook_page_build,向页面数组追加其它元素。区块系统通过实现这个钩子函数,把所有区域作为页面元素添加了进来。
function block_page_build(&$page) {
global $theme;
// The theme system might not yet be initialized. We need $theme.
drupal_theme_initialize();
// Fetch a list of regions for the current theme.
$all_regions = system_region_list($theme);
$item = menu_get_item();
if ($item['path'] != 'admin/structure/block/demo/' . $theme) {
// Load all region content assigned via blocks.
foreach (array_keys($all_regions) as $region) {
// Assign blocks to region.
if ($blocks = block_get_blocks_by_region($region)) {
$page[$region] = $blocks;
}
}
...
}
...
}
在每个区域对应的呈现数组中,包含了里面所有启用的区块。
function block_get_blocks_by_region($region) {
$build = array();
if ($list = block_list($region)) {
$build = _block_get_renderable_array($list);
}
return $build;
}
区域内所有区块共同组成了区域呈现数组:
function _block_get_renderable_array($list = array()) {
$weight = 0;
$build = array();
foreach ($list as $key => $block) {
$build[$key] = $block->content;
unset($block->content);
//...
//添加区块的上下文链接,system_main、system_help除外
if ($key != 'system_main' && $key != 'system_help') {
$build[$key]['#contextual_links']['block'] = array('admin/structure/block/manage', array($block->module, $block->delta));
}
$build[$key] += array(
'#block' => $block,
'#weight' => ++$weight,
);
$build[$key]['#theme_wrappers'][] = 'block';
}
$build['#sorted'] = TRUE;
return $build;
}
还记得我们在前面表单系统一章中,讲到的呈现数组吧,是的,区块区域的显示,也采用了呈现数组。在构建呈现数组中,我们对主题内的所有可用区域进行迭代,然后又对区域内所有区块进行迭代。有关呈现数组的相关知识,可参看表单API一章。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在模块中,通过使用一组区块相关的钩子,便可以定义区块了,而且一个模块可以定义多个区块。一旦区块定义好了以后,它就会显示在区块管理界面。另外,管理员可以通过后台管理界面,手动的创建自定义区块;当然,也可以使用views模块创建各种列表区块。在本节中,我们主要讨论,如何通过自定义模块的方式来创建区块。首先让我们了解一下区块的数据库表结构,如图所示。
区块的数据库表结构
每个区块的相关属性大都存储在表block里面,自定义区块的描述和内容信息存放在block_custom表中,与内容类型相关的可见性设置存放在block_node_type里面,而与角色相关的可见性设置则存放在block_role里面。我们先学习一下表block中的各种属性:
bid:这是每个区块的唯一标识ID。
module:这个字段存放的是定义区块的模块的名称。比如,用户登录区块是在用户模块中定义的,它在这里的module值就是user。对于管理员在“首页 » 管理 » 结构 » 区块”中创建的自定义区块,则被认为是由区块模块创建的,此时的module值就是block。
delta:由于一个模块可以在钩子hook_block_info中定义多个区块,所以delta存放了每个区块的键,它们在该模块的hook_block_info钩子实现中是唯一的,但对于整个站点的所有区块,则不一定是唯一的。delta可以是整数,也可以是字符串。
theme:一个区块可以用于多个主题。因此,Drupal需要存放启用了该区块的主题的名称。对于启用了该区块的每个主题,在这个数据库表中都有自己的对应记录。配置选项不能在主题之间共享。
status:它用来追踪区块是否被启用。1意味着启用,0意味着禁用。如果一个区块,没有为其指定一个区域,那么Drupal会将其状态设置为0.
weight:区块的重量,用来判定区块在区域中的相对位置。
region: 放置该区块的区域的名字,例如,footer(页脚)、header(页首)。
custom: 这是这个区块的“基于用户的可见性设置”里面的值。0意味着用户不能控制该区块的可见性;1意味着该区块默认是显示的,但是用户可以隐藏它;2意味着该区块默认是隐藏的,但是用户可以选择显示它。
visibility: 这个值属于“基于页面路径的可见性设置”,用来表示如何判定区块的可见性。0意味着区块将显示在除所列页面以外的所有页面;1意味着区块只能显示在下面的所列页面;2意味着,Drupal将通过执行一段由管理员定义的PHP代码,来判定区块的可见性。
pages: 该字段的内容依赖于visibility字段中的设置。如果visibility字段的值为0或1,那么该字段将包含一列Drupal路径。如果visibility字段的值为2,那么该字段将包含一段PHP代码,通过对其计算来判定是否需要显示区块。
title:这是区块的自定义标题。如果这个字段为空,那么将使用区块的默认标题(由区块所在的模块提供)。如果这个字段为<none>,那么该区块就不显示标题。其余情况,该字段的文本将用作区块的标题。
cache: 这个值用来判定Drupal是如何缓存该区块的。-2表示自定义缓存,从区块缓存系统的角度来看,它等价于不缓存;当标准区块缓存无效时,比如使用了节点访问控制时,此时又需要缓存,那么就可以使用这种方式。–1表示区块不被缓存。1表示基于角色缓存区块,如果没有声明缓存设置,那么这是Drupal区块的默认设置。2表示基于用户缓存区块。4表示基于页面缓存区块。8表示区块将被全局缓存,也就是说,不管是什么角色、什么用户、什么页面,缓存的方式都是一样的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果使用模块的方式创建区块,那么我们就需要了解与区块相关的钩子。区块模块提供的钩子可以参看modules\block文件夹下面的block.api.php文件。主要包含以下钩子:
hook_block_info:
这个钩子用来声明该模块提供哪些区块,同时在这里还可以指定初始化的区块设置。在这个钩子中,可以定义多个区块,每个区块都有一个标识ID,这就是前面提到的delta。Delta主要用于:
作为参数传递给其它区块钩子,比如hook_block_configure、hook_block_view。
在构建区块的html时,用来生成区块的html ID。
用于区块的模板建议block__MODULE__DELTA。
在hook_block_info_alter钩子中,供第三方模块使用。
在每个区块对应的关联数组中,可以包含以下键:
info:这个值是必须的。一个可翻译的字符串(使用t()封装),为站点管理员提供了区块的可读名字,非站点管理员看不到这一信息。
cache: 表示这个区块如何被缓存。可能的值有DRUPAL_NO_CACHE (不缓存区块)、 DRUPAL_CACHE_CUSTOM(使用自定义的缓存)、DRUPAL_CACHE_PER_ROLE (基于角色缓存区块)、DRUPAL_CACHE_PER_USER (基于用户缓存区块,站点用户多时最好不要用这种方式!)、 DRUPAL_CACHE_PER_PAGE (基于页面缓存区块)、DRUPAL_CACHE_GLOBAL (缓存一次区块,所有的都一样)。
properties:添加到区块上的附加元数据数组。常用属性:'administrative'。
status:区块默认是否被启用。
region:为区块设置的默认区域。
weight:它控制着区块在区域内的相对位置。
visibility:基于页面路径的可见性设置。参看上一节的描述。
pages:取决于visibility。参看上一节。
hook_block_configure:
为区块定义一个配置表单。
hook_block_save:
保存来自于hook_block_configure的配置选项。
hook_block_view:
返回区块的呈现数组。
hook_block_info_alter:
在区块信息保存到数据库之前,修改区块的定义信息。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
在这个例子中,我们将创建一个区块,用于获取Discuz论坛中的最新帖子。我们知道,Discuz是中国最流行的论坛软件,很多使用Drupal建站的用户,由于各种原因选择了Discuz作为网站的论坛。此时两者之间用户的集成,内容的集成成了首要解决得问题。我们这里讨论的就是内容的集成,在Drupal里面显示Discuz中的最新帖子。
让我们创建一个名为discuz_topics的模块,它将包含我们的区块代码。在路径sites/all/modules/custom下面创建一个名为discuz_topics的文件夹。然后创建文件discuz_topics.info、discuz_topics.module。
接着,向discuz_topics.info文件中添加以下信息:
name = Discuz论坛帖子
description = 在Drupal中获取Discuz论坛的最新帖子
core = 7.x
接下来,向discuz_topics.module文件中添加以下代码:
<?php
/**
* @file
* 在Drupal中获取Discuz论坛的最新帖子,以区块的形式显示
*/
在添加好这些内容后,就可以在“首页 » 管理 » 模块”下面启用该模块。现在让我们再做一些准备工作,假定我们已经在本地安装好了Discuz论坛,里面已经添加了一些测试内容。我们需要在settings.php文件中添加以下信息,这里假定论坛的数据库名为discuz,数据库用户名为root,密码为空,用于连接Discuz的数据库:
$databases['discuz']['default'] = array(
'driver' => 'mysql',
'database' => 'discuz',
'username' => 'root',
'password' => '',
'host' => 'localhost',
'prefix' => '',
);
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
让我们添加钩子hook_block_info,这样,我们的区块就会显示在区块管理界面上了。
/**
* 实现钩子hook_block_info().
*/
function discuz_topics_block_info(){
$blocks['topics'] = array(
'info' => t('最新的Discuz帖子'),
// 默认使用DRUPAL_CACHE_PER_ROLE.
);
return $blocks;
}
图 新建区块“最新的Discuz帖子”显示在了区块列表页面。
注意数组的info键只显示在管理界面,而不是显示给普通用户的区块标题。我们将在接下来的hook_block_view钩子中实现真正的区块标题。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,我们需要为区块创建相关的配置选项,这个配置选项不是必须的,根据需要选用。我们打算帖子数量是可以配置的,同时对于帖子链接到的论坛网址的基路径,我们也希望能够配置,为此,需要实现hook_block_configure钩子,向module文件中添加以下代码:
/**
* 实现钩子hook_block_configure().
*/
function discuz_topics_block_configure($delta = '') {
$form = array();
//当区块的delta就是我们前面定义的topics时
if ($delta == 'topics') {
//显示在区块配置页面的表单元素
$form['discuz_topics_num'] = array(
'#type' => 'select',
'#title' => t('要显示的最新帖子的条目数量'),
'#default_value' => variable_get('discuz_topics_num', 5),
'#options' => drupal_map_assoc(array(5, 10, 15, 20, 25 , 30)),
);
$form['discuz_topics_base_url'] = array(
'#type' => 'textfield',
'#title' => t('论坛的基路径'),
'#default_value' => variable_get('discuz_topics_base_url', 'http://localhost/discuz'),
);
}
return $form;
}
我们添加了两个新的表单字段,当点击区块的配置链接时,就可以看到这两个字段了,如图所示。
图 我们定义的配置项显示在了区块配置表单中
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于熟悉Drupal6的用户来说,CCK应该是必选的第三方模块,使用这个模块,可以方便的扩展内容类型的字段信息。Drupal7最大的一个改进,就是将CCK模块核心化,在Drupal7里面,它的名字已经换成了Field,并成为Drupal7下面的核心必选模块。有了这个模块,我们就可以方便的为节点、评论、分类术语、用户添加扩展字段了,是的,它可以应用于节点、评论、分类术语、用户等等,而不像Drupal6下面的CCK那样只适用于节点类型。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们在本章,我们将通过创建一个自定义的字段类型,来学习Field API相关的各种知识。我们先来介绍一下这个模块的背景知识:
我以前在给客户做网上书店的时候,就遇到过这样的需求,图书内容类型下面包含一个“拼音名称”字段,用来输入图书名称的拼音,那个时候我们采用了这样的解决办法,使用专门的软件,将书名转换为拼音,这方面有很多现有的工具可用,然后将这一信息导入到Drupal系统中来。那个时候我就在想,如果能够开发一个第三方模块,自动地生成拼音字段,就可以省去很多的麻烦。后来又遇到了这样的一个需求,对节点标题按照拼音的a、b、c、d…z进行检索,此时如果我们自带了一个拼音字段,那么实现起来就会方便很多。这就是我们这个模块的实际的背景。
实现所用的技术,我决定在Drupal7下面实现,然后采用定制一个字段类型的形式,实现节点标题的拼音字段。中文转拼音,的确有很多现成的PHP程序,但是大多数都不是开源的,我突然想到Transliteration模块,也可以完成中文转拼音这一工作。同时对这个模块又做了抽象,除了我们中文有这样的需求以外,日文、俄文…等等,其它文字是否也有这样的需求。这就是我为什么选用Transliteration模块作为转换程序的原因,因为它用途更广,更国际化。
我们需要创建一个字段,这个字段有一个对应的源字段,该字段的值,由源字段的值使用Transliteration模块转换而成,我们不需要负责它的输入。此时我所想到的就是,使用一个textfield就可以搞定这个字段。如果这样的话,其实使用现有的模块也可以解决问题,比如Computed Field模块,只需要把我们的转换代码放在对应的配置中,我想就可以解决问题了,当然我们此时仍然可以开发一个模块。后来我想到了另一个问题,那就是Transliteration模块的转换并不是100%准确的,我不知道其它语言的转换,但是对于中文来说,确实存在这样的问题。因此,这又多出来了一个需求,那就是允许用户手动地调整这个字段。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先我们来判断一下,这个字段什么时候为空,让我们添加以下代码:
/**
* Implements hook_content_is_empty().
*/
function transliteration_title_field_is_empty($item, $field) {
if (empty($item['value'])) {
return TRUE;
}
return FALSE;
}
这段代码相当简单,我们这里没有考虑是否是手动输入,只考虑了文本框。当然用到字段为空的地方并不常见,特别是对于我们这个字段。这里我们主要顺带介绍一下这个hook_content_is_empty钩子函数,在这个钩子函数中,根据字段的输入值,进行判断,当字段为空时放回TRUE,否则返回FALSE。字段是否为空,有时候,对于一些特殊的字段来说,还是很有必要的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,我们需要检查用户的输入是否正确,我们主要检查用户手动输入的情况,看用户输入的字符是否是ASCII字符,因为经transliteration模块转换后的字符串只包含ASCII字符。我们来看看这个字段的验证函数:
/**
* Implements hook_field_validate().
*
* Possible error codes:
* - 'transliteration_title_invalid': The transliteration title is not valid
*/
function transliteration_title_field_validate($obj_type, $object, $field, $instance, $langcode, $items, &$errors) {
foreach ($items as $delta => $item) {
//drupal_set_message($item['value']);
//[\x20-\x7e] [A-Za-z0-9]
if (!empty($item['manual']) && !preg_match('/^[\x20-\x7e]+$/', $item['value'])) {
$message = t('"%value" is not a valid transliteration titles', array('%value' => $item['value']));
/*
//这段代码不能正常工作
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => "transliteration_title_invalid_".$field['field_name'],
'message' => t('It is not a valid transliteration titles'),
);
*/
form_set_error($field['field_name'] .']['.$langcode.']['. $delta .'][value', $message);
}
}
}
这段代码的含义是,如果当前为手动输入,而用户输入的字符包含非ASCII字符时,设置一个错误信息。这里使用了正则表达式,来检查ASCII字符,我费了很大的功夫才找到了这个还能工作的表达式。另外,$errors[$field['field_name']][$langcode][$delta][]这种方式,工作起来总有问题,所以采用了更为原始一点的form_set_error,这在表单API里面讲过。注意$field['field_name'] .']['.$langcode.']['. $delta .'][value',在这个字符串的末位,没有“]”符号,对于这种数组形式的表单元素,就是这种用法。
此时,如果在手动方式下,输入一个包含中文字符的字符串,系统就会报错,表单不能提交。如图所示:
当含有中文字符时,系统提示输入错误
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们完成了字段的验证以后,还有一项工作要做,那就是在将字段的值,保存到数据库之前,重新设置一下,在不是手动输入的情况下,将其设置为源字段经过transliteration模块转换后的值。为此,我们还需要实现一个钩子函数,在module文件中添加以下代码:
/**
* Implements hook_field_presave().
*/
function transliteration_title_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
//drupal_set_message($field['settings']['size']);
if($field['type'] == 'transliteration_title_field'){
if(isset($entity->{$instance['widget']['settings']['source_field_id']})){
$source_field = $entity->{$instance['widget']['settings']['source_field_id']};
if(is_array($source_field)){
$source_field_items = $source_field[$langcode];
foreach ($source_field_items as $delta => $item) {
if(empty($items[$delta]['manual'])){
$upper_lower_case = $instance['widget']['settings']['upper_lower_case'];
if (function_exists($upper_lower_case)) {
$items[$delta]['value'] = $upper_lower_case(transliteration_get($item['value']));
$items[$delta]['manual'] = 0;
}else{
$items[$delta]['value'] = transliteration_get($item['value']);
$items[$delta]['manual'] = 0;
}
}
}
}else{
if(empty($items[0]['manual'])){
$upper_lower_case = $instance['widget']['settings']['upper_lower_case'];
if (function_exists($upper_lower_case)) {
//drupal_set_message('123');
//drupal_set_message( $upper_lower_case);
$items[0]['value'] = $upper_lower_case(transliteration_get($entity->{$instance['widget']['settings']['source_field_id']}));
$items[0]['manual'] = 0;
}else{
//drupal_set_message('123456');
$items[0]['value'] = transliteration_get($entity->{$instance['widget']['settings']['source_field_id']});
$items[0]['manual'] = 0;
}
}
}
}
}
}
这段代码,首先对源字段作了检查,假如默认的源字段为title,那么此时实体(节点)的title属性就是一个字符串值。如果是一个普通的字段,那么这里就会是一个数组。然后对于这两种情况,进行了分别处理。前者相对简单一点,后者稍微复杂了一点。这里面的核心代码是:
$items[$delta]['value'] = $upper_lower_case(transliteration_get($item['value']));
这里我们对源字段的值进行了transliteration转换,然后根据我们的大小写情况,对字符串作了进一步处理,最后将返回值赋给了$items[$delta]['value']。这是最主要的代码,里面又细分了多种情况,但是逻辑基本上一致。
此时,当我们编辑这个节点,取消手动输入,提交后,再回到编辑页面,我们看到系统为我们自动转换成了拼音。如图所示:
系统自动根据标题生成了拼音
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这就是我们想要的效果。不过,当我们访问节点页面时,并没有看到这个字段的内容,系统已经为其生成内容了阿。对于字段的显示,仍然需要由我们的模块来负责,谁让这个字段是由这个模块定义的呢。我们添加最后的两个钩子函数:
/**
* Implements hook_field_formatter_info().
*
*/
function transliteration_title_field_formatter_info() {
$formats = array(
'transliteration_title_default' => array(
'label' => t('Default'),
'description' => t('Default display for the transliteration title.'),
'field types' => array('transliteration_title_field'),
),
'transliteration_title_plain' => array(
'label' => t('Plain text'),
'description' => t('Display the transliteration title as plain text.'),
'field types' => array('transliteration_title_field'),
),
);
return $formats;
}
/**
* Implements hook_field_formatter_view().
*/
function transliteration_title_field_formatter_view($object_type, $object, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'transliteration_title_default':
foreach ($items as $delta => $item) {
//drupal_set_message(print_r($item));
$element[$delta] = array('#markup' => $item['value']);
}
break;
case 'transliteration_title_plain':
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => check_plain($item['value']));
}
break;
}
return $element;
}
首先,我们实现了hook_field_formatter_info钩子函数,在这里我们定义了两个显示格式,一个是默认的格式,一个是纯文本的格式,本质上两者也没有太大的区别。此时清空缓存,然后访问字段的管理显示页面,我们会看到我们定义的字段格式,如图所示:
我们定义的格式显示在了管理显示页面
最后,我们在hook_field_formatter_view钩子中,定义了两种显示格式的具体实现,这里需要注意的是,这个钩子函数返回的是一个呈现数组。当我们再次访问节点页面时,就可以看到这个字段的值了。
我们的这个实例模块,到这里就讲解完毕,这里我将模块的实现,按照我们使用字段的先后顺序,逐一的剥离处理。希望读完这一节后,大家能对通过模块定义一个字段类型有所了解,对我们日常所用的字段有更深一层的把握。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
大多数字段模块,都自带了验证功能,但有时候,这些验证并不能满足我们实际的需求。对于常用的文本字段,其验证功能更弱。假如我们创建了一个book节点类型,为其添加了一个isbn文本字段,此时我们想对isbn作进一步的验证,假定验证规则为,如果输入的字符串的长度既不是10也不是13,那么我们就认为没有通过验证,同时假定当前语言为“und”。
不妨将这个模块命名为isbn_validation,我们来看看这个模块的主代码:
/**
* Implements hook_field_attach_validate().
*/
function isbn_validation_field_attach_validate($entity_type, $entity, &$errors) {
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
if($entity_type =='node' && $bundle == 'book'){
$field_isbn_length = strlen($entity->field_isbn['und'][0]['value']);
if(($field_isbn_length != 10) && ($field_isbn_length != 13)){
$errors['field_isbn']['und'][0][] = array(
'error' => 'field_isbn',
'message' => t('无效的ISBN号.'),
);
}
}
}
对于其它模块定义的字段,我们想对其进行验证的话,可以使用hook_field_attach_validate,在上面的代码中,我们对文本字段field_isbn按照我们的验证进行了检查,如果没有通过,则设置错误信息。注意这里的$errors是通过引用传递的,如果它不为空,表单就通不过验证。这里的结构为:
$errors[字段名][当前语言][delta][]
而右边数组值,则包含两个键,一个是'error',表示错误代码;一个是 'message',表示出现错误时显示给用户的错误提示。
通过编写代码,进行验证,对于不懂程序的人来说,麻烦了很多。不过drupal.org上有一个第3方模块“Field validation”,可以方便的为每个字段实例添加正则表达式规则验证,可以帮我们解决常见的验证问题。项目地址:http://drupal.org/project/field_validation。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果你玩过三国杀,并且还比较熟悉的话,那么你一定了解里面的一个武将,袁术,他最常见的技能就是能够多摸几张牌,相信这个很多人都熟悉,他还有一个技能,叫做“伪帝”,就是说他具有当前主公的主公技,但是他本身不是主公。我这里借用一下,将显示在管理字段页面的非字段称之为“伪字段”,表示它们本身不是字段,但是又具有字段的一些属性。
有些信息,比如节点的标题,它的实现方式没有采用Field API的形式,但是为了管理的方便,Drupal也将其显示在了管理字段界面,Drupal是怎么实现这一点的呢?通过node.module我们可以找到对应的代码:
/**
* Implements hook_field_extra_fields().
*/
function node_field_extra_fields() {
$extra = array();
foreach (node_type_get_types() as $type) {
if ($type->has_title) {
$extra['node'][$type->type] = array(
'form' => array(
'title' => array(
'label' => $type->title_label,
'description' => t('Node module element'),
'weight' => -5,
),
),
);
}
}
return $extra;
}
节点模块就是通过这段代码,将节点的标题显示在了字段管理界面,注意这里将'weight'属性设置为了-5,所以节点的标题默认总是显示在其它字段的上面,如图所示。
在管理字段页面,节点标题总是显示最前面
如果熟悉Ubercart模块的话,我们知道,在这个电子商务模块中,产品属性并没有实现为字段的形式,或许将来会朝这方面发展,它的竞争对手Commerce模块已经完全采用了字段的形式了。在Ubercart的Drupal7版本里面,产品属性就采用了伪字段的形式,下面是该模块的对应代码,这里我们有删减:
function uc_product_field_extra_fields() {
$extra = array();
foreach (uc_product_types() as $type) {
$extra['node'][$type] = array(
'display' => array(
'display_price' => array(
'label' => t('Display price'),
'description' => t('High-visibility sell price.'),
'weight' => -1,
),
……
'add_to_cart' => array(
'label' => t('Add to cart form'),
'description' => t('Add to cart form'),
'weight' => 10,
),
),
);
}
return $extra;
}
当然,这些伪字段并不是具有所有字段的属性,比如在字段的管理显示页面,就没有出现这些伪字段。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
有时候,一个字段模块自己提供的格式器,可能会出现不够用的情况,比如说图片字段,在Drupal7里面,它的格式器可以用来显示原始图片、各种缩略图,但是无法显示图片的链接。如果使用views模块的时候,想输出图片字段的URL,默认是不可能的,当然我们可以通过定制views的模板文件,来输出图片的URL。如果我们能够为图片提供一个URL路径格式器,就可以解决上述的问题了。
是的,在drupal.org上就存在着这样的一个第三方模块“Image URL Formatter”,专门来解决这个问题的,这个项目地址是http://drupal.org/project/image_url_formatter。我们来学习一下这个模块,这个模块的代码大部分是从image模块中直接拷贝过来的。
/**
* Implements hook_field_formatter_info().
*/
function image_url_formatter_field_formatter_info() {
$formatters = array(
'image_url' => array(
'label' => t('Image URL'),
'field types' => array('image'),
'settings' => array('image_style' => '', 'image_link' => ''),
),
);
return $formatters;
}
我们在这里为image字段定义了一个新的格式器“Image URL”,顾名思义,它就是用来输出图片的URL路径的。
/**
* Implements hook_field_formatter_settings_form().
*/
function image_url_formatter_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$image_styles = image_style_options(FALSE);
$element['image_style'] = array(
'#title' => t('Image style'),
'#type' => 'select',
'#default_value' => $settings['image_style'],
'#empty_option' => t('None (original image)'),
'#options' => $image_styles,
);
$link_types = array(
'content' => t('Content'),
'file' => t('File'),
);
$element['image_link'] = array(
'#title' => t('Link image url to'),
'#type' => 'select',
'#default_value' => $settings['image_link'],
'#empty_option' => t('Nothing'),
'#options' => $link_types,
);
return $element;
}
这里的定义和核心image模块中的对应实现完全一致。我们创建一个节点类型,并为其添加图片字段,然后启用这个模块,在管理显示页面,假定我们选择了Image URL作为显示格式,如图所示:
图片在管理显示页面的对应项
当我们点击最右边的配置按钮时,就会出现更多的配置选项,如图所示:
格式器自带的设置项
上述代码就对应于右边的两个配置项。这下我们明白了钩子hook_field_formatter_settings_form是用来做什么的。在这个钩子里面,API函数image_style_options值得学习一下,它用来获取图片的样式选项。
/**
* Implements hook_field_formatter_settings_summary().
*/
function image_url_formatter_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = array();
$image_styles = image_style_options(FALSE);
// Unset possible 'No defined styles' option.
unset($image_styles['']);
// Styles could be lost because of enabled/disabled modules that defines
// their styles in code.
if (isset($image_styles[$settings['image_style']])) {
$summary[] = t('URL for image style: @style', array('@style' => $image_styles[$settings['image_style']]));
}
else {
$summary[] = t('Original image URL');
}
$link_types = array(
'content' => t('Linked to content'),
'file' => t('Linked to file'),
);
// Display this setting only if image is linked.
if (isset($link_types[$settings['image_link']])) {
$summary[] = $link_types[$settings['image_link']];
}
return implode('<br />', $summary);
}
上述代码就对应于图片字段,最右边配置按钮前面的那段描述,如图所示:
格式器具体配置的描述
这个钩子hook_field_formatter_settings_summary,就是为格式器的当前具体配置,返回一个简洁的描述,方便用户浏览。
/**
* Implements hook_field_formatter_view().
*/
function image_url_formatter_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'image_url':
// Check if the formatter involves a link.
if ($display['settings']['image_link'] == 'content') {
$uri = entity_uri($entity_type, $entity);
}
elseif ($display['settings']['image_link'] == 'file') {
$link_file = TRUE;
}
foreach ($items as $delta => $item) {
if (isset($link_file)) {
$uri = array(
'path' => file_create_url($item['uri']),
'options' => array(),
);
}
//debug($item);
$element[$delta] = array(
'#theme' => 'image_url_formatter',
'#item' => $item,
'#image_style' => $display['settings']['image_style'],
'#path' => isset($uri) ? $uri : '',
);
}
break;
}
return $element;
}
在这段代码中,我们返回了图片的呈现数组,在这个呈现数组的每个delta项中,对应的主题函数替换为了theme_image_url_formatter,而不是image模块里面的theme_image_formatter。这里面,API函数file_create_url用来把uri转换为URL,uri就是文件在Drupal里面存储的路径信息,但是这种信息,都是这种形式的“public://img/my123.jpg”,我们需要使用这个函数将其转换一下。
/**
* Implements hook_theme().
*/
function image_url_formatter_theme() {
return array(
'image_url_formatter' => array(
'variables' => array(
'item' => NULL,
'path' => NULL,
'image_style' => NULL,
),
),
);
}
/**
* Returns HTML for an image url field formatter.
*
* @param $variables
* An associative array containing:
* - item: An array of image data.
* - image_style: An optional image style.
* - path: An array containing the link 'path' and link 'options'.
*
* @ingroup themeable
*/
function theme_image_url_formatter($variables) {
$item = $variables['item'];
$image = array(
'path' => $item['uri'],
'alt' => $item['alt'],
);
// Do not output an empty 'title' attribute.
if (drupal_strlen($item['title']) > 0) {
$image['title'] = $item['title'];
}
$output = file_create_url($item['uri']);
if ($variables['image_style']) {
//debug($image);
$image['style_name'] = $variables['image_style'];
$output = image_style_url($image['style_name'], $item['uri']);
}
if ($variables['path']) {
$path = $variables['path']['path'];
$options = $variables['path']['options'];
// When displaying an image inside a link, the html option must be TRUE.
$options['html'] = TRUE;
$output = l($output, $path, $options);
}
return $output;
}
在这个主题函数中,我们返回的是图片的URL,而不是带有img标签的图片了。整个模块,全部都是复制拷贝image模块的代码,只有这里在逻辑上作了修改。image_style_url用来获取图片特定样式下对应图片的URL的。有时候,我们会用到这个函数。
我们看到,为一个已有的字段定义一个新的格式器,并不麻烦,而且很多代码都可以从原有模块中复制过来。采用这种方式的好处是,这个格式器定义好了以后,很多人都可以用,虽然定义的时候稍微麻烦了一点,但是以后会方便很多,送人玫瑰,手留余香。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习,你应该可以:
自定义一个字段模块
为已有字段添加验证
了解什么是伪字段
为已有字段定制格式器
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在让我们实际的构建这个模块,我们不妨把这个模块的中文名字叫做“标题拼音字段”,英文名字叫做“Transliteration title”,这样更国际化一点。我们在sites\all\modules\custom目录下,创建一个名为transliteration_title的文件夹,向里面添加两个文件transliteration_title.info、transliteration_title.module。接着向info文件中添加以下信息:
name = Transliteration title
description = Transliteration title field.
core = 7.x
dependencies[] = transliteration
注意,我们这里使用了依赖关系,表示这个模块依赖于transliteration模块。如果没有安装于transliteration模块,并且这个模块现在还不存在于我们的站点目录下,那么我们就无法启用我们的这个自定义模块。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,我们向module文件中添加以下代码:
<?php
/**
* Implements hook_field_info().
*/
function transliteration_title_field_info() {
return array(
'transliteration_title_field' => array(
'label' => 'Transliteration title',
'description' => t('This field stores and renderes transliteration title.'),
'instance_settings' => array(
'size' => 255,
'upper_lower_case' => 'strtolower',
'source_field_id' => 'title',
),
'default_widget' => 'transliteration_title_field',
'default_formatter' => 'transliteration_title_default',
),
);
}
我们在这里实现了hook_field_info(),在这个钩子中,我们返回了一个关联数组,通过这个关联数组,我们定义了一个新的字段transliteration_title_field,它包含以下键:
label:表示这个字段的名字。
description: 表示这个字段的描述。
instance_settings: 表示这个字段实例的配置,这里给出的是默认配置,在创建好该字段后,可以修改这一配置,这个配置中的键是随意的,取决于你的实际需要。这里我们包含了三个键,size是默认textfield的大小,upper_lower_case表示字符串大小写的情况, source_field_id表示源字段的机读名字。
default_widget:默认的字段输入控件。这里为transliteration_title_field。
default_formatter:默认的字段显示格式器。这里为transliteration_title_default。
此时,如果我们启用这个模块,然后进入一个内容类型的管理字段页面,在字段类型的选择列表中,还找不到我们定义的这个字段,如图所示:
我们新建的字段类型还没有显示出来
除了实现hook_field_info()钩子以外,我们还需要实现多个钩子,才能让这个模块工作起来。我们按照这样的一个顺序,添加字段、配置字段、输入字段、显示字段,这样的一个顺序来开发我们的模块,这也是我们使用一个字段类型的通常顺序。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
让我们实现另一个钩子,向module文件中添加以下代码:
/**
* Implements hook_field_widget_info().
*/
function transliteration_title_field_widget_info() {
return array(
'transliteration_title_field' => array(
'label' => t('Transliteration title'),
'field types' => array('transliteration_title_field'),
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
),
);
}
这里我们定义了这个字段的输入控件,字段输入控件的名字,和字段的名字这里是一样的,在Drupal7里面,很多字段类型都采用了这种习惯。当然,一个字段可以有多个控件类型,我们这里只有一个而已。此时,当我们再次访问一个内容类型的管理字段页面时,我们就可以添加“Transliteration title”字段了,如图所示:
字段类型“Transliteration title”显示了出来
我们看到,如果我们想要在管理字段页面显示出来我们的新建字段,我们需要实现两个钩子hook_field_info()和hook_field_widget_info()。
我们创建一个测试节点类型,比如“test type”,然后向该内容类型添加一个名为“Test”的“Transliteration title”字段,此时一切都能正常工作。但是,当我们配置这个字段,没有我们想要的配置字段,当我们想为这个字段输入内容时,我们发现在节点的输入表单中找不到这个字段。当我们检查这个字段创建的数据库表时,我们发现它并没有包含用来保存用户输入的列,如图所示:
字段对应表field_data_field_test的结构
为此,我们首先需要在添加这个字段时,能够正确地创建我们想要的表结构,对于这个字段,我们需要保存两个值,一个是转换后的字符串,我们不妨采用默认的value;一个表示是否是手动输入的,我们这里使用manual。
接下来,让我们添加一个transliteration_title.install文件,然后向该文件中添加以下内容:
<?php
/**
* @file
* Install file for the transliteration title module.
*/
/**
* Implements hook_field_schema().
*/
function transliteration_title_field_schema($field) {
return array(
'columns' => array(
'value' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'sortable' => TRUE,
),
'manual' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
);
}
注意,这个钩子和hook_schema有点类似,它是专门用于定义字段的表结构的。这里我们按照前面的要求,定义了两列:'value'、'manual'。
让我们删除以前添加的“Test”字段,再重新创建一遍。我们来看一下新字段对应数据库表的表结构,如图所示:
字段对应表field_data_field_test的结构
注意,transliteration_title_field_schema里面的'value'、'manual',对应于这个表结构中的field_test_value、field_test_manual。表结构中的其余列,在所有字段的field_data表中都是通用的。这里前缀“field_test”就是我们这个字段的机读名字。我们来看看对应关系:
“field_test” + “_” + “value” = “field_test_value”
“field_test” + “_” + “manual” = “field_test_manual”
字段名 + 列名
字段对应表名的命名规则:
“field_data” + “_” + “field_test” = “field_data_field_test”
“field_data” + “_” + “body” = “field_data_body”
“field_data” + 字段名
如果我们想要读取我们这个字段中的值,假定当前语言为未定义语言,也就是“und”:
$node->field_test['und'][0]['value'];
$node->field_test['und'][0]['manual'];
这里需要注意的是,并不是所有的字段,都采用“value”存储自己的值,比如分类术语字段,采用的就是“tid”,获取分类术语字段的值,所用的代码就是:
$node->field_myfield['und'][0]['tid'];
而不是
$node->field_myfield['und'][0]['value'];
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下,如何为字段实例添加自己的设置,在我们这里,不需要实现钩子hook_field_settings_form,我们只需要在字段控件的设置表单中,添加自己的设置即可。
/**
* Implements hook_field_widget_settings_form().
*/
function transliteration_title_field_widget_settings_form($field, $instance) {
$widget = $instance['widget'];
$settings = $widget['settings'];
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => isset($settings['size'])?$settings['size']:'255',
'#required' => TRUE,
'#element_validate' => array('_element_validate_integer_positive'),
);
$form['source_field_id'] = array(
'#type' => 'textfield',
'#title' => t('Source field ID'),
'#default_value' => isset($settings['source_field_id'])?$settings['source_field_id']:'',
'#required' => TRUE,
);
$form['upper_lower_case'] = array(
'#type' => 'select',
'#title' => t('Case'),
'#default_value' => isset($settings['upper_lower_case'])?$settings['upper_lower_case']:'',
'#required' => TRUE,
'#options' => array(
'strtolower' => 'Lower case',
'strtoupper' => 'Upper case',
'ucfirst' => 'Capitalize first letter',
'ucwords' => 'Capitalize each word',
),
);
return $form;
}
在hook_field_widget_settings_form钩子实现里面,返回的是一个普通的表单数组,里面包含配置项对应的表单元素。在这里,我们定义了三个配置项,文本栏的尺寸、源字段ID、大小写情况。添加了上述代码以后,重新刷新字段的编辑页面,我们看到上面显示了我们定义的三个字段,如图所示:
我们定义的配置项在字段的编辑页面显示了出来
当提交这个表单时,配置选项的值会保存到控件设置里面,我们在后面会用到这里的设置。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
现在,该向字段里面输入信息了,此时,我们点击创建节点链接,来创建一个该类型下面的一个节点,此时在添加节点的表单中,并没有我们这个字段对应的表单元素。现在就让我们定义具体的输入控件,向module文件里面添加以下代码:
/**
* Implements hook_field_widget_form().
*/
function transliteration_title_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$element += array(
'#type' => $instance['widget']['type'],
'#default_value' => isset($items[$delta]) ? $items[$delta] : '',
);
return $element;
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这段代码看起来有点简单,加进来以后,并没有得到我们想要的效果,是的。我们还需要实现更多一点的钩子。在Drupal的表单元素中,部分表单元素是由其它表单元素复合而成的,比如date、file,以及一些第三方的表单元素。其实我们可以把我们的这个输入控件,定义成为一个表单元素类型,这样更有利于复用。我们来看看这个表单元素类型的定义:
/**
* Implements hook_element_info().
*/
function transliteration_title_element_info() {
$elements = array();
$elements['transliteration_title_field'] = array(
'#input' => TRUE,
'#process' => array('transliteration_title_field_process'),
'#theme' => 'transliteration_title_field',
'#theme_wrappers' => array('form_element'),
);
return $elements;
}
/**
* Process the transliteration_title type element before displaying the field.
*
* Build the form element. When creating a form using FAPI #process,
* note that $element['#value'] is already set.
*
* The $fields array is in $complete_form['#field_info'][$element['#field_name']].
*/
function transliteration_title_field_process($element, $form_state, $complete_form) {
$settings = &$form_state['field'][$element['#field_name']][$element['#language']]['instance']['settings'];
$element['value'] = array(
'#type' => 'textfield',
'#maxlength' => 255,
'#title' => t('transliteration title'),
'#required' => isset($element['#required']) ? $element['#required'] : FALSE,
'#default_value' => isset($element['#value']['value']) ? $element['#value']['value'] : NULL,
);
$element['manual'] = array(
'#type' => 'checkbox',
'#title' => t('manual'),
'#default_value' => isset($element['#value']['manual']) ? $element['#value']['manual'] : NULL,
);
return $element;
}
我们在前面讲表单元素的时候,就提到过hook_element_info(),这里我们使用这个钩子函数定义了一个自己的表单元素类型,为它指定了一个定制的处理函数,在transliteration_title_field_process里面,我们看到这个表单元素是由一个文本字段和一个复选框复合而成的。
让我们清空缓存,然后刷新添加节点的表单页面,此时我们看到了新增的这个表单元素了。它和普通的表单元素没有太大的区别,如图所示:
transliteration_title_field表单元素(输入控件)
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
对于表单元素,我们可以为其定义专门的主题函数,用来控制其外观,现在添加主题相关的代码,如下所示:
/**
* Implements hook_theme().
*/
function transliteration_title_theme() {
return array(
'transliteration_title_field' => array(
'render element' => 'element',
),
);
}
/**
* FAPI theme for an individual text elements.
*/
function theme_transliteration_title_field($vars) {
drupal_add_css(drupal_get_path('module', 'transliteration_title') .'/transliteration_title.css');
$element = $vars['element'];
$output = '';
$output .= '<div class="transliteration-title-field clearfix">';
$output .= '<div class="transliteration-title-field-value">'. drupal_render($element['value']) .'</div>';
$output .= '<div class="transliteration-title-field-manual">'. drupal_render($element['manual']) .'</div>';
$output .= '</div>';
return $output;
}
这里我们实现了hook_theme,在里面注册了theme_transliteration_title_field函数,在后者的具体实现里面,我们为表单元素添加了特有的html标签,用来控制CSS样式。当然,我们这里面并没有添加任何的CSS,所以看起来样式没有什么变化。
现在我们该考虑一下如何处理用户的输入了,也就是需要对用户的输入进行验证,把这个字段的输入,正确的保存到数据库中。当然现在也是可以工作的,但是还没有按照我们的要求工作。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal 7数据库API提供了一个标准的、与数据库供应商无关的抽象层,用来访问数据库服务器。该API尽可能的保留了SQL的语法和功能,同时还提供:
· 轻松支持多个数据库服务器;
· 允许开发者使用更复杂的功能,比如事务;
· 为查询语句的动态构建提供了一个结构化接口;
· 执行安全检查和其它良好习惯;
· 为模块提供了一个干净的接口,用来拦截和修改站点的查询语句。
数据库API文档(http://api.drupal.org/api/group/database/7)直接来自于代码中的注释。这个手册对标准文档进一步的做了补充,为模块开发者提供了与数据库交互的教程,同时方便其他人员从概貌上把握Drupal的数据库系统。注意本手册没有涵盖数据库API的所有特性。
数据库API采用了面向对象的设计理念,因此本文档假定大家对面向对象有所了解。不过,大多数操作都有面向过程的形式,程序员为了代码的可读性,仍然可以使用面向过程的形式。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal数据库层是建立在PHP的PDO基础之上的。PDO提供了一个统一的、面向对象的API,用来访问不同的数据库,但是它没有对不同数据库的方言提供抽象。
由于Drupal的数据库层依赖于PHP的PDO库,所以你在选用主机时,确保你的PHP是支持PDO的。
Drupal中最常见的查询方式就是静态查询。静态查询通常会被一字不差的传递给数据库。通常只有选择查询才可以是静态的。
对于特别简单的查询,通常应该适用静态查询机制。对于更加复杂的查询,比如动态生成的查询或者其它类似查询,则应该使用动态查询。
静态查询的内部方式是使用query方法:
<?php
$result = $conn->query("SELECT nid, title FROM {node}");
?>
经过过程化包装的方式,则更常用:
<?php
$result = db_query("SELECT nid, title FROM {node}");
?>
上面db_query()函数的调用,等价于下面的语句:
<?php
$result = Database::getConnection()->query("SELECT nid, title FROM {node}");
?>
为什么过程化的版本更受欢迎?其中因为Drupal本身主要采用过程化的方式,此外使用db_query是为了保持对Drupal6的兼容,还有就是这样语法更加简洁。
db_query()有3个参数。第一个参数就是查询字符串,在这里所有的数据库表名需要使用花括号进行封装,此外在合适的地方可以使用占位符。第2个参数是包含占位符值的数组。第3个参数是一个可选的包含配置指令的数组,用来指示查询的执行方式。
<?php
$result = db_query("SELECT nid, title FROM {node} WHERE created > :created", array(
':created' => REQUEST_TIME - 3600,
));
?>
上面的代码,将选择过去一小时(3600秒)内创建的所有节点。在查询运行时,占位符:created将会被动态的替换为REQUEST_TIME – 3600的当前值。一个查询可以包含任意多个占位符,但是占位符之间,即便是它们的值相同,但是它们的名字必须是唯一的。根据具体情况,占位符数组可以内联指定(如上例所示),也可以先构建好,然后再传递过来。数组内,元素的顺序对结果并不影响。
以"db_"开头的占位符是为内部系统所保留的,绝不应该在自定义模块中使用。
无论占位符的类型如何,它们都不能被转义或者添加引号。这是因为它们是单独传递给数据库的,数据库服务器可以区分查询字符串和占位符的对应值。
<?php
// 错误:
$result = db_query("SELECT nid, title FROM {node} WHERE type = ':type'", array(
':type' => 'page',
));
// 正确:
$result = db_query("SELECT nid, title FROM {node} WHERE type = :type", array(
':type' => 'page',
));
?>
Drupal的数据库层,包含了有关占位符的一个附加特性。对于一个占位符,如果传递给它的值是一个数组,它就会自动地将占位符扩展为逗号分隔的占位符列表,分别对应于数组中的元素。这意味着开发者不需要考虑他们需要多少个占位符。
我们通过实例来学习一下这一特性:
<?php
// This code:
db_query("SELECT * FROM {node} WHERE nid IN (:nids)", array(':nids' => array(13, 42, 144));
// Will get turned into this prepared statement equivalent automatically:
db_query("SELECT * FROM {node} WHERE nid IN (:nids_1, :nids_2, :nids_3)", array(
':nids_1' => 13,
':nids_2' => 42,
':nids_3' => 144,
));
// Which is equivalent to the following literal query:
db_query("SELECT * FROM {node} WHERE nid IN (13, 42, 144)");
?>
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
db_query()的第3个参数,也就是连接对象query方法的第3个参数,是一个选项数组,用来指示查询应该如何执行。对于大多数的查询来说,常用的指令只有两个。其它值则大多数时候在内部使用。
"target"键用来指定所要使用的目标数据库。如果没有指定,则使用默认的"default"。在下面的例子中,有效值只有一个"slave",这表示如果存在一个从数据库,查询则运行在它的上面。
"fetch"键用来指定用何种方式,来获取查询返回的结果记录。有效的值包括PDO::FETCH_OBJ、PDO::FETCH_ASSOC、PDO::FETCH_NUM、PDO::FETCH_BOTH,或者是一个表示类名的字符串。如果指定的是一个字符串,取回的每条记录都会被放在该类的一个新创建的对象中。其它值对应的行为是由PDO定义的,将返回的记录相应的存放在stdClass对象中、关联数组中、数值数组中、或者数值和关联数组中。有关PDO的相关信息可参考http://php.net/manual/en/pdostatement.fetch.php。默认值为PDO::FETCH_OBJ,通常情况下,尽可能的使用默认值就可以了,在特定情况下,也可以改用其它值。
下面的例子,将在从服务器上(如果存在的话)执行一个查询,并将从结果集中返回的记录存放在关联数组中。
<?php
$result = db_query("SELECT nid, title FROM {node}", array(), array(
'target' => 'slave',
'fetch' => PDO::FETCH_ASSOC,
));
?>
最常用的就是使用foreach()循环,对结果集进行迭代处理。
<?php
$result = db_query("SELECT nid, title FROM {node}");
foreach ($result as $record) {
// Do something with each $record
}
?>
由于所需返回结果的不同,除此以外,还有其它一些方式来获取记录。
为了明确的获取下一条记录,可以使用:
<?php
$record = $result->fetch(); // Use the default fetch mode.
$record = $result->fetchObject(); // Fetch as a stdClass object.
$record = $result->fetchAssoc(); // Fetch as an associative array.
?>
如果没有下一条记录,此时则返回FALSE。通常情况下,应该尽可能的避免使用fetch(),而是选择fetchObject()和fetchAssoc(),后两个语义明确,便于理解。如果你需要使用PDO所支持的其它获取模式,则可以使用fetch()。
为了从结果集中获取单个字段,可以使用:
<?php
$record = $result->fetchField($column_index);
?>
$column_index的默认值为0,表示第一个字段。
为了计算返回记录的总数,可以使用:
<?php
$number_of_rows = $result->rowCount();
?>
为了将所有记录放在单个数组中,可以使用下面的任意一种方式:
<?php
// Retrieve all records into an indexed array of stdClass objects.
$result->fetchAll();
// Retrieve all records into an associative array keyed by the field in the result specified.
$result->fetchAllAssoc($field);
// Retrieve a 2-column result set as an associative array of field 1 => field 2.
$result->fetchAllKeyed();
// You can also specify which two fields to use by specifying the column numbers for each field
$result->fetchAllKeyed(0,2); // would be field 0 => field 2
$result->fetchAllKeyed(1,0); // would be field 1 => field 0
// Retrieve a 1-column result set as one single array.
$result->fetchCol();
// Column number can be specified otherwise defaults to first column
$result->fetchCol($column_index);
?>
注意,fetchAll() 和fetchAllAssoc()在默认情况下,使用在查询上设置的获取模式(数值数组、关联数组、或对象)。通过向其传递一个新的获取模式常量,就可以修改默认的获取模式了。对于fetchAll(),它是第一个参数。对于fetchAllAssoc(),它是第二个参数。
由于PHP 支持对返回的对象调用链式方法,所以通常情况下,会完全跳过$result变量,采用下面的简洁方式:
<?php
// Get an associative array of nids to titles.
$nodes = db_query("SELECT nid, title FROM {node}")->fetchAllKeyed();
// Get a single record out of the database.
$node = db_query("SELECT * FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchObject();
// Get a single value out of the database.
$title = db_query("SELECT title FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchField();
?>
也可以把查询的结果存放在自定义类的对象中去。例如,如果我们有一个名为ExampleClass的类,下面的查询则会返回类型为exampleClass的对象。
<?php
$result = db_query("SELECT id, title FROM {example_table}", array(), array(
'fetch' => 'ExampleClass',
));
?>
如果该类拥有构造函数__construct(),那么将会创建相应的对象,并会向对象中添加相应的属性,接着将会调用__construct()方法。例如,如果你有下面的类和查询:
<?php
class exampleClass {
function __construct() {
// Do something
}
}
$result = db_query("SELECT id, title FROM {example_table}", array(), array(
'fetch' => 'ExampleClass',
));
?>
将会创建相应的对象,并向该对象中添加属id和title,接着执行__construct()。这些事件的执行顺序,是由低于PHP5.2的PHP版本中的一个bug(http://bugs.php.net/bug.php?id=46139)引起的。
如果该对象上有一个__construct()方法,并且该方法需要在属性添加到对象上的前面执行,则可以使用下面例子中所给的方法:
<?php
$result = db_query("SELECT id, title FROM {example_table}");
foreach ($result->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'ExampleClass') as $record) {
// Do something
}
?>
传递给fetchAll的参数,也可以原封不动的用在fetch上。PDO::FETCH_CLASS告诉fetchAll获取返回的结果集,并将值作为属性添加到ExampleClass类型(第二个参数)的对象上。PDO::FETCH_PROPS_LATE告诉fetchAll,先调用__construct(),然后再将结果集作为属性添加到对象上。
动态查询指的是由Drupal动态的构建查询,而不是直接提供一个查询字符串。所有的插入、更新、删除、和合并查询都应该是动态的。选择查询可以是静态的,也可以是动态的。因此,一般动态查询指的就是动态的选择查询。
所有的动态构建的查询都是使用查询对象构建的,而查询对象则源自于对应的连接对象。和静态查询一样,在绝大多数的情况下,我们使用过程化的方式来获取该对象。然而,随后的查询指令,都是采用触发查询对象上的方法的形式,进行调用。
1. 概貌
2. 关联
3. 字段
4. 唯一
5. 表达式
6. 排序
7. 随机排序
8. 分组
9. 范围和限制
10. 表排序
11. 条件语句
12. 执行查询
13. 总计查询
14. 调试
动态的选择查询由db_select()函数开始,如下所示:
<?php
$query = db_select('node', 'n', $options);
?>
在这个例子中,"node"是查询的基表;也就是说,FROM语句中紧跟着FROM的第一个表。注意,这里没有使用花括号;查询构建器会自动地处理这一点。第二个参数是表的别名;如果没有指定的话,将会使用表名。$options数组是可选的,它与静态查询中的$options数组完全相同。
动态选择查询可以非常简单,也可以非常复杂。我们在这里只讲述它的基本原理,如果完全展开的话,一本书也未必写的完。
由于数据库的不同,需要的交互方式也不相同,所以Drupal的数据库层需要为每个数据库类型提供一个驱动。驱动对应文件都放在includes/database/driver中,在这里driver就是表示该驱动的唯一字符串。大多数情况,驱动的键,就是数据库名字的小写版,比如"mysql", "pgsql",或"mycustomdriver"。
每个驱动都包含多个类,它们继承自核心数据库系统中的父类。这些特定于驱动的类,可以根据数据库类型的需要,来覆写各种特性。这些特定于驱动的类,它们的命名方式采用“父类名字”+“_”+”驱动名字”形式。例如,InsertQuery在MySQL下的版本的名字就是InsertQuery_mysql。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
下面是一个有关users表的相对简单的查询。接下来我们会分析这个查询中的每个构成部分,以及会学习一些高级技巧比如表之间的关联。
<?php
$query = db_select('users', 'u');
$query
->condition('u.uid', 0, '<>')
->fields('u', array('uid', 'name', 'status', 'created', 'access'))
->range(0, 50);
$result = $query->execute();
?>
上述代码,大致等价于下面的语句:
$result = db_query("SELECT uid, name, status, created, access FROM {users} u WHERE uid <> 0 LIMIT 50 OFFSET 0");
这个语句,源自于用户的管理页面所对应的代码,我们在这里做了简化处理,我们在后面的学习中,会进一步的用到这一语句。
为了向选择查询添加一个字段,可以使用addField()方法:
<?php
$title_field = $query->addField('n', 'title', 'my_title');
?>
上述代码将会指示查询,从别名为"n"的表中选择字段"title",并为其设定一个别名"my_title"。如果没有指定别名,那么就会自动生成一个。在大多数情况下,自动生成的别名就是字段名字本身。在这个例子中,如果没有指定别名,自动生成的别名将会是"title"。如果该别名已经存在,那么别名就是表名+字段名。在这个例子中,它将会是"n_title"。如果这个别名也存在了,那么就会在这个别名后面追加一个计数器,直到找到一个未使用的别名,比如"n_title_2"。
注意,如果你自己创建并填充查询,并且没有指定别名,而默认的别名又不可用,通常此时你的代码中肯定存在一个bug。然而,如果你正在实现hook_query_alter()钩子,由于你不确定都已经使用了哪些别名,所以你应该总是使用自动生成的别名。
注意,为了选择多个字段,只需要简单得依次调用addField()多次就可以了。注意,在大多数的情况下,字段之间的顺序并不影响结果;如果它影响了,通常意味着该模块中存在一个漏洞。
我们也可以使用fields()方法,一次添加多个字段:
<?php
$query->fields('n', array('nid', 'title', 'created', 'uid'));
?>
上述方法等价于调用四次addField(),一次对应一个字段。不过,fields()中不支持为字段指定别名。并且它返回的是查询对象本身,而不是返回生成的各种别名,此时可以对其继续使用链式方法。如果你需要知道生成的别名,你可以使用addField(),也可以使用getFields()来访问原始的内部字段结构。
调用fields()方法,而没有列出字段,此时相当于使用了一个"SELECT *"查询。
<?php
$query->fields('n');
?>
这将会在查询的字段列表中包含一个"n.*"。此时不会自动生成别名。如果使用SELECT *的表包含了一个字段,而这个字段又恰好在另一个表中被指定,此时,在结果集中,就有可能出现字段名字冲突。在这种情况下,结果集中,只会包含一个同名字段。因此,尽量不要使用SELECT *。
有些查询肯能回产生一些重复结果。在静态查询中,可以使用关键字"DISTINCT"来过滤重复的记录。在动态查询中,可以使用distinct()方法。
<?php
//强制过滤结果集中的重复记录。
$query->distinct()
?>
注意,DISTINCT可能会带来性能上的损失,只有在已经没有其它方法可用的情况下,才会考虑这种方式。
选择查询构建器支持在字段列表中使用表达式。表达式的例子包括“年龄字段的两倍”,“所有名字字段的总计”,或者是标题字符串的一个子集字符串。注意,很多表达式使用SQL函数,并不是所有的函数都可以跨数据库。因此模块开发者,需要自己确保只使用跨数据库的表达式。相关信息可参考:http://drupal.org/node/773090。
为了向一个查询添加表达式,可以使用addExpression()方法。
<?php
$count_alias = $query->addExpression('COUNT(uid)', 'uid_count');
$count_alias = $query->addExpression('created - :offset', 'uid_count', array(':offset' => 3600));
?>
上面的第一行,将会向查询中添加"COUNT(uid) AS uid_count"。第二个参数是字段的别名。在个别情况下,别名已被使用,此时将会生成一个新的别名,而addExpression()的返回值将会是正被使用的别名。如果没有指定别名,将会生成一个默认的"expression" (或者 expression_2、 expression_3、 等等。)
第三个参数,是一个可选的关联数组,里面包含了供表达式使用的占位符的值。
注意,有些表达式,只在Group By语句存在的情况下才起作用。对于查询本身的有效性,需要程序员自己来确保。
为了给动态查询添加一个order by语句,可以使用orderBy()方法:
<?php
$query->orderBy('title', 'DESC');
?>
上面的代码将会指示查询,按照标题字段的降序排列。第二个参数的可选值有"ASC"和"DESC",分别表示升序和降序,默认值为"ASC"。注意,这里的字段名字,应该是addField() 或addExpression()方法中创建的别名,所在,在大多数情况下,你会在这里使用这些方法的返回值,从而确保使用了正确的别名。为了按照多个字段排序,简单的依次调用orderBy()多次就可以了。
查询的随机排序,在不同的数据库系统上,所需要的语法也有所不同。因此,对于随机排序,最好使用动态查询。
为了让给定查询采用随机排序,需要在查询上面调用orderRandom()方法。
<?php
$query->orderRandom();
?>
注意,orderRandom()是一个链式方法,可以与orderBy()一起堆叠使用。也就是说,下面的代码是成立的:
<?php
$query->orderBy('term')->orderRandom()->execute();
?>
在上面的例子中,首先按照"term"字段排序,对于拥有同一术语的记录,则采用随机顺序。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
为了按照给定字段分组,可以使用groupBy()方法。
<?php
$query->groupBy('uid');
?>
上述代码将会指示查询按照uid字段分组。注意,这里的字段名,应该是由addField()、addExpression()等方法创建的别名,所以大多数情况下,你需要使用这些方法的返回值,从而确保使用了正确的别名。为了按照多个字段分组,只需要简单得依次调groupBy()多遍就可以了。
有时候,我们需要对查询返回的结果集做一下限制,取其特定的子集。通常这被称作“范围查询”。在MySQL中,通过使用LIMIT语句来实现这一点。为了限制一个动态查询的范围,可以使用range()方法:
<?php
$query->range(5, 10);
?>
上述代码指示结果集从第6个记录开始,而不是从第一个,并且只返回10条记录。通常我们需要的是返回“前n个记录”,为了实现这一点,需要把第一个参数设置为0,把第二个参数设置为n。
如果调用了range()两次,那么最后一次的调用,会覆写掉前面的。如果调用了这个方法,而不带任何参数,这意味着删除查询上面的所有范围限制。
为了生成一个可以按照任意一列排序的结果集表,可以使用TableSort扩展器,接着添加表头。注意,扩展器会返回一个新的查询对象,从这一点起,你使用的就是这个新的查询对象了。
<?php
$query = $query
->extend('TableSort')
->orderByHeader($header);
?>
一个连接就是类DatabaseConnection的一个对象,这个类继承自PDO类。Drupal要连接的每个数据库,都有一个唯一的连接对象与之关联。对于每个独立的驱动,该连接对象必须是一个子类。
为了访问(并打开,如果需要的话)一个连接对象,使用:
<?php
$conn = Database::getConnection($target, $key);
?>
为了访问当前连接,使用:
<?php
$conn = Database::getConnection();
?>
这将得到当前连接的默认目标。
注意,大多数情况下,你不需要直接请求连接对象。因为很多过程语句都帮你封装好了。除非你要连接多个数据库,并且需要做复杂的操作而又不想改变当前活动的数据库,此时你可以考虑直接访问连接对象。
为设置活动连接,使用:
<?php
db_set_active($key);
?>
关于连接的键和目标,参看下面的一节,数据库配置。
条件语句是一个很复杂的课题,在选择、更新、删除查询中,都会用到条件语句。因此,我们把它独立出来,单独讲解。与更新和删除查询不同,选择查询有两种类型的条件语句:WHERE语句和HAVING语句。Having语句效果和WHERE语句完全相同,唯一的区别是它使用方法havingCondition()和having(),而不是condition()和where()。
一旦构建好了查询,就可以调用execute()来编译和运行查询了。
<?php
$result = $query->execute();
?>
execute()方法将会返回一个结果集/语句对象,这与db_query()返回的结果完全一致,此外,它的迭代和获取方式也完全相同:
<?php
$result = $query->execute();
foreach ($result as $record) {
// Do something with each $record
}
?>
任何查询都有一个对应的“总计查询”。总计查询返回的是源查询的结果集记录的总数。为了获得一个总计查询,可以使用countQuery()方法。
<?php
$count_query = $query->countQuery();
?>
$count_query现在是一个新的动态选择查询,它没有排序限制,当执行时,它返回的结果只有一个值,也就是源查询匹配记录的总数。由于PHP支持在返回的对象上使用链式方法,下面的代码是常见的调用方式:
<?php
$num_rows = $query->countQuery()->execute()->fetchField();
?>
为了检查查询对象在其生命周期中的特定一点所构建的SQL查询,可以调用它的__toString()方法:
<?php
print_r($query->__toString());
?>
· 扩展器
· 修改查询
选择查询支持“扩展器”这一概念。一个扩展器,就是在运行时向选择查询添加功能的一种方式。添加功能的方式有两种,可以是附加新的方法,也可以是改变已有方法的行为。
对于熟悉面向对象设计模式的人来说,扩展器就是装饰模式的一个实现。它通过一种灵活的方式,而不是采用子类继承的方式,从而向对象动态的追加额外的扩展功能。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
为了使用一个扩展器,你首先需要有一个查询对象。extend()方法由查询对象返回一个新的对象,来替换原有的查询对象。例如:
<?php
$query = $query->extend('PagerDefault');
?>
上面的代码中,查询对象调用extend()方法,创建了一个新的PagerDefault查询对象,新对象包含了原来的选择查询,最后返回这个新创建的对象。现在,$query除了具有原来查询对象的各种功能以外,现在又有了新的附加方法可用。
注意,$query的变更不是当即生效的。从extend()中返回了一个新的对象,我们需要把这个对象存放在一个变量中,否则它就会丢失。例如,下面的代码就不会按照你的期望执行:
<?php
$query = db_select('node', 'n');
$query
->fields('n', array('nid', 'title')
->extend('PagerDefault') //这一行返回一个新的PagerDefault对象
->limit(5); //这一行能够工作,因为正被调用的是PagerDefault对象。
//从extend()中返回的对象没有被保存,所以$query仍然是最初的选择对象。
$query->orderBy('title');
//这一行执行的是最初的选择对象,而不是扩展。扩展现在已不存在。
$result = $query->execute();
?>
为了避免这一问题,推荐大家在初次定义查询时,就对选择查询进行扩展。
<?php
$query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort');
$query->fields(...);
// ...
?>
这就确保了,从一开始,$query就是完全扩展后的对象。
还有需要注意的是,尽管扩展器可被堆叠调用(如上例所示),但是并不是所有的扩展器都相互兼容,并且扩展的先后顺序也有可能会对结果产生影响。例如,同时扩展了分页和表排序行为的查询,必须要先扩展PagerDefault。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个扩展器,就是一个简单的实现了SelectQueryInterface接口的类;并且在它的构造函数中,有两个参数:一个选择查询(或者是一个实现了SelectQueryInterface接口的对象)和一个DatabaseConnection对象。它必须重新实现SelectQueryInterface接口中的方法,并把它们传递给构造函数中指定的查询对象,在合适的时候返回自己。
在大多数情况下,所有的这些工作都可以通过继承SelectQueryExtender类来实现,因为后者帮我们处理了这些工作。因此,在实践中,一个扩展器就是SelectQueryExtender的一个子类。类的名字,就是在查询对象的extend()方法中指定的字符串。
那么,也就是扩展类负责添加或者覆写方法。对于它没有覆写的方法,将会透明的传递给包装后的查询对象。当覆写一个方法时,扩展器可能会也可能不会调用底层的查询对象,但是它必须返回SelectQuery接口规定的相同值。在大多数情况下,也就是查询对象本身,或者是扩展器对象本身。
下面的例子,可能帮我们更好的理解这一点。
<?php
class ExampleExtender extends SelectQueryExtender {
/**
* Override the normal orderBy behavior.
*/
public function orderBy($field, $direction = 'ASC') {
return $this;
}
/**
* Add a new method of our own.
*/
public function orderByForReal($field, $direction = 'ASC') {
$this->query->orderBy($field, $direction);
return $this;
}
}
?>
在上面的例子中,我们覆写了orderBy()方法,使其不做任何工作;同时添加了另一个方法,orderByForReal(),它实现了实际的排序行为。当然,这完全是一个例子,仅仅用来说明扩展器是如何工作的,它本身没有任何实际意义。注意,在两个方法中,被返回的$this就是扩展器对象本身。通过返回查询对象,就确保了扩展器没有“迷失”。
任何模块都可以声明一个扩展器。Drupal核心自带了两个非常有用的扩展器:PagerDefault 和TableSort。对于如何在你的代码中使用这些扩展器,可以参看对应的API文档。
动态选择查询的一个重要特性,就是其它模块能够修改它们。这就允许了其它模块把它们自己的限制注入到查询中,这可以是修改一个模块的行为,也可以是在查询运行时应用限制条件,比如节点访问限制。查询变更包含三个组成部分:tagging(标签化)、元数据(meta data)和hook_query_alter()。
任何动态的选择查询,都可以在它上面添加一个或多个标签。这些标签用来标示查询的类型,它允许hook_query_alter()钩子了来判定它们是否需要采取行动。标签应该是小写的字母数字字符,命名规则和PHP变量相同。也就是说,只能包含字母,数字,下划线,并且必须以字母开头。为了向查询中添加一个标签,可以使用addTag()方法:
<?php
$query->addTag('node_access');
?>
为了判定给定查询对象是否具有给定标签,有三种方法可用:
<?php
//如果这个查询对象拥有这个标签,返回TRUE。
$query->hasTag('example');
//如果查询对象具有所有的指定标签,返回TRUE。
$query->hasAllTags('example1', 'example2');
//如果查询对象具有任意一个指定的标签,返回TRUE。
$query->hasAnyTag('example1', 'example2');
?>
hasAllTags() 和hasAnyTag()可以带有任意数量的参数,每个参数表示一个标签。在这里,参数的位置对结果没有影响。
这里没有硬性规定使用哪些标签,但通常使用一些标准标签。下面是部分标准标签:
node_access
这个查询应该包含基于节点访问控制的限制。
translatable
这个查询应该包含可被翻译的列。
term_access
这个查询应该包含基于分类术语的限制。
views
这个查询由views模块生成
还可以为查询添加一些元数据,从而为alter钩子提供附加的上下文信息。元数据可以是任意的PHP变量,也可以用一个字符串作为键。
<?php
$node = node_load($nid);
// ... Create a $query object here.
$query->addMetaData('node', $node);
?>
元数据没有内在的意义,它本身对查询对象没有任何影响。它的存在仅仅是为了向alter钩子提供附加信息,并且通常只有当查询具有特定标签时才起作用。
为了访问一个查询上的给定元数据,使用getMetaData()方法。
<?php
$node = $query->getMetaData('node');
?>
如果没有为该键指定元数据,那么将会返回NULL。
一个查询就是传递给数据库连接的SQL语句。Drupal的数据库系统支持6种类型的查询:静态、动态、插入、更新、删除,还有合并。有些查询采用SQL字符串模板(准备语句)的形式,有些则采用面向对象的查询构建器。“查询对象”表示一个查询构建器的实例,适用于各种查询类型。
标签化和元数据本身并不起任何作用。它们的存在就是为hook_query_alter()提供信息,在hook_query_alter()中,可对选择查询作任何修改。
所有的动态选择对象都是通过execute()方法,在查询字符串被编译之前,传递给hook_query_alter()的。这就为模块提供了一个机会,方便它们按照自己的需要来操作查询。hook_query_alter()接收单个参数:选择查询对象本身。
<?php
/**
* 实现hook_query_alter()
*/
function example_query_alter(QueryAlterableInterface $query) {
// ...
}
?>
还有一个特定于标签的alter钩子,hook_query_TAG_NAME_alter(),它会在通用的alter钩子调用后,对于给定查询具有的所有标签,调用这个钩子。对于拥有标签'node_access'的查询,会为其调用下面的代码:
<?php
function example_query_node_access_alter(QueryAlterableInterface $query) {
// ...
}
?>
对于hook_query_alter(),有两点需要明确一下。
1. $query参数不是通过引用传递的。由于它本身是一个对象,由于PHP5及后续版本处理对象的特定方式,所以我们这里不需要使用引用。因此这里使用引用是没有必要的。另外alter钩子没有返回值。
2. 参数类型被明确指定为了QueryAlterableInterface。虽然不是必须的,但是明确的指定参数类型,可以更好的避免在运行时传递过来错误的变量类型。还有类型被指定为了QueryAlterableInterface,而不是简单的使用SelectQuery,这是为了提供更好的向前兼容性。
在alter钩子函数中,除了避免执行查询本身以外(因为这样会造成无限循环),我们可以对查询对象采取任何操作。 alter钩子可以根据查询上的标签和元数据来决定执行哪些动作。模块开发者,可以在查询对象上调用前面所列的各种附加方法,从而为查询添加额外的字段、关联、条件语句等等;也可以请求访问查询对象的内部数据结构,从而对它们进行直接操作。第一种方法主要用来向查询添加新的信息,而后一种方法允许alter钩子从查询中删除信息,或者操作已经排好队列的指令。
<?php
$fields =& $query->getFields();
$expressions =& $query->getExpressions();
$tables =& $query->getTables();
$order =& $query->getOrderBy();
$where =& $query->conditions();
$having =& $query->havingConditions();
?>
这里特别提醒的一点是,在上面的代码中,所有返回的值都是引用形式(=&),这样就确保了alter钩子访问的数据结构和对象的完全一致。上面的所有方法,返回的都是一个数组,它们的结构,在includes/database/select.inc中,SelectQuery的内联文档中有具体介绍。
插入查询必须使用一个查询构建器对象。对于LOB (大对象, 比如MySQL中的TEXT) 和BLOB(二进制大对象)字段,某些数据库需要特殊处理,所以我们需要一个抽象层,从而允许独立的数据库驱动,按照它们自己的要求实现自己的特殊处理。
插入查询使用db_insert()函数作为开始,如下所示:
<?php
$query = db_insert('node', $options);
?>
上述代码创建了一个插入查询对象,它将向节点表中插入一个或多个记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
插入查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
插入查询对象支持多个不同的用法模式,用来满足不同的需求。在一般情况下,它的工作流程包括,指定需要插入到的字段,为这些字段指定将要插入的对应值,执行查询。下面列出了最常见的使用模式。
对于大多数插入查询,推荐的形式就是紧凑形式:
<?php
$nid = db_insert('node')
->fields(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
->execute();
?>
上面的代码就等价于执行下面的查询:
INSERT INTO {node} (title, uid, created) VALUES ('Example', 1, 1221717405);
上面的代码,把插入流程的关键部分,链到了一起。
<?php
db_insert('node')
?>
这一行代码为节点表创建了一个新的插入查询对象。
<?php
->fields(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
?>
fields()方法的参数有多种形式,但是最常见的就是单个关联数组。数组的键,就是要插入的列名,数组的值就是要插入的对应值。这将会为指定的表生成一个插入查询对象。
<?php
->execute();
?>
execute()方法告诉查询开始运行。除非调用这个方法,否则查询就不会执行。
对于查询对象上的其它方法,返回的都是查询对象本身,但是execute()方法与此不同,它返回的是插入查询向数据库中插入记录的自增字段(如果存在的话)的值。这也就是为什么,在上面的例子中,我们把它的返回值,分配给了$nid。如果没有自增字段,那么execute()的返回值就未被定义,此时就不应该使用它的返回值。
在大多数情况下,这就是插入查询的推荐形式。
<?php
$nid = db_insert('node')
->fields(array('title', 'uid', 'created'))
->values(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
->execute();
?>
这段代码和前面的代码相比,输出的结果完全相同,就是罗嗦了一点。
<?php
->fields(array('title', 'uid', 'created'))
?>
当调用fields()时,使用的参数是一个索引数组而不是关联数组,此时它只设置了在查询中用到的字段(数据库列),而没有为它们设置值。这在插入多值的查询中非常有用,我们在后面会讲到这一点。
<?php
->values(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
?>
这个方法指定一个关联数组,数组的结构就是左边的为要插入的字段名字,右边为对应的值。values()也可以使用索引数组。如果使用的是索引数组,那么值的顺序必须要与fields()方法中字段顺序保持一致。如果用的是关联数组,则数组内元素的顺序可以随意。一般情况下,为了代码的可读性,尽量选用关联数组的形式。
由于大家大多数时候都使用紧凑形式,所以这种查询形式很少使用。只有当运行多值插入查询时,才会将fields()和values()分开使用。
插入查询对象上还可以使用多值集合。也就是说,values()可被调用多次,从而将多个插入语句排在一起调用。这种形式的具体执行情况,则取决于数据库自身的能力。对于大多数的数据库,多值插入语句都会放在一个事务中一起执行,这样能够保证更好的数据一致性和执行效率。对于MySQL,它将会使用MySQL的多值插入语法。
<?php
$values = array(
array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
),
array(
'title' => 'Example 2',
'uid' => 1,
'created' => REQUEST_TIME,
),
array(
'title' => 'Example 3',
'uid' => 2,
'created' => REQUEST_TIME,
),
);
$query = db_insert('node')->fields(array('title', 'uid', 'created'));
foreach ($values as $record) {
$query->values($record);
}
$query->execute();
?>
在上面的例子中,我们把三个插入查询放在一个单元中执行,对于不同的数据库驱动,将会为其使用最有效的方法。注意,在这里我们把查询对象保存在了一个变量中,这样我们就可以对$values进行循环并重复的调用values()方法了。
在退化形式下,上面的例子等价于执行下面3条查询:
INSERT INTO {node} (title, uid, created) VALUES ('Example', 1, 1221717405);
INSERT INTO {node} (title, uid, created) VALUES ('Example2', 1, 1221717405);
INSERT INTO {node} (title, uid, created) VALUES ('Example3', 2, 1221717405);
注意,对于一个多值插入查询,execute()的返回值未被定义,因此不要调用它的返回值,数据库驱动的不同,也会对返回值产生影响。
如果你想使用其它数据库表中的结果来填充当前的数据库表,你可以使用SELECT语句将其取出,然后在PHP中对其进行迭代并将其保存到新表中去,另外你还可以使用INSERT INTO...SELECT FROM查询,在这里从SELECT查询返回的每条记录都会送入到INSERT查询中。
在这个例子中,我们想构建一个表"mytable",它包含两个字段节点ID和用户名,我们从数据库中取出所有page类型节点的对应信息,然后插入到表"mytable"中。
Drupal 6
<?php
db_query('INSERT INTO {mytable} (nid, name) SELECT n.nid, u.name FROM {node} n LEFT JOIN {users} u on n.uid = u.uid WHERE n.type = "%s"', array ('page'));
?>
Drupal 7
<?php
//构建选择查询
$query = db_select('node', 'n');
//关联users表
$query->join('users', 'u', 'n.uid = u.uid');
//添加我们想要的字段
$query->addField('n','nid');
$query->addField('u','name');
//添加一个条件,把节点限制在类型page上去。
$query->condition('type', 'page');
//执行插入
db_insert('mytable')
->from($query)
->execute();
?>
在正常情况下,如果你没有为给定字段指定一个值,而表的模式(schema)定义了默认值,那么数据库会自动的为你插入默认值。然而,在大多数的情况下,你需要明确的指示数据库使用默认值。它包括你是否想为整个记录使用所有的默认值。为了明确的告诉数据库,为给定的字段使用默认值,我们可以使用useDefaults()方法。
<?php
$query->useDefaults(array('field1', 'field2'));
?>
这段代码指示查询,为字段field1和field2使用数据库定义的默认值。注意,如果你指定的字段,已经在fields()或values()中使用,此时就会抛出一个异常,并提示你存在一个SQL错误。
更新查询必须使用一个查询构建器对象。对于LOB (大对象, 比如MySQL中的TEXT) 和BLOB(二进制大对象)字段,某些数据库需要特殊处理,所以我们需要一个抽象层,从而允许独立的数据库驱动,按照它们自己的要求实现自己的特殊处理。
更新查询使用db_update()函数作为开始,如下所示:
<?php
$query = db_update('node', $options);
?>
上述代码创建了一个更新查询对象,它将修改节点表中的一个或多个记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
更新查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
更新查询的结构是非常简单的,它包含一个要设置的键值对儿集合,还有一个WHERE语句。有关WHERE语句的结构,我们在这里只是简单的说明,更详细的介绍可参看后面的条件语句一节。
典型的更新查询如下所示。
<?php
$num_updated = db_update('node')
->fields(array(
'uid' => 5,
'status' => 1,
))
->condition('created', REQUEST_TIME - 3600, '>=')
->execute();
?>
上面的查询将会更新节点表中最近一小时内创建的所有记录,把它们的uid字段设置为5,把status字段设置为1。fields()方法中的参数是单个关联数组,用来指定要设置哪些字段以及对应的值,当指定的条件满足时,就会进行相应更新。注意,与插入查询不同,UpdateQuery::fields()只接收一个关联数组。还有,数组中字段的顺序,以及方法的调用顺序,与结果无关。
上面的代码就等价于执行下面的查询:
UPDATE {node} SET uid=5, status=1 WHERE created >= 1221717405;
execute()将返回查询影响到的记录总数。注意,影响到的与匹配到的通常不一致。在上面的查询中,如果一个已有记录,它的uid为5,status为1,并且满足条件语句,此时它就是匹配的,但是由于它里面的数据没有被修改,也就是它没有被查询影响到,因此不会记入到返回值中。作为一个副作用,这使得更新查询无法判定一个记录是否存在。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
删除查询必须使用一个查询构建器对象。它们使用db_delete()函数作为开始,如下所示:
<?php
$query = db_delete('node', $options);
?>
上述代码创建了一个删除查询对象,它将会删除节点表中的对应记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
删除查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
删除查询的结构是非常简单的,它只包含一个WHERE语句。有关WHERE语句的结构,我们在这里只是简单的说明,更详细的介绍可参看后面的条件语句一节。
下面是一个完整的删除查询示例。
<?php
$num_deleted = db_delete('node')
->condition('nid', 5)
->execute();
?>
上面的查询将会删除{node}表中nid为5的所有记录。它等价于执行下面的查询:
DELETE FROM {node} WHERE nid=5;
execute()方法将会返回被删除的记录数量,作为查询的结果。
合并查询是一种特殊的混合查询类型。虽然在SQL2003规范中为它们定义了语法,但实际上几乎没有任何数据库支持该标准语法。然而,大多数数据库通过使用特定于数据库的语法,都提供了一些替代实现。在Drupal中,合并查询构建器把合并查询的概念抽象成为了一个结构化的对象,根据数据库的不同,它们会被编译成特定于数据库的语法。
在一般意义上,合并查询就是插入查询和更新查询的联合体。如果满足了给定条件,比如包含给定主键的记录已经存在,此时运行一个更新查询。如果给定条件未满足,则运行其它查询,比如插入查询。在大多数情况下,它等价于:
<?php
if (db_query("SELECT COUNT(*) FROM {example} WHERE id=:id", array(':id' => $id))->fetchField()) {
// Run an update using WHERE id = $id
}
else {
// Run an insert, inserting $id for id
}
?>
实际的具体实现,在不同的数据库之间,大不相同。注意,尽管合并查询在概念上是一个原子操作,但它们是否真的是原子操作,则取决于具体数据库的实现。例如,在MySQL中,它实现为了单个原子查询,不过上面的退化情况则不是原子操作。
下面列出了合并查询的常见的习语。
语句对象就是选择(Select)查询的结果。它的类型应该是DatabaseStatement,或者是DatabaseStatement的子类。DatabaseStatement扩展了PDOStatement类。
Drupal为所有的查询使用预处理语句(prepared statements)。一个预处理语句就是一个查询的模板,将会向模板中填充数值然后执行。我们可以把预处理语句看作是SQL的函数形式,需要向里面传递一些参数才可使用。
在正常的PDO中,首先必须明确的准备一个语句对象,然后把相应的值绑定到查询中的占位符上,并执行查询。接着,语句可以作为结果集被迭代处理。也就是说,在语句执行以后,语句和结果集就成为了同义词。
Drupal没有直接暴露预处理语句对象。模块开发者需要使用一个查询对象或者一个SQL字符串来执行查询,接着该查询就会返回相应的语句对象。因此,术语“语句对象”和“结果集对象”表示的含义基本相同。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->execute();
?>
在上面的例子中,我们指示查询在"example"表上进行操作。我们接着指定了一个主键字段,'name',它的值为$name。我们接着指定了要设置的值的数组。
如果存在这样一个记录,它的"name"字段的值为$name,那么在这个记录中,字段field1和field2将会被设置为对应的值。如果不存在这样的记录,那么就会创建一个,其中"name"的值为$name,"field1"的值为$value1,"field2"的值为$value2。因此,在查询的最后,无论记录是否已经存在,最终的结果都是相同的。
在有些情况下,我们可能会需要根据记录是否存在(由key()中的字段标识),来有条件的设置值。这样做有两种方式。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->update(array(
'field1' => $alternate1,
))
->execute();
?>
上面的例子和最初的例子相比,在对应记录不存在时,结果完全相同;在记录已经存在时,在这里我们更新它,把field1字段的值设置为$alternate1,而不是$value1,对于field2字段则不作任何操作。update()方法的参数可以是一个关联数组,也可以是两个数值数组,一个字段一个值,并且必须使用相同的顺序。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->expression('field1', 'field1 + :inc', array(':inc' => 1))
->execute();
?>
在这个例子中,如果记录已经存在,那么field1的值将被设置为当前值加1。如果当特定事件发生时你想为计数器作加法,对于这种计数器查询,上述方式非常有用。无论记录是否存在,field2仍然被设置为相同的值。
注意, expression()可被调用多次,每次对应一个需要设置为表达式的字段,如果记录存在,就会对该字段按表达式进行设置。第一个参数是字段;第二个参数是一个SQL片断,也就是表达式,它用来指示应该如何设置字段;第三个参数是可选的数组,里面包含了占位符的值,这些值将插入到表达式中。
如果一个字段已经出现在了fields()当中,这个字段仍然可以用在expression()中,两者之间没有冲突关系。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->updateExcept('field1')
->execute();
?>
updateExcept()方法,可以使用由字段构成的数组作为参数,也可以使用一列字段分别作为单独的参数。对于updateExcept()中指定的字段,如果记录已经存在,那么它们就不受影响。也就是说,如果存在一个name = $name的记录,那么field2将被设置为$value2,而field1将被完全忽略,原来什么值就是什么值,但是如果记录不存在,那么它会被设置为$value1。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
使用上面的API,程序员很有可能定义出来没有任何意义的查询,比如一个字段,在假定记录存在的情况下,既被设置为了忽略,又被设置为了表达式。下面的规则,可以帮助减少潜在的错误:
· 如果一个字段设置成一个expression(),那么它的优先级高于update()和updateExcept()。
· 如果在update()中已经指定了值,那么updateExcept()将被忽略。也就是update()的优先级高于updateExcept()。
· 如果值被指定在update()中,那么在记录已存在时,只有这些字段才会被修改。没有在update()中指定的字段将不受影响。
即便是遵守这些规则,仍然有可能定义出来没有意义的查询。查询的有效性,需要开发者自己来掌握,尽量避免定义出没有意义的查询。
“条件语句”是查询的一部分,它通过特定的条件,来限制匹配的记录。在SQL中,它就是SELECT、 UPDATE、或DELETE查询中的WHERE或HAVING部分。在Drupal的所有动态查询中,条件语句的实现机制都相同。如果没有特别说明,下面所讲的适用于所有的查询类型。
条件语句片断
条件语句片断是条件语句自包含的一部分。
连接词
在条件语句中,条件语句片断使用连接词连接。连接词通常为AND或OR,它能够把两个语句连到一块。
条件语句对象
Drupal把所有的条件语句片断都处理成了QueryConditional类的实例。一个条件语句对象就是该类的一个实例。
作为一个例子,下面的查询可以这样分解:
查询:
SELECT FROM {mytable} WHERE (a = 1 AND b = 'foo' OR (c = 'bar'))
条件语句:
WHERE (a = 1 AND b = 'foo' OR (c = 'bar'))
条件语句片断:
(a = 1 AND b = 'foo' OR (c = 'bar'))
(c = 'bar')
连接词:
AND, OR
选择、更新、和删除查询对象都实现了QueryConditionalInterface接口,这样它们在条件语句上的接口是完全相同的。在内部,它们封装了一个QueryConditional对象。也可以直接实例化QueryConditional类。
条件语句中的每一片断都是使用连接词连在一起的。如果一个条件语句包含多个片断,那么片断之间将会使用指定的连接词。默认情况下,连接词为AND。每个条件语句片断本身就是一个带有不同连接词的条件语句对象,这样就允许条件语句中片断之间的相互嵌套。这样,就可以构建任意复杂的条件语句了。
对于所有的条件语句对象,主要使用的方法有两个:
$query->condition($field, $value = NULL, $operator = '=')
condition()方法允许添加一个标准的条件语句片断,其形式为$field $value $operator。它包含了二进制比较的各种情况,比如=、 <,、>=、 LIKE、等等。如果没有指定操作符,则使用默认的=。这意味着,最常见的情况的就是condition('myfield', $value),它生成一个条件语句片断myfield = :value;其中,当查询运行时,:value将被替换为$value。
$query->where($snippet, $args = array())
where()方法允许使用任意的SQL作为条件语句片断。$snippet可以包含任何合法的SQL片断,如果它里面包含了变量内容,那么必须使用命名占位符的形式。$args是占位符数组,它里面的值将会替换到SQL片断中去。开发者需要自己确保SQL的有效性。不要对SQL片断作任何特定于数据库的修改。
在大多数的情况下,推荐使用condition()方法,除非出现$field $value $operator形式不再适用的情况,比如当你需要使用表达式时,或者一个条件作用于两个字段时。两个方法返回的都是对应的条件语句对象,所以它们可被无限的链式调用。
condition()还可以用于一些其它特殊情况。
一些运算符用于value参数为数组时。最常用的就是IN 和BETWEEN。那么$value应该是一个数组,它包含了字段可能等于的值。因此,下面的调用将会这样解析:
<?php
$query->condition('myfield', array(1, 2, 3), 'IN');
//变成了: myfield IN (:db_placeholder_1, :db_placeholder_2, :db_placeholder_3)
?>
如果运算符为BETWEEN,那么$value是一个包含两个元素的数组,字段位于两者之间。例如:
<?php
$query->condition('myfield', array(5, 10), 'BETWEEN');
//变成了: myfield BETWEEN :db_placeholder_1 AND :db_placeholder_2
?>
condition()的第一个参数,也可以是一个条件语句对象。内部的条件语句对象将被纳入到外部的条件语句中去,并放在括号中间。内部对象所使用的连接词,可以与外部不同。这样,就可以通过“自下而上”的方式,来创建条件语句对象,从而就可以构建一个复杂的嵌套的条件语句了。
db_condition()帮助函数将返回一个新的条件语句对象。它只有一个参数,就是对象使用的连接词。一般情况下,帮助方法db_and()、db_or()、和db_xor()就可以涵盖大多数情况了。它允许条件语句以内联的方式插入到查询中,这样代码看起来更加紧凑。
例如,看一下下面的结构体:
<?php
$query
->condition('field1', array(1, 2), 'IN')
->condition(db_or()->condition('field2', 5)->condition('field3', 6))
// 对应结果:
// (field1 IN (:db_placeholder_1, :db_placeholder_2) AND (field2 = :db_placeholder3 OR field3 = :db_placeholder_4))
?>
在有些情况下,我们可能会根据一个字段的值是不是NULL来进行过滤。当然,此时也可以使用condition(),不过我们更推荐使用下面的实用方法,因为它们更易于理解:
<?php
$query->isNull('myfield');
//结果为:(myfield IS NULL)
$query->isNotNull('myfield');
//结果为:(myfield IS NOT NULL)
?>
两个方法都可被链式调用,根据需要可以和condition()、where()结合使用。
定义一个数据库连接的主要方式,是使用settings.php中的$databases数组。从它的名字,我们就可以看出,$databases允许定义多个数据库连接。它还支持定义多个目标。只有在对该数据库运行查询时,才会打开对应的数据库连接,或者说是创建相应的连接对象。
condition()中的$value还可以使用子查询的形式。为了使用子查询,首先需要使用db_select()构建一个SelectQuery对象。接着,我们不执行这个Select查询,而是将其作为condition()的value参数传递给另一个查询。当主查询执行时,它将被自动的集成过来。
子查询通常只在两种情况下使用:当子查询生成的结构只包含一行并且只有一个值时,同时运算符为=、<、>、<=、>=;或者当子查询只返回单列信息,而运算符为IN。除此之外,大多数其它的关联方式都会造成语法错误。
注意,在一些数据库上,特别是MySQL,条件语句中的子查询,运行效率有点慢。如果可能的话,尽量在FROM语句中使用关联、子查询,或者使用多个简单的条件语句片断来代替子查询。
下面有几个例子,希望能够更好的帮助理解条件语句。为了清晰起见,我们在注释中给出等价的查询字符串,当然在实际的应用中,将会使用占位符和预备语句(prepared statements)。
<?php
db_delete('sessions')
->condition('timestamp', REQUEST_TIME - $lifetime, '<')
->execute();
// DELETE FROM {sessions} WHERE (timestamp < 1228713473)
?>
<?php
db_update('sessions')
->fields(array(
'sid' => session_id()
))
->condition('sid', $old_session_id)
->execute();
// UPDATE {sessions} SET sid = 'abcde' WHERE (sid = 'fghij');
?>
<?php
// From taxonomy_term_save():
$or = db_or()->condition('tid1', 5)->condition('tid2', 6);
db_delete('term_relation')->condition($or)->execute();
// DELETE FROM {term_relation} WHERE ((tid1 = 5 OR tid2 = 6))
?>
这部分内容很快就会加上!webchick坚持让我的文档写了多少,发布多少。到目前为止,有关数据库驱动的还没有写好。我将尽快地把它补上来。在此以前,大家可以参考对应的单元测试(http://cvs.drupal.org/viewvc.py/drupal/drupal/modules/simpletest/tests/database_test.test?&view=markup)。
Drupal7使用PDO(PHP数据对象)来访问数据库。更多信息可参看:http://drupal.org/node/549702。
数据库API在遇到错误时,就会抛出异常,我们可以把数据库操作放在try {} catch() {}区块中,这样异常就会被我们抓住,相关信息可参看http://api.drupal.org/api/group/database/7中的最后一个例子。
Drupal还支持事务,对于那些不支持事务的数据库,Drupal还包含了一个透明的回退。然而,当你在同一时间,尝试并启动两个事务时,事务就会变得复杂起来。数据库不同,此时的行为也不相同。
在C/C++里面的嵌套锁中,也存在类似的问题。如果代码已经获得锁A,并尝试去获取锁A时,代码就会进入死循环。你可以在代码中添加检查语句,如果已经获得了锁,那么就不再尝试获取它了,这样就可以避免死循环;但是你也可以提前释放锁。
在SQL中,我们也存在同样的问题。如果你的代码已经处于一个事务中了,那么启动一个新的事务,这会给你带来非常惊讶和不幸的后果。
Java通过支持类似于我们下面测试所用的嵌套结构,成功解决了它的锁嵌套问题。Java允许你把函数标记为“同步”("synchronized"),函数在运行前,首先等待锁的释放;当它获得了锁以后,就会执行;当它不再需要时,就会把锁释放。如果在同一个类中,一个同步函数调用另一个,Java将会追踪锁的嵌套。外部的函数获取锁,内部函数执行无锁操作;当外部函数返回时,释放锁。
尽管我们不能在PHP中把函数声明为“事务的”,但是通过使用带有构造函数和析构函数的对象,我们就可以模拟Java的嵌套逻辑。在一个函数中,在它的第一个操作或者接近于第一个操作的地方,简单的调用"$txn = db_transaction();",就使得函数具有事务特性了。如果一个事务函数调用了另一个,我们的事务抽象层通过在内部嵌套层执行非事务操作(至少在数据库看来是这样),来实现嵌套。
为了启动一个新事务,在你的代码中简单的调用$txn = db_transaction();即可。只要变量$txn仍然在范围内,那么事务就会保持打开的状态。当$txn被销毁时,事务将被提交。如果你的事务嵌套在另一个里面,那么Drupal会追踪每一个事务,只有当最后一个事务对象超出范围,也就是所有相关的查询都成功执行完毕时,Drupal才会提交最外面的事务。
例如:
<?php
function my_transaction_function() {
//在这里打开事务
$txn = db_transaction();
$id = db_insert('example')
->fields(array(
'field1' => 'mystring',
'field2' => 5,
))
->execute();
my_other_function($id);
return $id;
//在这里,$txn超出了范围,整个事务被提交。
}
function my_other_function($id) {
//此处,事务仍然是开着的。
if ($id % 2 == 0) {
db_update('example')
->condition('id', $id)
->fields(array('field2' => 10))
->execute();
}
}
?>
我们已经讲了很多有关数据库API函数的链式调用了,例如:
<?php
$result = db_select('mytable')
->fields('mytable')
->condition('myfield', 'myvalue')
->execute();
?>
然后,并不是所有的函数都可以采用这种链式结构调用,有些函数不能采用链式调用,否则就会导致你的代码不能正常工作。
对于不能使用链式形式的函数,只能按照下面的方式调用:
<?php
$query = db_select('mytable');
$query->addField('mytable', 'myfield', 'myalias');
$query->addField('mytable', 'anotherfield', 'anotheralias');
$result = $query->condition('myfield', 'myvalue')
->execute();
?>
如果一个函数是链式的,那么它的返回值,必须是查询对象本身。为了查看任意数据库API函数的返回值,可以参看http://api.drupal.org。
在这个页面,我们列出了一些常用的链式函数,和一些非链式函数,方便大家快速查看,不过这里面并没有涵盖所有的函数。如果你觉得哪个函数比较常用,也可以将其添加进来。
注意:如果一个函数不是链式的,这只意味着你不可以在它的后面链接更多的函数,但是你仍然可以在它前面使用链式函数。
下面是链式函数:
· fields()
· where()
· isNull()
· having()
· union()
· exists()
· range()
· addTag()
下面是非链式函数:
· join()
· extend()
Drupal的数据库层,没有为SQL函数提供跨数据库的抽象。为了让你的代码更好的兼容所支持的数据库引擎,你使用的函数应该符合ANSI标准,并且在Drupal支持的所有数据库中都能正常工作。下面是一个正在完善的列表。这里所列的,是推荐大家使用的,其它的语法变体可能不垮数据库。
注意,数据库层没有提供运算符的白名单,所以你可以使用非标准的函数,比如REPLACE(),对于支持该语法的数据库,代码是可以正常工作的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
连接键是给定数据库连接的唯一标识符。对于给定站点,连接键必须是唯一的,并且必须有一个"default"连接,用作Drupal的主数据库。对于大多数站点,这可能也是唯一定义的连接。
CONCAT(string1, string2)
SUBSTRING(string, from, length)
SUBSTRING_INDEX(string, delimiter, count)
LENGTH(string)
GREATEST(num1, num2)
POW(num1, num2)
LOG(base, value)
RAND()
COUNT(expression)
SUM(expression)
AVG(expression)
MIN(expression)
MAX(expression)
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习,你应该可以
了解数据库API的一般概念
熟悉数据库的配置
能够熟练的掌握静态查询,
能够熟练的掌握动态查询,插入、更新、删除、合并查询
熟悉条件语句
了解事务机制和链式结构
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个给定的连接键,必须有一个或者多个目标。一个目标就是一个可选的备用数据库。如果请求的目标没有定义,系统就会自动采用“默认”目标,这是必须要定义的。
目标的主要用途就是主从数据库。默认目标是主SQL服务器。接着可以定义一个或多个“从”目标。对于标记为尝试使用从服务器的查询,如果从服务器可用,那么它们就会尝试访问“从”目标。如果有一个从服务器可用,那么就会打开对应的连接,并运行相应的查询。如果没有从服务器可用,那么查询就会运行在主服务器上。这样,就有了一个透明的回退,如果有从服务器可用,代码就会利用从服务器,如果没有从服务器可用,代码无需修改,仍然能够正常工作。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
$databases数组是一个至少包含3层的嵌套数组。第一层定义了数据库的键。第二层定义了数据库目标。每个目标的值就是对应的连接信息。通过实例我们可以更好的理解这一点。
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb',
'username' => 'username',
'password' => 'secret',
'host' => 'localhost',
);
?>
上面的$databases数组定一个单个的连接键("default"),单个的目标("default")。连接采用的数据库为MySQL,数据库名字为“drupaldb”,访问数据库的用户名密码分别为“username”、“secret”,数据库所在的主机为“localhost”。上面的例子代表着Drupal单SQL服务器安装的典型情况,对于大多数站点,这就足够了。
对于主/从服务器的配置,我们则需要这样定义:
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb1',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver1',
);
$databases['default']['slave'][] = array(
'driver' => 'mysql',
'database' => 'drupaldb2',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver2',
);
$databases['default']['slave'][] = array(
'driver' => 'mysql',
'database' => 'drupaldb3',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver3',
);
?>
这个定义提供了一个“主”服务器和两个“从”服务器。注意,“从”服务器的键是一个数组。如果有目标被定义成为连接信息的数组形式,那么对于该目标的每个页面请求,系统会随机的从中选择一个服务器。也就是说,对于一个页面请求,所有的从查询会发送给dbserver2,而对于下一个请求,则可能都会发送给dbserver3。
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb1',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver1',
);
$databases['extra']['default'] = array(
'driver' => 'sqlite',
'database' => 'files/extradb.sqlite',
);
?>
这个配置定义了一个单独的主Drupal数据库,和一个键为"extra"的附加数据库,后者采用SQLite。注意SQLite的连接信息的结构和MySQL的大不相同。对于每个驱动,由于情况不同,所以可以有不同的配置。
注意,无论你定义了多少个连接,Drupal只有在该连接被用到时才会打开它。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先,感谢我的父母及亲人,感谢他们的从小到大的理解和支持。
感谢Eskalate的罗先生和曹先生,没有他们辛勤的市场开拓,就没有eskalate的Drupal团队,也就没有了译者的Drupal技能。
感谢同事liu、dikers、deny、geoge、andy、shark、john、blade,感谢他们在Java和Drupal学习上的帮助与支持,很多Drupal技能是在与同事共同解决客户的问题时学到的。
感谢jcob和jason,他们不辞辛劳的组织北京的Drupal聚会。
感谢外研社网络部雇我担任他们的技术顾问,书中的很多技术都来直接自外研社相关站点的实践。感谢外研社的小白、章林、张超、王海霞。
最后,感谢JOHN VANDYK和Matt Westgate,感谢他们为Drupal编写了《Pro Drupal developemnt》,本书在其第2版的基础上,根据Drupal7的最新变动重新编写而成。当然,也感谢一下Dries Buytaert,没有他就没有Drupal了。