雲客Drupal源碼分析之字段控件FieldWidget

   在某些語境下控件等同於表單中的輸入標籤,如input、select、textarea等,如在談論前端設計時,而本篇所指字段控件是程序上的控件對象,用於爲字段類型產生輸入表單的渲染數組,並負責在表單處理流程中提取輸入值、標記驗證錯誤等,解決字段類型的用戶輸入問題,本篇講解drupal控件的實現。
   字段控件是以插件方式提供,在其釋文定義中用field_types指定她可以被哪些字段類型使用,每個字段類型的插件定義中應該以default_widget指定默認使用的控件,管理員可以在管理表單顯示頁面爲字段指定其他可使用的控件

字段控件插件管理器:
服務id:plugin.manager.field.widget
類:\Drupal\Core\Field\WidgetPluginManager
獲取方式:\Drupal::service('plugin.manager.field.widget')
插件定義緩存位置:緩存表cache_discovery 的field_widget_types_plugins條目
插件儲存目錄:Plugin/Field/FieldWidget
字段控件插件定義修改鉤子名:field_widget_info
釋文類:\Drupal\Core\Field\Annotation\FieldWidget
控件接口:Drupal\Core\Field\WidgetInterface
控件插件管理器方法說明:
public function getInstance(array $options)
public function createInstance($plugin_id, array $configuration = [])

見下文:控件插件的實例化

public function prepareConfiguration($field_type, array $configuration)
參數$field_type爲字段類型id,參數$configuration來自字段定義中的以下方法:
$field_definition->getDisplayOptions(‘form’);
合併顯示選項中的子健settings與控件的默認設置選項,字段定義優先,其中無效多餘的settings將去除

public function getOptions($field_type = NULL)
返回字段類型可用的控件,參數爲字段類型插件id,返回值是一個數組,鍵名爲字段類型可用的控件插件id,鍵值爲控件label,如果沒有傳遞字段類型參數,將返回所有字段類型的該數組,此時第一級鍵名爲字段類型插件id

public function getDefaultSettings($type)
參數爲控件插件id,獲取某個字段控件的默認配置選項,也就是調用控件的靜態方法:defaultSettings()

控件插件的實例化:
在渲染內容實體表單時,有顯示選項且允許顯示的字段,其表單渲染數組的產生、值提取等在實體表單顯示對象中進行,在該對象內部通過字段的表單顯示配置確定一個控件類型,並通過控件插件管理器實例化控件對象,最終由控件對象來完成相應工作,詳見以下方法:
    \Drupal\Core\Entity\Entity\EntityFormDisplay::getRenderer
該方法實例化過程相當於在執行以下代碼:
    \Drupal::service('plugin.manager.field.widget') ->getInstance($options);
參數$options是一個數組,有如下鍵名:
field_definition:字段定義對象
form_mode:原始請求的表單模式originalMode
prepare:布爾值,是否需要合併控件默認值,默認爲FALSE,因爲表單顯示對象中儲存的配置是合併了控件默認配置的
configuration:字段對應的表單顯示配置,儲存在表單顯示配置的字段鍵名下(詳見本系列實體表單顯示主題),這裏假設表單顯示對象爲$entityFormDisplay,字段名爲$field_name(字符串值),可通過以下代碼查看:
$entityFormDisplay->getComponent($field_name)

實例化爲什麼沒有直接使用createInstance方法呢?那是因爲如果配置錯誤(如控件不支持字段類型、不適用等),將回退使用字段的默認控件,最終還是會統一調用插件管理器的創建實例方法:
    \Drupal::service('plugin.manager.field.widget')->createInstance($plugin_id, $configuration)
其中$configuration來自前文提到的字段表單顯示配置,是一個數組,有以下鍵:
type:要使用的控件類型id,如省略、配置錯誤或不適用將使用字段類型定義中指定的默認控件
weight:字段顯示排序的權重
region:顯示字段所在的區域通常爲content
settings:用於控件本身的設置,來自字段定義顯示選項和控件默認值,可由控件提供的配置表單設置
third_party_settings:第三方模塊附加的設置,以模塊名做第一級鍵名
此外被附加了鍵名field_definition其值爲字段定義對象,請熟知該數組的結構,對於插件而言:
如果實現了容器工廠接口那麼將用插件類自身的創建方法實例化,並傳入如下參數:
    $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
