“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
在阅读本主题前建议你先阅读本系列前面的《表单定义示例》主题,看一看在drupal8中是如何运用表单的。
表单处理流程:
一般情况下表单流程是先显示一个表单,用户填写,然后提交,系统处理,如果有错则重新显示并给出错误提示,反之没有错误那么完成后给出一个响应或者一个重定向响应,这是任何系统的基本流程,drupal也不例外,在drupal中可以这么认为:显示表单和处理提交在系统流程上其实是一样的,只不过后者多了验证和处理提交数据的步骤而已,总体来说是处于同一个流程管道,管道中针对的是表单数组的处理,整个表单组件围绕着以下主要元素展开:
表单数组:$form表示,用于表单的渲染数组,她是核心,整个流程都是在操作她。
表单对象:$formObject表示,由用户定义,告诉系统表单是什么样、怎么验证、怎么处理提交,充当回调。
表单状态对象:$form_state表示,伴随表单的处理流程,记录着关于表单的信息以供系统使用
表单构建器:form_builder表示整个流程的核心执行者,也是表单处理的入口。
在drupal中显示表单和处理表单提交的地址是一样的,提交表单的方法既可以是get也可以是post,那么系统是如何知道当前是在显示表单还是在处理提交呢?取决于以下判断:
如果用户有输入数据(不管是get还是post),且数据中有form_id变量,变量值和当前表单id相同,即认为正在处理提交,以$form_state->isProcessingInput();来标识,如果处于提交处理流程中那么将执行验证和提交处理器,否则只是显示表单。
除了用户可以在浏览器中提交表单外,系统也是可以在程序中直接提交的,这称为以编程方式提交,如果是以编程方式提交表单,那么此时不会有显示表单步骤,总是被认为正在处理提交,以$form_state->isProgrammed()来判断。
表单路由:
访问一个表单,用户可以不用定义控制器,只需要针对表单定义专用路由即可,见《表单定义示例》主题,那么系统是如何路由的呢?是哪个控制器在执行?在路由中指定_form默认项后会经过表单路由增强器的处理(见本系列路由主题),将补充设置控制器“'_controller'”为 controller.form:getContentResult,也就是服务“controller.form”的getContentResult方法,该服务的类为:
Drupal\Core\Controller\HtmlFormController
注意:在路由中设置了_form选项后不能再设置_controller选项,否则系统不会运用表单路由增强器,也就没有后面的逻辑了,而是转而执行路由中_controller设置的控制器
表单代理控制器:
服务id:controller.form
类:Drupal\Core\Controller\HtmlFormController
入口方法:getContentResult
接收以下服务做为参数:
controller_resolver、form_builder、class_resolver
这是一个很简单的控制器,主要功能是在表单构建器中完成的,该控制器只是准备参数而已,同样,在普通的控制器中需要用到表单时也是调用表单构建器,表单构建器是表单系统的核心,见下。
表单状态对象FormState:
类:\Drupal\Core\Form\FormState
接口:\Drupal\Core\Form\FormStateInterface
表单处理时全程记录相关信息,各部分程序也可以用她储存并传递信息,用户提交的数据也保存在这里。
表单对象:
要完成一个表单,系统只需要用户提供四个必要的信息:指定一个识别id,表单内容是什么?怎么验证?提交怎么处理?其他事情就由系统包办了,无微不至的贴心,因此表单对象实现的接口:
\Drupal\Core\Form\FormInterface
只有四个方法,用户只需要提供一个表单对象就能实现表单功能,此四个方法为:
\Drupal\Core\Form\FormInterface::getFormId
返回表单id,系统用该id识别表单,定义主题钩子,通过她派发表单数组修改钩子,这也是系统判别表单是否处于提交状态的关键。
\Drupal\Core\Form\FormInterface::buildForm
定义表单是什么,返回基本的表单渲染数组,在接口中虽然只定义了两个参数,可是我们在实现中可以定义更多参数,在使用中额外定义的参数分以下两种情况传入:
在使用表单路由时,这些参数的值在内部使用控制器解析器(服务id:controller_resolver)去从请求对象中获取,这和普通控制器的参数解析一样,在$request->attributes和$request->attributes->get('_raw_variables')中的参数都能获取,还能得到请求对象、路由匹配器对象,因此在路由定义中附带的参数也是能获取的,因为它们被设置在了请求对象中,这里需要特别注意:因为参数解析是用反射机制获得的,所以这个方法中前两个参数的参数名必须为$form和$form_state否则系统会提示找不到参数而抛出异常。
在使用表单构建器的得到表单时:
\Drupal::formBuilder()->getForm('Drupal\mymodule\Form\ExampleForm')
可以在第一个参数后添加其他参数,他们会被自动传入表单对象的buildForm方法。
我们可以在该方法中直接返回一个响应对象,这在当前版本drupal8.4是允许的,该响应是以异常的方式返回给http核心,通过异常控制流发给用户;虽然异常不应该用于代码流控制,但代码注释也说了这是由于当前表单api和http核心实现上有冲突,不得已而为之。
表单构建器:
服务id:form_builder
类:Drupal\Core\Form\FormBuilder
表单流程的核心控制者,实例化表单对象,从表单对象的构建表单方法中取回表单数组,预备表单数组,为其添加必要的属性和子元素,派发修改钩子,处理表单等等都在这里完成,以下是他的主要方法说明:
\Drupal\Core\Form\FormBuilder::getFormId
实例化表单对象,并将其注入到$form_state,返回表单id
\Drupal\Core\Form\FormBuilder::retrieveForm
构建最初的表单数组,为其添加必要的类属性,调用表单对象的构造表单buildForm方法,产生原始表单数组
\Drupal\Core\Form\FormBuilder::prepareForm
为表单数组$form添加必要的元素,属性如:#action、#method、#build_id、#token、#id、#validate、#submit、#theme,子元素如:form_build_id、form_token、form_id,执行表单数组的修改钩子,默认的钩子名有:'form'、'form_' . $base_form_id、'form_' . $form_id,钩子的函数名有:
hook_form_alter()、hook_form_BASE_FORM_ID_alter()、hook_form_FORM_ID_alter()
假设模块名为yunke,表单id为yunkeId,表单基本id为:yunkeBaseId那么钩子的函数名为:
yunke_form_alter()
yunke_form_yunkeBaseId_alter()
yunke_form_yunkeId_alter()
这些修改钩子函数接收参数:$form, $form_state, $form_id 其中$form应该以引用方式接收
比如:\core\modules\contact\contact.module中的以下函数:
contact_form_user_form_alter(&$form, FormStateInterface $form_state)
注意:表单缓存中就保存的是经过该方法返回的表单,如果表单状态被设置为不可缓存,那么将不会使用缓存系统
\Drupal\Core\Form\FormBuilder::processForm
调用验证器和提交器执行验证和提交
\Drupal\Core\Form\FormBuilder::doBuildForm
在该方法中已经将表单数组当渲染数组看待了,会自我调用以递归处理所有的子元素,表单数组不是只能包含表单元素,她也可以包含其他的html元素的;注意表单缓存系统缓存的是没有经过该方法处理的表单数组(对子元素未经递归,也未执行#processed、#after_build等回调),但是否缓存受到该方法影响;该方法处理如下:
根据元素的#type属性,调用元素信息插件管理器补充默认的属性内容;
为所有元素添加基本的通用的属性;
判别表单是否采用https提交;
将表单数组以引用方式保存到$form_state,这允许其它程序得到整个表单数组
如果输入中存在变量form_id,且等于当前表单id,那么为$form_state设置旗标($form_state->setProcessInput();),这表明系统不是在显示表单,而是在处理提交,后续将执行验证和提交处理;
如果是在处理提交,那么在要求csrfToken时将验证Token;
设置data-drupal-selector和id的属性值
如果存在#description,那么设置aria-describedby属性
如果#input不为空,那么转化元素的输入、设置#value属性、收集按钮元素、确定触发元素,注意:在元素信息插件管理器中每一个可输入表单元素都默认设置了#input属性,其值为true
如果存在#process且还没有执行过,那么执行,用于对元素进行特别修改,传递三个参数:&$element, &$form_state, &$complete_form
判断可访问性,递归处理子元素,传递#tree、#access、#disabled、#allow_focus、#parents、#array_parents、#weight给子元素并赋值,如果父元素禁止访问,那么子元素也就禁止访问了,这就是$inherited_access的作用
执行#after_build回调
判断是否有#type为file的元素,以决定表单编码
设置表单的编码enctype值
处理触发提交的元素,并设置验证和提交处理器,触发元素上的处理器优先于表单上的,一旦设置,后者不会被执行
\Drupal\Core\Form\FormBuilder::handleInputElement
保护方法,如果元素'#input'属性不为空(在系统中所有具备输入能力的表单元素都会默认设置该属性,值为true,见元素信息插件管理器),那么转化元素的输入、设置#value属性、收集按钮元素、确定触发元素等
表单值回调:
表单提交后需要将提交的值转化为要验证的值,注意是值转化,而不是值验证,验证作用在转化后的值上,也就是将$form_state->getUserInput()转化为$form_state->getValues(),转化函数及其优先级是(优先级从高到低):
#value_callback属性指定的回调
'form_type_' . $element['#type'] . '_value'命名的函数
\Drupal\Core\Render\Element\FormElement::valueCallback默认回调
侦查触发元素:
系统需要知道是哪一个元素触发了表单提交,这里有AJAX提交和浏览器点击提交两种:
当通过AJAX提交表单时以_triggering_element_name参数指明触发元素的名字,可选的以_triggering_element_value参数指明触发元素的元素值
除Ajax方式外,表单只可能被点击按钮提交,如果按钮名和按钮值出现在输入中,那么她就是触发元素,这里图片按钮是一种特殊情况,浏览器传递的是点击图片按钮的坐标值,系统通过为图片按钮配置#has_garbage_value属性以识别这种提交,此时提交的值一定不会是空值。
有一种特殊情况,当表单包含一个textfield输入框,此时按下回车键后,Internet Explorer浏览器提交的数据中不包含任何触发提交的元素,其他浏览器以第一个按钮作为触发元素,所以此种情况发生时统一以第一个按钮作为触发元素。
表单数组:
表单数组是用于表单的渲染数组,是渲染数组的一个子集,有些属性只用于特定的表单元素,它们在元素类的注释文档中被解释,有些可用于全部表单元素;可用于全部表单元素的属性如下:
#ajax:指定了Ajax行为的元素数组,更多信息见ajax主题
#array_parents:只读的字符串数组,元素值是在表单渲染数组中从根元素开始到自己的元素名,包括自己,在顶层时该数组为空,全部表单元素都有此属性。也参看 #parents, #tree。
#parents:只读的字符串数组,要理解该属性需要先明白表单提交到系统中的数据可以是一个多维数组,在很多情况下我们只使用了一维数组($_POST是一维的),但她可以是多维的,且这样带来强大的功能,这样能表达表单中值与值之间的关系,在前端通过表单的name属性实现,name属性可以指定多维数组,如:name="price[car][jac][heyue]"这样的name值可以表示汽车品类下江淮品牌和悦系列轿车的价格,明白这个知识点后就不难理解#parents属性了,该属性值受到 #tree属性的影响,#tree表示值是否存在某种上下级关系,是一个布尔值,如前文提到的“汽车、品牌、系列”,如果 #tree为false那么只有一个元素(本元素的元素名),如果为true那么包含父元素的名字,可以这么理解:如果提交值呈现树结构(有上下级关系),那么该属性代表值在树中的路径,用于从 $form_state中获取值,比如值为$value[‘a’][‘b’][‘c’]那么该属性值就是[‘a’,’b’,’c’],在程序中有时候又叫做section,在表单数组的顶层时该数组为空,换句话说她指示如何在多维数组$_POST中找到值,也参看: #array_parents, #tree,该属性与#array_parents的区别是它代表提交值的结构路径,而后者代表表单数组元素的路径,表单数组元素可以是任意元素,可能没有值。
#tree: 布尔值,默认为false,在内部所有的表单元素都会被追加设置,有些表单元素的输入值是有树状关系的,该属性表明是否具备树关系,指示在$form_state中元素和子元素的值是否为分层次的,也见: #parents, #array_parents。
#default_value:默认值,也参考 #value.
#description:在用户界面中的帮助或描述文本,通常#title已经足以描述元素,所以大多数元素不应有这个属性,如果确实需要,那么确保是翻译后的文本,如果不是markup对象,那么会进行XSS安全过滤
#description_display:描述显示位置及方式,如:after,见#title_display
#element_validate:一个回调数组,被调用来验证用户输入,参数有:$element, $form_state, $form。
#field_prefix:字段前缀,显示在输入元素的前面,应该是翻译后的值,如果不是markup对象那么会进行xss过滤
#field_suffix:与前缀相同,显示在元素后面
#input:布尔值,表示元素是否接受输入,所有可接受输入的元素都被默认追加,且值为true。
#process:回调数组,针对元素做特殊处理,在表单构建期间被调用,参数:&$element, &$form_state, &$complete_form,第一个参数为设置该属性的表单数组中的元素,最后一个参数是完整的表单数组。回调需要返回处理后的$element
#processed:布尔值,true表明表单已经被#process回调处理过,如果设置可以阻止回调执行。
#after_build: 一个由回调构成的数组,她们在元素被构建后调用(包括子元素),调用时输入值已经被转换,#process回调已经被执行,参数是:$element, &$form_state。注意与#process不同,第一个参数不是以引用传递,回调需要返回处理后的$element
#after_build_done:布尔值,指示#after_build回调是否已经执行,如果设置可以阻止回调执行
#required:布尔值,表明元素是否为必须输入的。
#states:用于JavaScript的状态信息数组,比如根据另外一个元素的输入情况而定的这个元素是隐藏还是显示,更多见: drupal_process_states()
#title:表单元素的标题,是一个翻译后的字符串。
#title_display:指定#title的位置和显示方式,可能的值有:before(元素的前面,大多数元素的默认值)、after(元素的后面, radio元素的默认值)、invisible (用css隐藏)、attribute(采用弹出提示tooltip)。
#value_callback:值回调,用于转换用户输入到元素的值,参数为:$element, $input, $form_state。如果没有默认用:
'form_type_' . $element['#type'] . '_value',再没有就用:
\Drupal\Core\Render\Element\FormElement::valueCallback
#errors:错误信息
#attributes:属性数组,指定元素的属性,如:
$form['#attributes']['class'][]=”yunke”;
$form['#attributes']['autocomplete'][]=”off”;
$form['#attributes']['data-drupal-selector']=”yunke”;
只能用于特定表单元素的属性如下:
#https:布尔值,只用于表单元素('#type' == 'form'),如果为true,那么将使用https做action
#disabled:布尔值,如果为true,那么能够显示,但处于禁用状态,用户不能输入值
#method:提交表单的方法,仅用于表单元素('#type' == 'form'),默认为post,未设定时也可以在表单状态对象中指定为get($form_state->setMethod("get");),一旦设定就以设定为准。
#build_id:仅用于表单元素('#type' == 'form'),指定缓存id,默认为form-前缀的随机字符串
#token:仅用于表单元素('#type' == 'form'),指定CSRF token,不使用则设置为false,反之设定为true。
#input:仅用于能接受用户输入的表单元素,称为输入元素,在元素信息插件管理器中默认设置了'#input' => TRUE,当系统发现存在该属性时,会进行输入值的转换
#value:仅限于输入表单元素,值是经过回调函数处理过的,也就是$form_state->getValues()而不是$form_state->getUserInput(),此过程在表单构建器的handleInputElement方法中进行
#required_but_empty:表示元素是必填项,但用户没有输入
#limit_validation_errors:用于触发提交的元素,进行错误抑制,有些按钮上面设置了专门的提交处理器,当点击提交后不会执行表单级别的提交处理器,而是她上面设置的专门的提交处理器,如“上一步”、“添加更多”,此时即便常规输入无效也需要执行她的提交处理器,以进行重定向之类的操作,此时就需要错误抑制,仅关注该按钮需要关注的错误,其他错误一概不管;该属性只有在提交元素设置了#submit时才有效,是一个数组值,系统只关心该数组中设置的元素的错误,其他错误都被抑制,该数组的元素值是表单元素的#parents属性数组;在系统中该属性被处理后赋值给:$form_state->limit_validation_errors属性,后者只能是NULL或数组,全等于NULL将记录所有错误,这是她的默认值,如果为数组,那么只有在其中的元素才记录错误,空数组“[]”将不记录任何错误,如果不为空数组那么该数组的元素是表单元素的#parents属性数组,表示她代表的元素将会记录错误,而其他的不会被记录错误
#submit:用于表单数组根元素或者触发表单提交的元素,是一个由一个或多个提交处理器回调构成的数组
#validate:用于表单数组根元素或者触发表单提交的元素,是一个由一个或多个验证处理器回调构成的数组
#executes_submit_callback:用于触发表单提交的元素,布尔值,只有触发元素的该属性为真,才会执行提交处理器,这意味着只有被允许的提交按钮才能提交表单,否则即使提交了也不会被处理。
更多专用属性见元素类型(本系列的元素类型插件管理器)
以编程方式提交表单:
表单通常是由用户从浏览器中提交的,但我们也可以从程序中提交,调用以下代码即可:
\Drupal::formBuilder()->submitForm($form_arg, FormStateInterface &$form_state);
$form_arg为全限定表单类名或表单对象
在调用前我们可以通过以下代码设置需要提交的值:
$form_state->setValues(array $values);
注意不是$form_state->setUserInput(array $values);这将无效
需要传递给表单对象buildForm方法的额外参数可以通过以下代码设置:
$form_state->addBuildInfo('args', $args);
或者直接在submitForm方法中传递
以编程方式提交表单将忽略表单返回的响应
表单缓存:
用于提供表单数组form 和 表单状态对象form state的缓存服务,这是一个用于表单构建器的私有服务:
类:Drupal\Core\Form\FormCache
接口:\Drupal\Core\Form\FormCacheInterface
默认情况下并不进行缓存,内部使用有期限限制的键值储存服务(见本系列键值储存主题),以表单构建id作为缓存id,注意不是表单id,每一次访问表单其表单构建id都会改变,因此缓存只能取回上一次缓存的数据,数据被储存在以下数据库表中:
key_value_expire
该储存器有一个特性:储存时间有一个期限,如果超期那么储存失效,默认为6小时,也就是21600秒,该值可以在配置文件中配置,配置键为:form_cache_expiration,单位为秒
在缓存时也设置了缓存验证码#cache_token,意味着会话中的Csrf Token Seed改变或者登陆状态改变,缓存也将失效。
表单缓存为什么不使用系统的缓存服务呢?那是因为它不涉及缓存标签、上下文,有固定的缓存键,在表单缓存中form_build_id相当于缓存id
注意在表单缓存系统中会加载表单状态对象中指定的文件,也就是$build_info['files']
表单缓存在过期时,获取会自动删除缓存,但如果没有发生获取动作就会一直存留在缓存中,需要运行自动任务以清除它
在请求是不应该产生副作用的请求时(如'GET', 'HEAD'),不能进行缓存
验证处理器:
系统在以下服务中进行表单验证逻辑:
服务id:form_validator
类:Drupal\Core\Form\FormValidator
接口:\Drupal\Core\Form\FormValidatorInterface
在表单提交处理前,系统将执行验证处理器进行表单验证,默认表单只验证一次,如果已经验证过将不再验证,以$form_state->isValidationComplete()作为判别标识,验证的值是$form_state->getValues(),而不是$form_state->getUserInput(),在验证时这两种值的转换已经在元素的#value_callback回调中完成了;验证前先进行CSRF token验证,然后整个验证过程就是对表单数组的遍历过程,采用深度优先遍历算法,叶子节点先处理,同级别节点按照元素的#weight属性排序依次进行,没有#weight或者相同那么按照元素出现在表单中的顺序进行;验证完成后将为元素设置属性#validated为true,她意为已经执行了验证, 我们可以利用这点预设置该属性去阻止验证。
验证是在验证处理器中进行的,其为一个合法的回调函数或方法,针对同一项验证可以设置多个验证处理器,验证处理器分为两大类别:子元素级别和表单级别。
在表单数组的任意子元素上都可以设置'#element_validate'属性来验证针对该子元素的子元素级别的验证器,该属性的值是一个由一个或多个验证器回调构成的数组,验证器回调的参数为&$elements, &$form_state, &$complete_form,由于表单数组是先从子元素再到根元素的顺序遍历验证,所以子元素验证过程将在表单级别的验证之前进行。
当所有子元素级别的验证完成后,将进行表单级别的验证,此验证又分为两种:触发元素(触发表单提交的元素)上设置的验证和表单根元素上设置的验证,前者优先级高于后者;表单级验证处理器和元素级相比参数不一样,她们以引用方式接收完整的表单数组$form和$form_state。
触发元素上设置的验证是在各个触发表单提交的按钮或元素上设置的验证处理器,通过属性#validate指定,其值格式和#element_validate一样,是一个由一个或多个回调验证器组成的数组,这允许很大的灵活性,当用户触发了表单提交,系统首先检查触发元素,看其是否设置了验证处理器,根据不同按钮执行不同验证;一旦设置将以此为准,不再执行表单根元素上设置的验证处理器,如果没有设置则执行表单根元素上设置的验证处理器,后者是在$form['#validate']中定义的验证处理器,内容同上,$form['#validate']数组是最常使用的表单级别的验证处理器,表单系统将会自动追加表单对象的validateForm方法到该数组中,验证时其中每一个回调都会被执行。
以上提到的子元素验证器、触发元素验证器、表单根元素验证器都可以在表单对象的buildForm方法中设置,模块可以在修改钩中设置,注意表单对象的validateForm方法已经被系统自动加入到$form['#validate']中,所以我们不必在设置她。
在所有的元素包括根元素中,如果设置了#needs_validation属性,那么除了执行验证处理器外,还将进行必须的额外验证:长度是否超限制,提供的选项是否在备选值中
可以在提交触发元素上面设置#limit_validation_errors属性以限制验证错误,该属性是一个数组,其中元素是不需要被抑制验证错误的表单元素的#parents属性数组,在这个数组中的元素以外的元素即便验证发生错误,也会被忽略,换句话说系统只在乎该数组中的元素的错误信息,如果为空数组,那么不在乎任何错误,相当于不被验证;在程序中该值被处理后传递给$form_state->limit_validation_errors属性,后者只能是NULL或数组,全等于NULL将记录所有错误,这是她的默认值,如果为数组,那么只有在其中的元素才记录错误,空数组“[]”将不记录任何错误,如果不为空数组那么该数组的元素是表单元素的#parents属性数组,表示她代表的元素将会记录错误,而其他的不会被记录错误。
验证完成后如果有错误将运行表单错误处理器,见上。
验证后获取一个元素的错误是从她的#parents数组中的顶层元素开始依次查找,找到即返回。
表单错误处理器:
容器id:form_error_handler
类:Drupal\Core\Form\FormErrorHandler
接口:\Drupal\Core\Form\FormErrorHandlerInterface
该处理器以容器服务的方式定义,当表单验证发现有错误的时候,为表单数组树中的每一个元素(非以#开始的键名,后称元素,包括表单最外层元素)定义'#children_errors'及'#errors'属性,采用深度优先的递归遍历算法。
只要出现一个错误,换句话说就是$form_state->getErrors()为真值,那么所有的表单元素都会被设定义'#errors'属性,即使该元素没有错误也会被定义,没有错误时为空值,有值时代表元素本身出现的错误,为$form_state->getError($elements)返回的值。
属性#children_errors在所有非叶子节点中被定义,她代表一个节点元素的所有直接子元素及其后代元素中出现的错误(后统称为子元素),如果没有错误,那么为空数组,否则是一个关联数组,键名为以发生错误的子元素的#array_parents的值以“][”连接的字符串,该字符串在表单数组中能唯一标识该子元素,键值为前文描述的'#errors'属性的值,往往是一个Markup对象;
通过为表单元素设置'#children_errors'及'#errors'属性就能在用户界面中准确标识错误了,这有个需要注意的地方,那就是组元素,如:containers、details、fieldgroups、fieldsets,他们在表单数组中可以不按照父子关系呈现,而是在子元素中设置'#group'属性以指定所在的分组,所以在遍历表单时给予了特别处理,让组元素也能获得正确的'#children_errors'属性,这可以让子元素出现错误时,组元素在显示时自动展开。
提交处理器:
系统在以下服务中进行表单提交逻辑:
服务id:form_submitter
类:Drupal\Core\Form\FormSubmitter
接口:\Drupal\Core\Form\FormSubmitterInterface
表单提交处理和验证处理不同的是不会进行表单遍历,和其类似的是可以在提交触发元素上面设置提交处理器,这将允许不同的提交按钮执行不同的提交处理,也可以在表单根元素上面设置提交处理器,两种处理器都是通过#submit属性进行,其值为一个由一个或多个提交处理器回调构成的数组,回调参数为以引用方式传递的$form和$form_state;设置提交处理器可以在表单对象的buildForm方法中进行,模块可以通过钩子设置;如果触发元素设置了提交处理器,那么将不会再使用表单根元素上设置的提交处理器,否则将执行表单根元素上设置的提交处理器,系统会自动将表单对象的submitForm方法设置到表单根元素上,这也是我们最常使用的处理器,不必再次设置。
验证逻辑首先检查表单是否提交,用$form_state->isSubmitted()判别,她表示一个表单是否已经提交了,而不是指是否已经提交并处理了,处理完成后会设置$form_state->setExecuted();,所以$form_state->isExecuted()才表示已经处理了。
在提交处理器中可以通过$form_state->setResponse(Response $response)设置一个响应对象,表示表单处理结束后返回该响应,如果有多个处理器,那么以最后一个的设置为准,这里需要注意该响应在控制器中以异常方式抛出,整个系统进入异常处理流程,在\Symfony\Component\HttpKernel\HttpKernel中最终会向客户端返回该响应。
处理器也可以设置一个重定向,方法为:
$form_state->setRedirect($route_name, array $route_parameters = [], array $options = [])
或者:
$form_state->setRedirectUrl(Url $url)
同样如果有多个处理器,那么以最后一个的设置为准,如果响应和重定向同时设置,那么以响应为准,如果二者都没有设置那么将以303状态的响应重定向到当前连接,这种重定向响应也是通过核心的异常处理流程返回给浏览器的。
批处理接口:
见相关主题
浏览器表单提交行为:
在火狐、chrome、Edge、IE中测试有如下结果:
被标记为禁用的输入元素(disabled="disabled")不会被浏览器提交;
有多个提交按钮时,只有被点击触发的提交按钮值被提交;
没有选择任何值的select、radio、checkbox不会被提交;
重置按钮不会被提交;
按钮类型的input不会被提交,它也不会触发表单提交行为,除非有js协助;
(以上不会被提交的意思是$_POST中无此键名)
text类型的input即使没有输入已会被提交,此时值为全等于‘’的string类型值,也就是一个空字符串值。
在表单没有值时很多浏览器会使用自动完成(将以前用过的值自动填充上去),各浏览器的默认行为不一样(chrome倾向于不填写,其他倾向于填写),这可能无意中导致错误,如无特别需要推荐设置autocomplete="off" 以避免这个问题。
表单提交元素的name值可以是数组形式,不止是一维数组,可以是任意多维的数组,该特性允许将多个表单输入元素(不要求是同类型元素)的值合并提交到一个$_POST键名中,如下列子:
<input name="choose['a']" type="checkbox" value="选择" autocomplete="off" />
<select name="choose['b'][]" size="3" multiple="multiple" autocomplete="off" >
<option value="1">选择1</option>
<option value="2">选择2</option>
<option value="3">选择3</option>
</select>
提交后将类似如下:
[choose] => Array
(
['a'] => 选择
['b'] => Array
(
[0] => 2
[1] => 3
)
)
这也就是系统为什么设置#parents的原因,在后端我们不需要知道前端元素的结构,只需要知道值的结构即可。这里需要注意:
复选框的名字需要是数组形式,否则将以最后一个值作为提交值
select元素允许多选时(具备multiple="multiple"),名字也需要是数组形式,否则将以最后一个值作为提交值
补充:
- bug1:在\Drupal::formBuilder()->buildForm方法中参数不应该采用引用传递,因为对象默认以引用传递,这会导致以下代码产生错误提示:
\Drupal::formBuilder()->buildForm(new a(), new b());
bug2:在表单缓存服务中传递了page_cache_request_policy服务做构造参数,但未被使用
2、\Drupal\Core\Form\FormBuilder::getForm只能接受表单对象或者其全限定类名,虽然类解析器可以接受服务名,但目前不能将表单对象定义为服务然后传递服务名。
3、提交表单的方法是get时,默认不运用CSRF token,这是因为get请求不应该产生副作用,所以没有验证的必要,但如有特殊情况可以在表单数组中指定#token以强制验证(在表单对象的buildForm方法中指定$form['#token'] = true;),系统默认只有认证人户的表单才会进行CSRF验证
4、在url产生器中“<current>”可以表示当前请求所在的路由
5、设置各处理器回调时,如果是表单对象里面的方法,那么以“::”开始加方法名即可
6、如果在uri中出现\Drupal\Core\Form\FormBuilderInterface::AJAX_FORM_REQUEST参数(该常量的值为'ajax_form'),说明请求是一个Ajax请求,
7、默认在表单构建器中事件派发器没有被使用
8、form_id和base_form_id的区别:
form_id用于唯一识别表单,用在主题钩子、表单修改钩子等等地方,base_form_id作为附加的id,可以添加额外的主题钩子和修改钩子
9、form_build_id和form_id的区别:
form_id:唯一标识一个表单
form_build_id:唯一标识一个用户输入的表单,每个用户都不一样,用作缓存id
10、当前表单api的实现中,提交处理后的响应和重定向是通过http核心的异常处理流进行的,然而异常控制不应该用于正常的逻辑流控制,所以将来该行为可能会重新实现
11、表单提交的W3C官方介绍:
https://www.w3.org/TR/2017/REC-html52-20171214/sec-forms.html#forms-form-submission
反馈互动