Drupal提供了一个应用程序接口(API),用来生成、验证和处理HTML表单。表单API将表单抽象为一个嵌套数组,里面包含了属性和值。在生成页面时,表单呈现引擎会在适当的时候将数组呈现出来。这种方式包含多层含义:
我们没有直接输出HTML,而是创建了一个数组并让引擎生成HTML。 由于我们将表单的表示作为结构化的数据进行处理,所以我们可以添加、删除、重新排序、和修改表单。当你想用一种干净利索的方式对其它模块创建的表单进行修改时,这会特别方便。 任意的表单元素可以映射到任意的主题函数上。 可以将额外的表单验证或处理函数添加到任意表单上。 对表单操作进行了保护,从而防止表单注入攻击,比如当用户修改了表单并接着试图提交它时。 使用表单的学习曲线有点高.
理解表单处理流程: Drupal的表单引擎负责为要显示的表单生成HTML,并使用三个阶段来安全的处理提交了的表单:验证、提交、重定向。
流程初始化 在处理表单时,有3个变量非常重要。第一个就是$form_id,它包含了一个标识表单的字符串。第二个就是$form,它是一个描述表单的结构化数组。而第三个就是$form_state,它包含了表单的相关信息,比如表单的值以及当表单处理完成时应该发生什么。drupal_get_form()在开始时,首先会初始化$form_state。 设置一个令牌(token) 表单系统的一个优点是,它尽力的去保证被提交的表单就是Drupal实际创建的,这主要是为了安全性和防止垃圾信息或潜在的站点攻击者。为了实现这一点,Drupal为每个Drupal安装都设置了一个私钥。这个私钥是在安装流程期间随机生成的,它能将这个特定的Drupal安装与其它的Drupal 区别开来。一旦私钥生成后,它将作为drupal_private_key存储在variables表中。Drupal将基于私钥生成一个随机的令牌,而该令牌将作为隐藏域发送到表单中。当表单提交时,会对令牌进行测试。相关背景信息请参看drupal.org/node/28420。令牌仅用于登录用户,因为匿名用户的页面通常会被缓存起来,这样它们就没有唯一的令牌了。 设置一个ID 一个包含了当前表单ID的隐藏域,将作为表单的一部分被发送给浏览器。该ID一般对应于定义表单的函数,它将作为drupal_get_form()的第一个参数传递过来。例如函数user_register()定义了用户注册表单,它的调用方式如下: $output = drupal_get_form('user_register');
收集所有可能的表单元素定义: 接着,调用element_info()。它将调用所有实现了hook_elements()的模块上的这个钩子函数。在Drupal核心中,标准的元素,比如单选按钮和复选框,都定义在modules/system/system.module中的hook_elements()实现中(参看 system_elements())。如果模块需要定义它们自己的元素类型,那么就需要实现这个钩子。在以下几种情况中,你可能需要在你的模块中实现 hook_elements():你想要一个特殊类型的表单元素时,比如一个图像上传按钮,在节点预览期间可用来显示缩略图;或者,你想通过定义更多的属性来扩展已有的表单元素时。 钩子element_info()为所有的表单元素收集所有的默认属性,并将其保存到一个本地缓存中。在进入下一步----为表单寻找一个验证器----以前,对于那些在表单定义中尚未出现的任何默认属性,都将在这里被添加进来。
寻找一个验证函数: 通过将表单的属性#validate设置为一个数组,其中函数名为为值,从而为表单分配一个验证函数。在调用验证函数时,后面的数组中的任何数据都将被传递给验证函数。可以使用下面的方式来定义多个验证器: // We want foo_validate() and bar_validate() to be called during form validation. $form['#validate'][] = 'foo_validate'; $form['#validate'][] = 'bar_validate'; 如果表单中没有定义属性#validate,那么接下来就要寻找名为“表单ID”+“_validate”的函数。所以,如果表单ID为user_register,那么表单的#validate属性将被设置为user_register_validate。
寻找一个提交函数: 通过将表单的#submit属性设置为一个数组,其中以函数名为键/值,这里的函数名就是用来处理表单提交的函数的名字,从而为表单分配一个提交函数: // Call my_special_submit_function() on form submission. $form['#submit'][] = 'my_special_submit_function'; // Also call my_second_submit_function(). $form['#submit'][] = 'my_second_submit_function'; 如果表单没有名为#submit的属性,那么接下来就要寻找名为“表单ID”+“_submit”的函数。所以,如果表单ID为 user_register,那么Drupal将把#submit属性设置为它所找到的表单处理器函数;也就是 user_register_submit。
允许模块在表单构建以前修改表单 在构建表单以前,模块有两个可以修改表单的机会。模块可以实现一个名字源于form_id + _alter的函数,或者可以简单的实现hook_form_alter()。任何模块,只要实现了这两个钩子中的任意一个,那么就可以修改表单中的任何东西。对于由第3方模块创建的表单,我们主要可以使用这种方式对其进行修改、覆写、混合。 构建表单 现在表单被传递给了form_builder(),这个函数将对表单树进行递归处理,并为其添加标准的必须值。这个函数还将检查每个元素的#access键,如果该元素的#access为FALSE,那么将拒绝对该表单元素及其子元素的访问。 允许函数在表单构建后修改表单 函数form_builder()每次遇到$form树中的一个新分支时(例如,一个新的字段集或表单元素),它都寻找一个名为#after_build 的可选属性。这是一个可选的数组,里面包含了当前表单元素被构建后会立即调用的函数。当整个表单被构建后,最后将调用可选属性$form[‘#after_build’]中定义的函数。$form和$form_state将作为参数传递给所有的#after_build函数。 Drupal核心中有一个实际例子,那就是在“管理➤站点配置➤文件系统”中,文件系统路径的显示。这里使用了一个#after_build函数(在这里就是system_check_directory()),用来判定目录是否存在或者是否可写,如果不存在或不可写,那么将为该表单元素设置一个错误消息。 检查表单是否已被提交 如果你是按照流程往下走的话,那么你将看到我们现在来到了一个分叉点。如果表单是初次显示的话,那么Drupal将会为其创建HTML。如果表单正被提交的话,那么Drupal将处理在表单中所输入的数据;我们稍后将会讨论这一点(参看后面的“验证表单”一节)。现在,我们将假定表单是初次显示。有一点非常重要,那就是不管表单是初次显示,还是正被提交,在此以前,它所走过的流程是一样的。 为表单查找一个主题函数 如果$form['#theme']已被设置为了一个已有函数,那么Drupal将简单的使用该函数来负责表单的主题化。如果没有设置,那么主题注册表将查找一个对应于这个表单的表单ID的条目。如果存在这样的一个条目,那么就会将表单ID分配给$form['#theme'],在后面,当Drupal呈现表单时,它将基于表单ID来寻找主题函数。例如,如果表单ID为taxonomy_overview_terms,那么Drupal将调用对应的主题函数theme_taxonomy_overview_terms()。当然,可以在自定义主题中,使用主题函数或者模板文件来覆写这个主题函数。 允许模块在表单呈现以前修改表单 最后剩下的一件事,就是将表单从结构化的数据转化为HTML。但是在这以前,模块还有最后一个机会来调整表单。对于跨页面表单向导,或者需要在最后时刻修改表单的其它方式,这将会非常有用。此时将会调用$form['#pre_render']属性定义的任何函数,并将正被呈现的表单传递给这些函数。
呈现表单: 为了将表单树从一个嵌套数组转化为HTML代码,表单构建器调用drupal_render()。这个递归函数将会遍历表单树的每个层次,对于每个层次,它将执行以下动作: 1. 判定是否定义了#children属性(这句话就是说,是否已经为该元素生成内容了);如果没有,那么按照以下步骤来呈现这个树节点的孩子: • 判定是否为这个元素定义了一个#theme函数。 • 如果定义了,那么将这个元素的#type临时设置为markup(标识字体)。接着,将这个元素传递给主题函数,并将该元素重置为原来的样子。 • 如果没有生成内容(可能是因为没有为这个元素定义#theme函数,或者因为调用的#theme函数在主题注册表中不存在,或者因为调用的#theme函数没有返回东西),那么逐个呈现这个元素的子元素(也就是,将子元素传递给drupal_render())。 • 另一方面,如果#theme函数生成了内容,那么将内容存储在这个元素的#children属性中。 2. 如果表单元素本身还没有被呈现出来,那么调用这个元素所属类型的默认主题函数。例如,如果这个元素是表单中的一个文本字段(也就是说,在表单定义中,它的#type属性被设置为了textfield),那么默认主题函数就是theme_textfield()。如果没有为这个元素设置#type属性,那么默认为markup。核心元素(比如文本字段)的默认主题函数位于includes/form.inc中。 3. 如果为这个元素生成了内容,并且在#post_render属性中找到了一个或多个函数名字,那么将分别调用这些函数,并将内容和该元素传递过去。 4. 在内容前面添#prefix,在后面追加#suffix,并将它从函数中返回。 这个递归迭代的作用就是为表单树的每个层次生成HTML。例如,一个表单了包含一个字段集,而字段集里面又包含两个字段,那么该字段集的#children属性将包含两个字段的HTML,而表单的#children属性将包含整个表单的HTML(其中包括字段集的HTML)。 生成的HTML将会返回给drupal_get_form()的调用者。这就是呈现表单所要做的全部工作!我们到达了流程中中的终点“返回HTML”。
验证表单: 我们在“检查表单是否已被提交”一节中所提到的分叉点。现在让我们假定表单已被提交并包含了一些数据;这样我们将沿着另一分支前进,看看这种情况是怎么样的。使用以下两点来判定一个表单已被提交:$_POST不为空,$_POST['form_id']中的字符串匹配刚被构建的表单定义中的ID。如果这两点都满足了,那么Drupal 将开始验证表单。 验证的目的是为了检查证被提交的数据的合理性。验证或者通过,或者失败。如果验证在某一点上失败了,那么将为用户重新显示这个表单,并带有错误消息。如果所有的验证都通过了,那么Drupal将对提交的数据进行实际的处理。 令牌验证 在验证中首先检查的是,该表单是否使用了Drupal的令牌机制。使用令牌的所有Drupal表单,都会有一个唯一的令牌,它和表单一起被发送给浏览器,并且应该和其它表单值一同被提交。如果提交的数据中的令牌与表单构建时设置的令牌不匹配,或者令牌不存在,那么验证将会失败(尽管验证的其余部分也会继续执行,这样其它验证错误也会被标识出来)。 内置验证 接着,检查必填字段,看用户有没有漏填的。检查带有#maxlength属性的字段,确保它没有超过最大字符数。检查带有选项的元素(复选框、单选按钮、下拉选择框),看所选的值是否是位于构建表单时所生成的原始选项列表中。 特定元素的验证 如果为单个表单元素定义了一个#validate属性,那么将会调用这个属性所定义的函数,并将$form_state和$element作为参数传递过去。 验证回调 最后,表单ID和表单值将被传递到表单的验证器函数中(函数名一般为:“表单ID”+ “_validate”)。
提交表单: 如果验证通过了,那么现在就应该把表单和它的值传递到一个函数中,该函数将做些实际的处理,以作为表单提交的结果。实际上,由于#submit属性可以包含一个数组,里面包含多个函数名字,所以可以使用多个函数来处理表单。调用数组中的每个函数,并向其传递参数$form和$form_state。
重定向: 用来处理表单的函数,应该把$form_state['redirect']设置为一个Drupal路径,比如node/1234,这样就可以将用户重定向到这个页面了。如果#submit属性中有多个函数,那么将会使用最后一个函数设置的$form_state['redirect']。如果没有函数把$form_state['redirect']设置为一个Drupal路径,那么用户将返回原来的页面(也就是,$_GET['q']的值)。在最后一个提交函数中返回FALSE,将会阻止重定向 通过在表单中定义#redirect属性,就可以覆写在提交函数中$form_state['redirect']设置的重定向了,比如 $form['#redirect'] = 'node/1'或$form['#redirect'] = array('node/1', $query_string, $named_anchor) 如果使用drupal_goto()中所用的参数术语,那么最后的一个例子将被改写为 $form['#redirect'] = array('node/1', $query, $fragment) 表单重定向的判定,是由includes/form.inc中的drupal_redirect_form()完成的。而实际的重定向则由drupal_goto()实现,它为Web服务器返回一个Location头部。 drupal_goto()的参数与后一个例子中的参数一致:drupal_goto($path = '', $query = NULL, $fragment = NULL)。
创建基本的表单: ....
表单属性: 属性和元素有哪些区别呢?最基本的区别就是,属性没有属性,而元素可以有属性。提交按钮就是一个元素的例子,而提交按钮的#type属性就是一个属性的例子。你一眼便可以认出属性,这是因为属性拥有前缀“#”。我们有时把属性称为键,因为它们拥有一个值,为了得到该值,你必须知道相应的键。一个初学者常见的错误就是忘记了前缀“#”,此时,无论是Drupal还是你自己,都会感到非常困惑。如果你看到了错误消息“Cannot use string offset as an array in form.inc”,那么十有八九就是你忘记了字符“#”。 属性是通用的,而有些则特定于一个元素,比如一个按钮。 例: $form['#method'] = 'post'; $form['#action'] = 'http://example.com/?q=foo/bar'; $form['#attributes'] = array( 'enctype' => 'multipart/form-data', 'target' => 'name_of_target_frame' ); $form['#prefix'] = ' ';
';
$form['#suffix'] = ' #method属性的默认值为post,它可以被忽略。表单API不支持get方法,该方法在Drupal中也不常用,这是因为通过Drupal的菜单路由机制可以很容易的自动解析路径中的参数。#action属性定义在system_elements(),默认值为函数request_uri()的结果。通常与显示表单的URL相同。
表单IDs: drupal_get_form()的调用中,所用的就是ID.对于大多数表单,其ID的命名规则为:模块名字+一个表述该表单做什么的标识。 Drupal使用表单ID来决定表单的验证、提交、主题函数的默认名字.另外,Drupal使用表单ID作为基础来为该特定表单生成一个 |