反之則直接實例化,傳入如下參數:
    $plugin_class($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
後三個參數來自$configuration的相應鍵名

控件對象實例化後在內容實體表單流程中的執行入口爲:
構建表單:
$form[$name] = $widget->form($items, $form, $form_state);
提取表單值:
$widget->extractFormValues($items, $form, $form_state);
標記驗證錯誤:
$widget->flagErrors($items, $field_violations, $form, $form_state);
以上方法中$items爲內容實體的字段對象屬性:
$items = $entity->get($name);
參數$form爲構建中的完整的內容實體表單渲染數組

控件插件定義:
和drupal其他插件定義一樣,詳見本系列插件篇,這裏對字段控件的釋文說明如下:
id:控件ID
label:控件人類可讀標籤
description:控件人類可讀描述
field_types:控件支持的字段類型,數組值,元素值爲字段類型id
weight:控件默認權重,用於與其他控件在一起時的排序
multiple_values:布爾值,單個字段條目控件能否一次性處理多個值,默認爲false,如爲true則需要指定

這裏需要對multiple_values詳細解釋一下:
其值爲true的控件稱爲多值控件,這裏的多值是什麼意思呢?我們知道一個字段對象是一個列表類型的數據對象,在將其傳給控件對象渲染時是通過以下方法:
    $widget->form($items, $form, $form_state);
在該方法內部會對列表中的每一個條目調用以下方法進行渲染:
    $widget->formElement($items, $delta, $element, $form, $form_state);
在大多數情況下,字段對象中有多少個條目(換句話說就是該字段對象有多少個值)就會調用formElement方法多少次,每次調用只渲染一個條目,然後將每次調用的結果組合爲一個數組,該數組就是這個字段的控件渲染結果,但是有一種控件只需要調用一次formElement方法就能渲染字段對象中所有的條目,這樣的控件就是多值控件,其插件釋文中的multiple_values屬性就應該標識爲true,多值控件也用於處理字段對象不可能會有多值的字段類型,如布爾字段類型(雖然字段對象依然是列表數據類型,但只會有一個條目),在默認提供的控件中有以下控件是多值的,控件id及類名如下:
boolean_checkbox:
Drupal\Core\Field\Plugin\Field\FieldWidget\BooleanCheckboxWidget
entity_reference_autocomplete_tags:
Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteTagsWidget
options_buttons:
Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsButtonsWidget
options_select:
Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget

注意不要將多值控件和字段的多屬性混淆,多屬性字段是指字段對象中單個條目就有多個屬性,這些屬性會在一次formElement方法調用中渲染;通常多屬性字段不會使用多值控件,這裏假設一個字段類型由A、B兩個屬性組成,A是單值的 B是多值的 那麼控件依然應該使用單值控件,因爲一個字段對應一個控件,而不是一個屬性對應一個控件
簡而言之:如果單次調用formElement()的結果能處理字段對象中多個條目的提交,則控件就是多值的

在字段的儲存定義中有一個方法getCardinality()用於返回字段對象可以輸入多少個值,如果是單值控件該值將限制formElement方法調用的次數,如果是多值控件,則對控件渲染沒有影響,僅調用一次,但在表單驗證時會限制提交值的個數

字段輸入控件類需要實現控件接口:Drupal\Core\Field\WidgetInterface
該接口繼承了控件基接口:\Drupal\Core\Field\WidgetBaseInterface
爲何要定義成兩個接口呢?drupal將通常來說會直接繼承使用的方法規劃在控件基接口中,而將通常需要被覆寫的方法定義在控件接口中。


默認基類:
系統提供了以下字段控件的默認實現:
\Drupal\Core\Field\WidgetBase
她實現了以上的接口,控件類通常需要繼承該基類,這裏結合接口對方法說明如下:
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL);
爲一個字段對象創建完整表單元素,參數$items來自$entity->get($name);,如果是新建的實體會填充默認值,參數$get_delta指定只返回多值字段(不是多屬性字段)中的某個下標值的表單元素,注意創建的表單元素是返回而不是直接修改傳入的表單數組

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state);
字段對象是一個列表型數據對象,可以有一個或多個元素,她們代表字段的多個值(注意這裏不是指字段的多屬性),每一個元素都需要一個控件表單,在渲染字段對象的完整表單時,如果控件對象是單值控件對象,則會按下標依次爲每個值調用一次該方法,以返回每個值的控件表單(字段條目表單渲染數組),如果是多值控件對象,則該方法僅被調用一次,她返回代表整個字段對象的控件表單;參數$element是依據字段配置產生的基本表單元素,包含了基本信息,該方法應該在此基礎上繼續構建

public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state);
從提交的表單值中提取字段值到字段對象,第一個參數因爲是對象,所以默認被以引用方式修改

public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state);
標記字段級別的所有驗證錯誤,實體表單有兩個級別的驗證錯誤:實體級別和字段級別,該方法僅報告字段級錯誤,且如果在實體級驗證中已經報告了該字段的錯誤,則直接返回,不再報告,參數$items是字段對象,來自$entity->get($name);,參數$violations是Symfony原生的約束違列列表對象:
    \Symfony\Component\Validator\ConstraintViolationList
其中的約束違列對象的屬性路徑($violation->getPropertyPath();)被去掉了字段名前綴,如:
    “title.0.value” 將變爲“0.value”
關於錯誤標記和實體表單驗證更多信息可參見本系列實體表單驗證上下集

