1. 程式人生 > >雲客Drupal8原始碼分析之表單Form API

雲客Drupal8原始碼分析之表單Form API

在閱讀本主題前建議你先閱讀本系列前面的《表單定義示例》主題,看一看在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"),名字也需要是陣列形式,否則將以最後一個值作為提交值


補充:
1、    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

我是雲客,【雲遊天下,做客四方】,微訊號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137