41. 配置系统Configuration(三):配置schema与类型化

前言:在阅读本主题前,请务必先阅读本系列的类型化数据API主题并深入掌握它,本主题涉及的内容是建立在其上的,那是必须的前备知识,否则难以理解。

数据类型:

大千世界那么丰富,事物的种类不计其数,却源自于一百多种元素,而这些元素又源自几种基本粒子,随着人类科技进步,进入更加微观的世界,也许会发现我们这个世界所有的事物万物归宗源于同一种东西,这种奇妙的现象也发生在数字世界里,那就是数据的类型,基础层面仅有为数不多的几种基本数据类型,然后由它们形成许许多多各样的类型(默认安装下有五百多种数据类型,模块还可以继续定义),有了类型就知道数据的特征(或者叫模式),很多事情就比较好处理了,比如自动数据验证、翻译等等。drupal进行了详细的类型化实现,系统中有以下数据类型:

基本类型:

php语言提供了基本的数据类型,drupal在此基础上进一步实现了系统所需的基本数据类型,类型化数据组件封装了这些基本类型,以对象的方式提供使用,基本类型有:booleanemailintegerfloatstringurimappingsequence,他们都以oop对象的方式存在,由类型化组件提供的类型类实例化而来。

简单扩展类型:

对基本类型的一种简单扩展,以指明语义或可翻译性等等

复合扩展类型:

以前面提到的类型组合而成的各类复合类型,为数不多的简单基本类型根据数量、种类形成了数量庞大的各种类型,以mappingsequence方式进行组合,如同元素构成分子,分子再构成丰富多彩的世界一般

除以上三大类的类型外还有两种特殊类型:

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文件,因此不要在该目录下放置内容非schemayml文件。

在模块的config\installconfig\optional文件夹中一个配置文件就是一个配置对象,不带扩展名的文件名就是配置对象名,同时也是该配置对象的类型名,多个配置对象的类型定义可以放置在一个schema文件中进行,以文件名(类型名)作为yml格式的根键名,该键名的值是一个数组,该数组就是这个配置对象的类型定义,类型定义有如下这些键:

Type:配置对象中值的类型,既可以是基本类型也可以是派生类型,详细见后

Label:值描述,它不一定就是配置表单的label

Translatable:表明该类型是否可翻译,值为true或不设置,不设置默认为不可翻译

Nullable:表明值是否可以为null,如不设置,默认不能为NULL

Class:用于封装该类型的类型类全限定类名,见类型化数据API

definition_class:该类型定义类的全限定类名,见类型化数据API

translation context:翻译上下文,可选的,见本系列的翻译主题

mapping:如果类型被指定为type: mapping,才可能有该子键,其值为映射数组

sequence:如果类型被指定为mappingsequence时,该键的值为序列数组(储存相同类型的映射)

 

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联用,表示某一层级数组的类型,注意在中括号里%typetype是不一样的两种变量类型

%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:字符串使用单号,不要用双引号

4label值用单引号包括,即便只有一个单词也这么做

5:数组键和type值不要使用引号,且不能有空格

6:如果整型想被转化为字符串,那么使用单引号包含

7:注意行缩进,避免yaml格式错误

更多可以查看补充说明中的代码标准

当定义好模式文件后,系统是如何查找它们的呢,这个就是schema配置储存服务要完成的工作。

 

Schema配置储存服务:

用以找到系统中被启用的模块、主题、安装配置的配置schema文件

容器idconfig.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_discoverycidtyped_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/yamljson对于结构化数据来说很简单,人类可读性和可理解性很好,但和xml相比缺乏DTD这样的东西,没有模式,因此Kwalify 项目出现了,为解决该问题而生,它是一个yamljson格式的解析、schema验证、数据绑定工具。
  • 配置schema官网文档不但给出了解释还给出了一些工具,地址为https://www.drupal.org/node/1905070
  • 配置文件代码标准,官方地址:

https://www.drupal.org/docs/develop/coding-standards/configuration-file-coding-standards

 

本篇暂时就讲到这里,下一篇将继续讲解配置系统中类型化的运用

 

添加新评论

受限制的 HTML

  • 允许的HTML标签:<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • 自动断行和分段。
  • 网页和电子邮件地址自动转换为链接。
请输入以上问题的答案,换一个问题请刷新,不区分大小写