public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state);
在標記字段對象的每個條目錯誤時,該方法進一步返回正確的條目表單子元素,錯誤消息將標記到返回的子元素上,參數$element是由formElement方法返回的內容,該方法允許依據控件內部結構進一步精確標記到更加下一級的控件子元素上,如字段條目對象的屬性對象對應的表單控件上,如果返回false將忽略驗證錯誤;如果字段對象只有一個屬性,通常原樣返回。

public static function getWidgetState(array $parents, $field_name, FormStateInterface $form_state);
public static function setWidgetState(array $parents, $field_name, FormStateInterface $form_state, array $field_state);

設置和取回控件狀態信息,狀態信息輔助其他功能的執行,如值提取、錯誤標記,這是一個數組,包含以下兩個鍵名:
items_count:爲顯示字段對象所需要的控件數量(前端頁面可以動態添加)
array_parents:數組值,表示控件在表單結構中的位置,注意是數組位置,而不是值位置
可能還會有original_deltas,其表示原始的下標值(在過濾空條目後,下標值會變)
參數$parents是控件的值位置,也就是#parents

public function settingsForm(array $form, FormStateInterface $form_state);
返回控件自身的配置表單,在管理表單顯示頁面使用,以供管理員配置控件,見:
\Drupal\field_ui\Form\EntityDisplayFormBase
系統會將提交的值存放到表單顯示配置中字段組件的settings鍵下,該數據也是控件的構造參數之一,注意該表單不要設置#parents、#tree屬性以免影響值提取,更多詳細可參見本系列實體表單顯示主題

public function settingsSummary();
用於在管理表單顯示頁面的字段行中顯示控件當前的設置信息,返回一個字符串數組,每個元素代表一條當前控件配置的摘要信息,模塊可以通過field_widget_settings_summary修改鉤去修改返回值

public function massageFormValues(array $values, array $form, FormStateInterface $form_state);
將提交的表單值轉化爲字段對象要求的數據類型,以便字段對象的$items->setValue($values)方法直接使用,參數$values是表單提交的整個字段的值,而不是某個條目,該方法在值提取過程中被調用

public static function isApplicable(FieldDefinitionInterface $field_definition);
返回控件能否用於被提供的字段,雖然在控件插件釋文中已經通過field_types表明了她支持的字段類型,但由於字段是可配置的,該方法通過字段定義對象進一步精確判斷是否可用,返回布爾值

public static function defaultSettings()
返回控件的默認設置,注意這是一個靜態方法,在產生控件設置數據時將合併該數據

 

控件鉤子:
針對字段表單中字段對象的單個值的控件修改鉤子:
   field_widget_form
   field_widget_控件插件id_form
有三個參數,順序如下:
$element:單個值控件的表單渲染數組,爲了修改應該以引用接收,有如下基本鍵名:
#title、#description、#field_parents(字段所在表單中的值位置)、#required(布爾值是否必填)、#delta(值下標)、#weight;以及控件相關鍵名
$form_state:整個表單的表單狀態對象,可以在其中得到表單顯示對象以判斷實體類型和bundle
$context:包含更多信息的上下文數組,鍵名及解釋如下:
   form:正在構建的整個表單數組
   widget:控件對象
   items:字段對象,可以從中得到字段定義和實體等
   delta:所在字段對象中的條目下標值
   default:布爾值,狀態對象中是否設置了默認值控件

針對整個字段的控件表單的修改鉤子:
   field_widget_multivalue_form
   field_widget_multivalue_控件插件id_form
在字段對象所有條目的控件渲染數組都構造完成後,會形成一個索引數組(如果是多值控件則不一定是索引數組),該數組代表整個字段對象的表單,此時會派發這兩個修改鉤子,參數按順序如下:
$elements:整個字段對象的表單渲染數組,爲了修改應該以引用接收
$form_state:整個表單的表單狀態對象,可以在其中得到表單顯示對象以判斷實體類型和bundle
$context:包含更多信息的上下文數組,鍵名及解釋如下:
   form:正在構建的整個表單數組
   widget:控件對象
   items:字段對象,可以從中得到字段定義和實體等
   default:布爾值,狀態對象中是否設置了默認值控件
經過修改鉤子修改後的字段表單會被包裝到一個容器渲染數組('#type' => 'container')中返回,該容器指定了和字段類型、字段名、控件類型相關的類名:
      field--type-字段類型插件id
      field--name-字段名
      field--widget-控件插件id
這樣做是爲了方便前端定位元素進行主題控制


補充:
1、多值字段如果定義中要求必須有值,則在控件中只有第一個值控件被要求必填
2、多值字段的表單中,每個值控件會默認追加權重控件,該控件只用於排序,並不被保存
3、控件對象並不執行表單驗證,僅轉化值類型、值提取、錯誤標記、表單構造

 

我是雲客,【雲遊天下,做客四方】,聯繫方式見主頁,歡迎轉載,但須註明出處

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章