148. 配置同步(导入、导出)
配置同步概述:
配置同步是指在同一个Drupal实例的不同副本间进行配置数据的导出和导入操作,以使各副本系统达到配置相同的状态。一个Drupal实例即是指我们用Drupal安装搭建的一个系统,副本是指通过克隆(或站点迁移)生成的一个完全相同的系统,一个实例可以有很多副本,这些副本不存在主和从,或原和副之间的区别,它们互为副本,这些副本属于同一个Drupal实例,注意这里所讲的副本并非指共用同一个数据库的多个Drupal服务器,这属于负载均衡方面的事情,安装实例副本通常用于建立生产站点和开发站点,而配置同步则用于维持两者间配置数据的一致;“不同副本”中的“不同”也可指副本在时间上的不同,从而配置同步也可用于同一个副本上配置数据的版本化管理。
同一个实例的所有副本间有一些信息是共享的(相同的),典型的有配置实体的UUID、站点配置文件中的哈希盐,配置同步仅在同一个实例下的不同时间或空间上的副本间进行配置数据同步。
表面看配置同步就是系统配置数据的转存,但实际上并不那么简单,配置数据改变除会引起系统行为改变外,可能还需要执行额外代码,比如新建内容类型时可选择的自动附加字段等,另外系统中模块的启禁用状态也是通过配置数据来保存的(保存在配置对象core.extension中),如果两个副本间启禁用的模块不相同,那么它们的配置对象core.extension就不同,在导入时该如何处理呢?实际上遇到这种情况需要先进行模块的安装或卸载,以使两副本系统间安装的模块一致,完成后再进行配置同步,有些配置可能依赖在内容上,副本间内容可能不同,同步后还需要进行这方面的处理。
在进行详细讲解前,我们先看一看配置导出、导入的管理入口部分。
全部配置导出:
导出页地址:/admin/config/development/configuration/full/export
路由名:config.export_full
控制器采用表单:\Drupal\config\Form\ConfigExportForm
当点击导出后实际上被跳转到以下路由了:
config.export_download
控制器如下:
\Drupal\config\Controller\ConfigController::downloadExport
导出过程如下:
调用“配置导出专用配置储存器”(该储存器的原理和作用见下文)将所有的配置导出添加到一个压缩包中,然后调用系统模块提供的文件下载控制器以提供下载,在这个控制器中将派发“file_download”钩子,配置模块在该钩子中进行了权限检查和文件命名;详见:
\Drupal\system\FileDownloadController::download
function config_file_download($uri)
单一配置导出:
导出页地址:/admin/config/development/configuration/single/export
路由名:config.export_single
控制器采用表单:\Drupal\config\Form\ConfigSingleExportForm
单一配置导出通常用于查看配置。这和全部配置导出不同,首先它不提供下载,而是在页面上以yaml格式直接显示配置,其次导出显示的配置数据就是活动配置中的数据,没有经过模块导出修改。
配置导入限制:
配置导入涉及配置对象的新建、更新、删除、重命名四个方面,在讲述配置导入前,我们先看一看在系统规划层面导入有哪些限制:
1、简单配置均可导入,配置实体能否进行导入,取决于其实体储存处理器是否实现了以下接口:
\Drupal\Core\Config\Entity\ImportableEntityStorageInterface
如果没有实现那么将不能被导入,如果实现了则导入时调用其对应方法来处理
2、简单配置不允许有重命名操作,这会导致导入验证不通过,而配置实体可以,这是因为配置实体默认有UUID属性,系统可以据此判断是否为同一个实体在进行改名操作。
3、同步配置实体不允许改变实体类型
更多限制将在下文提到。
单个配置导入:
导入页地址:/admin/config/development/configuration/single/import
路由:config.import_single
控制器采用表单:\Drupal\config\Form\ConfigSingleImportForm
这是一个由单个表单实现的多步表单,实现了再确认功能,关于多步表单详见本系列《多步表单与表单重建》主题,这里不再多述;导入过程和完整配置导入一样均采用配置导入器在批处理中进行,配置导入器详见下文,批处理请参考本系列《批处理API》主题。
在导入时需提供合法的yaml格式数据,如导入的配置已经存在且相同,那么没有导入的必要,因此不会做任何处理,另外:
当导入的是配置实体时:
导入配置数据中的实体id和自定义实体id必须二选一提供,如果均有提供那么以自定义实体id为准,如果导入的配置实体已经存在,那么导入配置中必须存在uuid且需要和已存在的配置实体相同,如果配置实体尚不存在,那么uuid可选提供,但不能提供一个已经存在的uuid。
当导入的是简单配置时:
必须提供配置名,即配置对象名(配置文件不带“.yml”扩展名后缀),此时自定义实体id被忽略
额外验证:
不管是什么配置都会在配置导入器层面进行额外验证,验证以下事情:
1、简单配置不允许有重命名操作;
2、从命名操作不允许导致实体类型改变
在这里模块可以参与验证过程,详见配置导入器一节。
完整配置导入:
导入页地址:/admin/config/development/configuration/full/import
路由名:config.import_full
控制器采用表单:\Drupal\config\Form\ConfigImportForm
该页面的作用仅是上传配置文件压缩包并解压到配置同步目录(上传的压缩包通常来自副本实例的完整配置导出),然后跳转到配置同步操作页面
配置同步操作页:
上传完整配置压缩包后会自动跳转到同步操作页面,该页面地址为:
/admin/config/development/configuration
路由名:config.sync
控制器采用表单:\Drupal\config\Form\ConfigSync
该页面会比较同步目录中的配置(准确说是经过模块导入修改后的配置,见下)和活动配置之间的差异,并允许用户进行配置导入,除此外系统还很贴心的显示出最后一次导入后系统活动配置的变化情况(这利用了配置快照功能,见下)。
在理解配置同步的细节前,我们先看一看需要用到的一些组件。
配置储存器:
配置储存器用于支持配置对象(包含配置实体)的底层配置数据的CRUD操作,配置同步即是用此读写配置数据,依据储存位置,系统中有两大类储存器:数据库型和文件型,都实现了相同的接口:
\Drupal\Core\Config\StorageInterface
系统提供了多个储存器服务,这里做一个简介:
活动配置储存器:
活动(active)配置也就是在系统中正生效的配置,默认情况下是储存在数据库中的(这可被改变),该储存器也是我们使用的主要配置储存器,其被服务“config.storage”包装使用,定义如下:
服务id:config.storage.active
类:Drupal\Core\Config\DatabaseStorage
该储存器读取的便是活动配置,读写将影响站点的行为
同步目录专用配置储存器:
是一个文件类型的储存器,在导入完整配置时会用到,服务定义如下:
服务id:config.storage.staging
服务别名:config.storage.sync
类:Drupal\Core\Config\FileStorage
该服务由以下工厂实例化:
\Drupal\Core\Config\FileStorageFactory::getSync
用于操作同步配置目录中的配置文件,同步目录的位置在站点配置文件的“$settings['config_sync_directory']”中指定,如果没有设置那么默认使用“$config_directories['sync']”的值,该变量会在站点安装时初始化,其值类似如下:
'sites/default/files/config_LrSduAqLq52nfAsive-wJy146xfopHbhzM717UY9a0qj58IxEdWDmkeKbY4eILC5ERrFrMILiw/sync';
服务id:config.storage.export
类:Drupal\Core\Config\ManagedStorage
由于代码涉及到弃用更新,所以这里对该储存器仅讲原理。
配置导出为什么不直接使用活动配置储存器呢?这是因为Drupal提供了一种能力:在导出前给模块一个修改配置数据的机会;如果直接使用活动配置那么这种修改将直接生效,而不是仅作用于导出的数据。为了达到这个目的专门设计了这个导出储存器,在导出前,她会将活动配置中的所有配置完整的复制到暂存储存器中,然后将暂存储存器提供给模块修改,这个暂存储存器使用数据库表“config_export”储存即将被导出的配置数据,模块可以订阅以下事件获得修改机会:
事件常量:ConfigEvents::STORAGE_TRANSFORM_EXPORT
常量值:“config.transform.export”
模块将从以下事件对象中获得暂存储存器进行修改:
\Drupal\Core\Config\StorageTransformEvent::getStorage
通常如果一个模块在导出时做了修改,那么在导入时也需要修改,与之对应的,系统也提供了导入专用配置存储器(详见下文的配置导入转化器)
另外该导出储存器也实现了为导出动作申请锁,这样可以保证在同一时刻仅有一个请求在执行导出
数据替换储存器:
这不以服务方式存在,而是一个工具类:
\Drupal\config\StorageReplaceDataWrapper
用于包装一个配置储存器,在被包装的储存器之上提供配置替换数据,可以通过其replaceData方法提供替换用的配置数据,一旦有提供,那么该储存器的CURD操作均以替换数据优先,这在单个配置导入中被用到。
快照配置储存器:
服务id:config.storage.snapshot
类:Drupal\Core\Config\DatabaseStorage
数据库类型储存器,使用数据库表“config_snapshot”,用于在每一次配置导入(不管是整体导入还是单条导入)操作后为所有活动配置生成一个快照,该快照用于追踪最后一次导入操作后活动配置发生的变化,这些变化将显示在配置同步页中
配置schema储存器:
采用和配置储存器相同的方式去读取系统中所有已安装的模块、主题和核心中提供的配置schema文件。
服务id:config.storage.schema
类:\Drupal\Core\Config\ExtensionInstallStorage
实现接口:\Drupal\Core\Config\StorageInterface
实际上该类并不止用于该服务:
首先她不仅可以读取配置schema文件目录,还可以读取默认配置和可选配置目录,具体读取哪种目录类型取决于传递给构造函数的第二个参数
其次她读取的并不是某一个扩展的某种目录类型,而是系统中所有已经安装的模块、主题和核心的某目录类型,即相同目录类型的多个目录,因此其构造函数的第一个参数需要传递活动配置储存,通过活动配置才能知道哪些扩展是已经被安装的。
因此该类在配置安装器等地方也被直接实例化去读取所有已安装模块、主题、核心的默认和可选配置。
该类继承自以下安装储存器类:
\Drupal\Core\Config\InstallStorage
不同的是该父类读取的扩展包含了没有被安装的扩展,即父类并不区分扩展是否已被安装。
以上安装储存继承自文件储存,但仅用于读取目录中的文件,不提供写入支持。
读者可能会问:如果有两个或多个扩展提供了同名yml文件,那么读取谁呢?这取决于扫描顺序,以最后一个为准,但如果包含了安装配置扩展,那么其总是优先。
服务id:config.import_transformer
类:Drupal\Core\Config\ImportStorageTransformer
前文讲到了配置导出专用配置储存器,在配置导出时有一个暂存阶段,允许模块修改导出的配置,与之对应的,在导入时也有一个暂存阶段供模块修改导入的配置,导入采用导入专用配置储存器,配置导入转化器即是用于将操作同步目录的同步配置储存器转化为导入专用配置储存器,该储存器使用数据库表“config_import”来存储即将导入的配置数据,模块对导入配置的修改结果即保存在其中,如果模块需要修改导入配置,那么需要订阅以下事件:
事件常量:\Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_IMPORT
常量值:config.transform.import
从事件对象的getStorage()方法可得到导入专用配置储存器,在该储存器上进行修改操作。
储存比较器:
这不是一个服务,而是一个工具类。
类:\Drupal\Core\Config\StorageComparer
接口:\Drupal\Core\Config\StorageComparerInterface
用于比较两个配置储存器中所储存的配置数据差异,并持有配置储存器,为配置导入操作提供支持,构造函数的第一个参数应为将导入的源储存(保存外部将被导入的配置),第二个参数应为当前系统的活动配置;
该工具类的关键方法如下:
public function createChangelist()
该方法将计算两个储存器在创建、更新、删除、重命名这4个方面的变化,调用该方法后就能得到配置改变列表,系统依据改变列表进行导入操作,改变列表在内部表示为一个数组:
[
'create' => [],
'update' => [],
'delete' => [],
'rename' => [],
];
每个元素的值数组元素为配置对象名(rename除外,其值数组元素为“$old_name . '::' . $new_name”),他们已经依据配置依赖排序,比如“delete”的值中,叶子节点靠前,遍历时先被处理,这就保证了叶子节点先被删除
配置导入器:
这不是一个服务,而是一个工具类。
类:\Drupal\Core\Config\ConfigImporter
该类负责执行具体的导入逻辑,导入默认进行以下四项事务(模块可以定义更多事务):
同步扩展状态:
也就是让副本间的模块或主题有相同的安装和卸载状态,如果状态不同,那么将在目标副本上进行扩展的安装或卸载操作
导入配置:
配置数据导致,简单配置将直接导入,而配置实体则调用配置实体储存处理器来处理,不支持配置导入的配置实体不能导入(支持须实现可导入配置储存处理器接口),将给出错误提示
处理配置内容依赖丢失:
部分配置可能依赖于某些内容,而副本间内容可能不相同,从而导致依赖于内容的配置出现隐患,系统将派发事件让配置所属模块自行处理
同步后处理:
取消锁,派发事件,创建配置快照等
方法解释如下:
public function import()
配置同步通常在批处理流程中执行,但也可以调用该方法在非批处理环境中执行
public function alreadyImporting()
通过锁来判断导入是否已经开始了,避免启动多个导入流程
public function validate()
对同步操作进行验证,验证内容有:
1、简单配置不允许有重命名操作;
2、从命名操作不允许导致实体类型改变
模块可以参与导入验证,须订阅以下事件进行自定义验证:
\Drupal\Core\Config\ConfigEvents::IMPORT_VALIDATE
模块的验证函数可以从事件对象中取得配置导入器对象,通过导入器对象可以取得源和目标配置储存器,如果有验证错误可以直接调用配置导入器的logError($message)方法进行错误设置
protected function createExtensionChangelist()
初始化扩展改变列表,即属性:$this->extensionChangelist,换句话说该方法判断配置导入过程中,有哪些模块或主题需要安装或卸载,注意:在进行两个副本间同步时,需要安装的模块如果没有被上传到模块目录,那么不会得到安装,这不会提示错误,由于配置的复杂性,强烈建议副本间的模块目录有相同模块文件。
public function initialize()
验证配置导入,并产生配置导入批处理的过程,相当于设置批处理回调,返回一个步骤数组,其中每个元素指示一个批处理操作回调,元素值:
如果是字符串且配置导入器对象存在该方法,那么回调即是该方法
如果是一个合法的回调,那么直接采用
模块可以实现修改钩子“config_import_steps”添加自定义步骤,钩子的第一个参数是步骤数组(应以引用接收进行修改),第二个参数是配置导入器对象;在该方法可以看到前文所述的几个配置同步默认事项。
protected function processExtensions(&$context)
批量安装或卸载扩展(如果配置同步不会引起扩展状态改变那么不会被执行),顺序是先模块后主题,先安装后卸载,即先把模块的安装卸载处理完再处理主题的安装和卸载;当$context['finished']小于1时该方法会循环调用,详见本系列批处理主题
protected function processExtension($type, $op, $name)
执行某个模块或主题的安装或卸载,$type指示扩展的类型(值为module或theme),$op指示操作类型(值为install或uninstall),$name为扩展机器名;注意该方法不会自动安装或卸载模块的依赖,仅考虑模块本身,因为被导入的副本配置应已考虑了依赖关系,这里仅是同步而已,这就导致了一个注意事项:由于采用了这种处理方式,如果某个模块在安装或卸载期间导致整个同步工作中断,那么系统可能会发生依赖错误,这种错误需要视具体情况去排查,因此我们在同步时,务必将流程走完。
protected function setProcessedExtension($type, $op, $name)
记录已经安装或卸载的模块或主题
protected function processConfigurations(&$context)
批量进行配置导入,导入顺序是先默认集再其他集,逐次处理,在每个集中按删除、创建、重命名、更新的顺序处理;在该方法中重置配置储存比较器是必须的,因为可能有模块的安装与卸载动作,重置后才能获取新的改变列表;
protected function checkOp($collection, $op, $name)
返回布尔值,检查在某个配置对象上的某种同步操作还是否有必要:
当重命名时,目标储存中已经存在了命名后的结果,那么只需要进行更新即可;
当删除时,目标中已经没有了,就不必进行;
当创建时,如目标中已经存在了,那么需要删除后重建
当更新时,目标中却没有,那么不必更新
protected function importInvokeOwner($collection, $op, $name)
处理配置实体的导入,仅配置实体的储存处理器实现了可导入配置实体储存处理器接口才能导入,导入逻辑由接口对应方法处理
protected function importInvokeRename($collection, $rename_name)
配置重命名导入,注意:仅配置实体才能进行重命名,且配置实体的储存处理器需要实现可导入配置实体储存处理器接口,导入逻辑由接口对应方法处理;
默认实现见方法:\Drupal\Core\Config\Entity\ConfigEntityStorage::importRename
protected function importConfig($collection, $op, $name)
处理所有的简单配置导入,以及非默认集上的配置实体导入
protected function processMissingContent(&$context)
该方法在配置导入完成后执行,处理配置的内容依赖丢失的情况:
如果某个配置依赖于某个或某些内容(在依赖声明中“content”项不为空),那么负责该配置的模块必须要订阅以下事件:
事件名:\Drupal\Core\Config\ConfigEvents::IMPORT_MISSING_CONTENT
事件值:config.importer.missing_content
事件对象为:\Drupal\Core\Config\Importer\MissingContentEvent
可通过事件对象得到丢失的内容,其是以下方法的返回值:
\Drupal\Core\Config\ConfigManager::findMissingContentDependencies
在订阅器中必须进行以下处理之一:
删除配置对象、消除依赖关系、建立内容实体
这些处理的目的是让依赖冲突消失,处理完后必须调用事件对象的resolveMissingContent方法通知系统依赖冲突已经解决;为了配置同步能顺利完成,系统默认提供了以下订阅器服务
“config.importer_subscriber”。
该订阅器进行兜底,会无条件通知系统所有的依赖都已解决。
注:不进行以上实质性的冲突处理而直接调用事件对象的resolveMissingContent方法也不会报错,但这种欺骗处理会给系统埋下隐患
protected function finish(&$context)
在配置同步最后阶段调用,派发以下事件(配置快照即是在该事件中生成):
\Drupal\Core\Config\ConfigEvents::IMPORT
取消跨请求锁,重置导入器
差异格式化器:
服务id:diff.formatter
类:Drupal\Core\Diff\DiffFormatter
用于格式化差异对象以对比显示其不同,能够像GIT一样对比两个文档发生的变化,这用在配置同步页面中显示配置发生的变化
用法示例:\Drupal\config\Controller\ConfigController::diff
差异对象:Drupal\Component\Diff\Diff
差异对象的产生:\Drupal\Core\Config\ConfigManager::diff
这是一个很棒很通用的工具,在许多PHP项目中会用到,有兴趣的读者可以深入研究
补充:
1.在drupal中生成压缩文件可以执行以下代码:
$archiver = new \Drupal\Core\Archiver\ArchiveTar('yunke.tar.gz', 'gz');
$archiver->addString("a.yml", 'aaaa');
$archiver->addString("b.yml", 'bbbb');
die;
如需上传压缩包解压等可参考:\Drupal\config\Form\ConfigImportForm::submitForm
2、配置导入过程使用了跨请求的持久锁,这种锁在请求结束时不会自动释放,如果导入过程出现故障,尚没有来得及释放锁,那么在锁有效期内(默认30秒)执行下一次导入会被提示“另一个请求已经在运行导入”,这种情况下需要手动释放锁;如果同步时间太长,超过30秒,则可能导致锁保护失效。
3、配置只能在同一个drupal实例的副本间同步,通常是生产站点和开发站点间,这会检查配置对象“system.site”中的uuid是否相同
4、没有UUID的配置在导入时不存在重命名,只能是删除再新建,准确理解是即便符合导入要求的配置实体,如果没有UUID那么也是不能重命名导入的,但需注意即便简单配置有UUID也不能有重命名导入。
5、在配置实体基类(\Drupal\Core\Config\Entity\ConfigEntityBase)中,属性$isSyncing用于标识当前是否处于配置同步过程中,其有重要用途,比如节点类型配置实体在新建后,会自动添加body字段,显然导入配置时不再需要这个添加动作,因此必须用该旗标指明当前是否处于同步中,以便决定是否执行某些动作
6、配置同步官网文档地址:https://www.drupal.org/documentation/administer/config
7、由于配置可以衍生出非常复杂的情况,有些问题系统可能没有考虑到,因此强烈建议在生产站点进行配置同步前做数据库备份,以便出现未知问题时回滚。