41. 配置系统Configuration(三):配置schema与类型化
前言:在阅读本主题前,请务必先阅读本系列的类型化数据API主题并深入掌握它,本主题涉及的内容是建立在其上的,那是必须的前备知识,否则难以理解。
数据类型:
大千世界那么丰富,事物的种类不计其数,却源自于一百多种元素,而这些元素又源自几种基本粒子,随着人类科技进步,进入更加微观的世界,也许会发现我们这个世界所有的事物万物归宗源于同一种东西,这种奇妙的现象也发生在数字世界里,那就是数据的类型,基础层面仅有为数不多的几种基本数据类型,然后由它们形成许许多多各样的类型(默认安装下有五百多种数据类型,模块还可以继续定义),有了类型就知道数据的特征(或者叫模式),很多事情就比较好处理了,比如自动数据验证、翻译等等。drupal进行了详细的类型化实现,系统中有以下数据类型:
基本类型:
php语言提供了基本的数据类型,drupal在此基础上进一步实现了系统所需的基本数据类型,类型化数据组件封装了这些基本类型,以对象的方式提供使用,基本类型有:boolean、email、integer、float、string、uri、mapping、sequence,他们都以oop对象的方式存在,由类型化组件提供的类型类实例化而来。
简单扩展类型:
对基本类型的一种简单扩展,以指明语义或可翻译性等等
复合扩展类型:
以前面提到的类型组合而成的各类复合类型,为数不多的简单基本类型根据数量、种类形成了数量庞大的各种类型,以mapping、sequence方式进行组合,如同元素构成分子,分子再构成丰富多彩的世界一般
除以上三大类的类型外还有两种特殊类型:
Undefined:表示未定义类型,没有被指定类型的数据默认分配该类型;
Ignore:表示当不能应用数据类型的时候以该类型明确表示
在drupal系统中对数据类型的描述是放在Schema文件中的,类型描述了数据结构,属于数据的元信息,由类型化数据组件负责实现数据的类型化应用,Schema可翻译为模式、格式、样板、成规、计划、图解、概要,这里为叙述方便,翻译为“模式”,模式文件以yml格式储存,存放在模块和核心的config\schema目录中,并以schema作为文件名后缀(不是扩展名)。在路径:core\config\schema中是系统核心提供的数据类型,那包括了基本类型,定义基本类型后可以用它组合出其他类型,一旦定义了一个类型,那么就可以用该类型继续组合派生出其他更大更复杂的数据类型。
在配置系统中,每一个配置对象都可以将其定义为一种数据类型,如果没有定义则将视为Undefined类型,接下来看看如何定义配置类型,即建立配置Schema文件。
schema定义:
模式Schema文件以yml格式储存在模块的config\schema目录中,按照最佳实践,命名方式为:
模块名+分类名+“.schema.yml”,如core.entity.schema.yml
一个模式文件中可以定义多个配置对象的类型,当不需要分类名时可以省略,如:contact.schema.yml
注意:schema后缀不是强制的,只是一种最佳实践,但.yml扩展是必须的,系统以在模块的config\schema目录中扫描yml类型文件的方式来查找schema文件,因此不要在该目录下放置内容非schema的yml文件。
在模块的config\install和config\optional文件夹中一个配置文件就是一个配置对象,不带扩展名的文件名就是配置对象名,同时也是该配置对象的类型名,多个配置对象的类型定义可以放置在一个schema文件中进行,以文件名(类型名)作为yml格式的根键名,该键名的值是一个数组,该数组就是这个配置对象的类型定义,类型定义有如下这些键:
Type:配置对象中值的类型,既可以是基本类型也可以是派生类型,详细见后
Label:值描述,它不一定就是配置表单的label
Translatable:表明该类型是否可翻译,值为true或不设置,不设置默认为不可翻译
Nullable:表明值是否可以为null,如不设置,默认不能为NULL
Class:用于封装该类型的类型类全限定类名,见类型化数据API
definition_class:该类型定义类的全限定类名,见类型化数据API
translation context:翻译上下文,可选的,见本系列的翻译主题
mapping:如果类型被指定为type: mapping,才可能有该子键,其值为映射数组
sequence:如果类型被指定为mapping或sequence时,该键的值为序列数组(储存相同类型的映射)
type属性的值稍微复杂,是一个字符串值,如果未定义type属性,那么其值为该类型自身的类型名,通常需要被设置,值为系统中存在的定义类型(和位置无关,在同一模式文件中可放在本类型定义前也可以在后,也可以存在于不同模式文件中),系统获取类型定义时将以此为依据进行定义的递归合并,使用如下合并方法:
NestedArray::mergeDeepArray([$merge, $definition], TRUE);
数组合并见本系列数组操作主题,$definition为当前定义,$merge表示type属性指定的类型定义,如果有同名键则当前定义覆盖$merge定义,所以当前定义是优先的,合并操作一直递归到基本类型(基本类型没有type属性),递归合并后type属性值为自身,合并示例如下:
string:
label: 'String'
class: '\Drupal\Core\TypedData\Plugin\DataType\StringData'
label:
type: string
label: 'Label'
translatable: true
在获取label类型的定义时会得到:
label:
type: label
label: 'Label'
translatable: true
class: '\Drupal\Core\TypedData\Plugin\DataType\StringData'
definition_class: '\Drupal\Core\TypedData\DataDefinition'
其中如果合并后没有definition_class项,会被追加默认值,如上所示。
有时候某些类型定义的type值依赖于父对象的类型或配置数据中的某配置值,也就是说type的取值不是静态的,而是需要运行时根据依赖进行计算,这称为动态类型,动态类型是在type属性值中使用特定的变量名,用到的变量名以方括号包括,可以有多个方括号段,在单个方括号中如果有多个变量名可以用点号分隔,方括号中不可以有变量以为的内容,如:type: '[%parent.%parent.%type].third_party.[%key]'
变量名有两大类:
配置数据中的某一配置键名:如[%parent.field_type]中的field_type,表示配置数组中上一级数组中的field_type键的值
特殊引用变量:
%parent引用到数组中的上一层级
%type必须和%parent联用,表示某一层级数组的类型,注意在中括号里%type和type是不一样的两种变量类型
%key本类型数据在父层数组中的键名,在映射类型类中也就是$parent->getName()
前文提到如果指定了Type属性的值,那么会找到它进行定义合并,如果找不到对应的类型定义,将进行回退查找,依据值的层级进行,如下:
如果查找的类型为:breakpoint.breakpoint.module.toolbar.narrow,在找不到的情况下,依次尝试一下值:
breakpoint.breakpoint.module.toolbar.*
breakpoint.breakpoint.module.*.*
breakpoint.breakpoint.module.*
breakpoint.breakpoint.*.*.*
breakpoint.breakpoint.*
breakpoint.*.*.*.*
breakpoint.*
类型名是有规则的,以点号分层级,也可以使用冒号,如:
查找block.settings.system_menu_block:footer在找不到的情况下,依次尝试一下值:
block.settings.system_menu_block:*
block.settings.*:*
block.settings.*
block.*.*:*
block.*
由此也可以看出定义类型名的时候,可以用星号做类型的通配符,这样可以定义一大类的配置类型,回退算法见:
Drupal\Core\Config\TypedConfigManager:: getFallbackName($name)
Schema文件书写的最佳实践:
有一个好的代码样式便于团队协作,在书写Schema文件时约定遵循以下原则:
1:在Schema文件的开始包含一个注释,说明该文件的一些信息
2:在定义中使用含义清晰的注释,有时候label太简短,可以额外添加注释说明
3:字符串使用单号,不要用双引号
4:label值用单引号包括,即便只有一个单词也这么做
5:数组键和type值不要使用引号,且不能有空格
6:如果整型想被转化为字符串,那么使用单引号包含
7:注意行缩进,避免yaml格式错误
更多可以查看补充说明中的代码标准
当定义好模式文件后,系统是如何查找它们的呢,这个就是schema配置储存服务要完成的工作。
Schema配置储存服务:
用以找到系统中被启用的模块、主题、安装配置的配置schema文件
容器id:config.storage.schema
类:Drupal\Core\Config\ExtensionInstallStorage
以在启用的模块、主题、安装配置中config\schema目录扫描yml文件的方式查找配置schema文件。
默认中文标准安装情况下有如下schema文件(通过schema配置储存服务的getAllFolders()方法返回,该方法只返回被启用模块的schema文件,键名为文件名,值为文件路径):
Array
(
[core.data_types.schema] => core/config/schema
[core.entity.schema] => core/config/schema
[core.extension.schema] => core/config/schema
[core.menu.schema] => core/config/schema
[automated_cron.schema] => core/modules/automated_cron/config/schema
[block.schema] => core/modules/block/config/schema
[block_content.schema] => core/modules/block_content/config/schema
[ckeditor.schema] => core/modules/ckeditor/config/schema
[color.schema] => core/modules/color/config/schema
[comment.schema] => core/modules/comment/config/schema
[comment.views.schema] => core/modules/comment/config/schema
[contact.schema] => core/modules/contact/config/schema
[contact.views.schema] => core/modules/contact/config/schema
[contextual.views.schema] => core/modules/contextual/config/schema
[datetime.schema] => core/modules/datetime/config/schema
[dblog.schema] => core/modules/dblog/config/schema
[dblog.views.schema] => core/modules/dblog/config/schema
[editor.schema] => core/modules/editor/config/schema
[field.schema] => core/modules/field/config/schema
[field.views.schema] => core/modules/field/config/schema
[field_ui.schema] => core/modules/field_ui/config/schema
[file.schema] => core/modules/file/config/schema
[file.views.schema] => core/modules/file/config/schema
[filter.schema] => core/modules/filter/config/schema
[history.views.schema] => core/modules/history/config/schema
[image.data_types.schema] => core/modules/image/config/schema
[image.schema] => core/modules/image/config/schema
[language.schema] => core/modules/language/config/schema
[link.schema] => core/modules/link/config/schema
[locale.schema] => core/modules/locale/config/schema
[menu_ui.schema] => core/modules/menu_ui/config/schema
[node.schema] => core/modules/node/config/schema
[node.views.schema] => core/modules/node/config/schema
[options.schema] => core/modules/options/config/schema
[path.schema] => core/modules/path/config/schema
[rdf.data_types.schema] => core/modules/rdf/config/schema
[rdf.schema] => core/modules/rdf/config/schema
[search.schema] => core/modules/search/config/schema
[search.views.schema] => core/modules/search/config/schema
[shortcut.schema] => core/modules/shortcut/config/schema
[system.schema] => core/modules/system/config/schema
[taxonomy.schema] => core/modules/taxonomy/config/schema
[taxonomy.views.schema] => core/modules/taxonomy/config/schema
[text.schema] => core/modules/text/config/schema
[tour.schema] => core/modules/tour/config/schema
[update.schema] => core/modules/update/config/schema
[user.schema] => core/modules/user/config/schema
[user.views.schema] => core/modules/user/config/schema
[views.access.schema] => core/modules/views/config/schema
[views.area.schema] => core/modules/views/config/schema
[views.argument.schema] => core/modules/views/config/schema
[views.argument_default.schema] => core/modules/views/config/schema
[views.argument_validator.schema] => core/modules/views/config/schema
[views.cache.schema] => core/modules/views/config/schema
[views.data_types.schema] => core/modules/views/config/schema
[views.display.schema] => core/modules/views/config/schema
[views.entity_reference.schema] => core/modules/views/config/schema
[views.exposed_form.schema] => core/modules/views/config/schema
[views.field.schema] => core/modules/views/config/schema
[views.filter.schema] => core/modules/views/config/schema
[views.pager.schema] => core/modules/views/config/schema
[views.query.schema] => core/modules/views/config/schema
[views.relationship.schema] => core/modules/views/config/schema
[views.row.schema] => core/modules/views/config/schema
[views.schema] => core/modules/views/config/schema
[views.sort.schema] => core/modules/views/config/schema
[views.style.schema] => core/modules/views/config/schema
[bartik.schema] => core/themes/bartik/config/schema
[seven.schema] => core/themes/seven/config/schema
)
系统在读取这些schema文件时,会使用yaml序列化器,默认使用组件提供的序列化器:
Drupal\Component\Serialization\Yaml
但在站点配置文件中也可以另外指定yaml序列化器:
键名为:yaml_parser_class
如:$settings[' yaml_parser_class ']=”全限定类名”;
该功能在核心提供的Drupal\Core\Serialization\Yaml中实现:$class = Settings::get('yaml_parser_class')
配置对象保存:
在配置对象Drupal\Core\Config\Config调用自己的save方法进行保存时,会进行数据验证和类型转化,过程如下:
首先验证配置名,必须有点号、不超过250字符,不含?: * < > " ' / \使用如下方法
public static function validateName($name)
有配置schema的情况下,根据配置schema转化配置值的类型,使用如下方法:
protected function castValue($key, $value)
没有配置schema则进行基本的配置值验证,值仅可以为null、标量或数组,使用如下方法:
protected function validateValue($key, $value)
之前在设置配置值时,配置数组的键名也会被验证,不能含有点号,使用如下方法:
protected function validateKeys(array $data)
以上过程的castValue方法是将配置数组中的标量数据或NULL依据配置schema中的设定转化为对应的php基本数据类型,也就是执行了Drupal\Core\TypedData\PrimitiveInterface中的getCastedValue()方法,这样配置信息数组经过类型校正后再序列化保存到数据库config表中,该表中保存的配置信息数组仅包含标量数据类型。
可是系统是如何把配置对象和类型化数据组件结合起来应用的呢?这就是类型化配置管理器的工作。
类型化配置管理器:
根据配置schema定义将配置对象中的配置数组转化为类型化数据对象,这样就可以以类型化数据对象来操作配置数据了,该服务定义如下:
config.typed:
class: Drupal\Core\Config\TypedConfigManager
arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler']
tags:
- { name: plugin_manager_cache_clear }
在实现上,该服务实际是一个插件管理器(见本系列的插件主题篇),继承自类型化数据管理器,将配置schema定义当做插件看待,每一个Schema定义都是一个插件类型,类型名就是插件ID,每一个Schema文件都通过插件机制找到,schema定义就是插件定义,和常规插件管理器不同的是他并不以释文作为插件发现机制,而是以Schema配置储存服务作为发现机制,定义的发现类为:
Drupal\Core\Config\Schema\ConfigSchemaDiscovery
该插件管理器中$this->getDefinitions()取得的插件定义数据为一个数组,键名为配置类型名,值为对应的schema定义数组,缓存在数据库表cache_discovery中cid为typed_config_definitions条目的data字段里,下载后使用下列程序查看:
$a=file_get_contents("cache_discovery-typed_config_definitions.bin");
$a=unserialize($a);
print_r(array_keys($a));
print_r($a);
在系统中文默认安装情况下,定义了五百多种配置对象数据类型,也可以使用如下方式查看:
查看没有经过模块钩子处理的定义,也就是从模式文件中读取的定义,在控制器中执行:
$discovery = new \Drupal\Core\Config\Schema\ConfigSchemaDiscovery(\Drupal::service("config.storage.schema"));
print_r($discovery->getDefinitions());
exit();
查看经过模块钩子处理过的定义,在控制器中执行:
$c=\Drupal::service("config.typed");
$a=$c->getDefinitions();
print_r(array_keys($a));
print_r($a);
exit();
public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE)
通过类型名(也就是插件id)得到递归合并后的类型定义数组,类型名已经过回退处理,但数组中涉及的动态类型中的变量尚未进行替换,如果类型不存在,则返回undefined的定义
public function buildDataDefinition(array $definition, $value, $name = NULL, $parent = NULL)
依据getDefinition方法返回的结果,创建类型化数据组件需要的数据定义类,有此定义类对象就可以创建类型类数据对象了,因为类型定义中可以含有动态类型,其值可能依赖于配置数据,所以他需要将配置数据一并传入。
public function get($name)
直接根据配置对象名返回类型化数据对象,该方法的实现和配置对象中得到schemaWrapper属性的方法一样,所不同的是配置对象中的schemaWrapper属性将包含实时配置数据。
注意:在默认情况下(从容器中获取服务的情况下)类型化配置管理器没有注入约束管理器,它只是根据schema信息转化配置类型,如有需求可以手动注入。
该管理器会执行config_schema_info钩子以允许模块修改类型schema定义数据,也就是执行模块定义的hook_config_schema_info_alter()钩子函数,该操作不允许添加或者移除schema定义项,但可以修改
补充说明:
一、drupal的配置schema 受到了Kwalify 项目的启发,该项目网址:http://www.kuwata-lab.com/kwalify/,yaml和json对于结构化数据来说很简单,人类可读性和可理解性很好,但和xml相比缺乏DTD这样的东西,没有模式,因此Kwalify 项目出现了,为解决该问题而生,它是一个yaml和json格式的解析、schema验证、数据绑定工具。
二、配置schema官网文档不但给出了解释还给出了一些工具,地址为https://www.drupal.org/node/1905070
三、配置文件代码标准,官方地址:
https://www.drupal.org/docs/develop/coding-standards/configuration-file-coding-standards
本篇暂时就讲到这里,下一篇将继续讲解配置系统中类型化的运用