“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
在某些语境下控件等同于表单中的输入标签,如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、控件对象并不执行表单验证,仅转化值类型、值提取、错误标记、表单构造
反馈互动