91. 块系统block
在drupal中系统流程指向一个控制器,通常控制器返回一个代表特定内容的渲染数组,那么还需要其他内容怎么办?这就是块系统要解决的,她让页面精彩纷呈,可展示多种信息或工具,如果没有她页面会非常单调,某种程度上说她是系统必须的,给各模块展示信息提供页面窗口。
从控制器返回的渲染数组说起:
一个渲染数组可以代表页面中的一部分,也可以是整个页面,在drupal中大多数时候控制器返回的渲染数组代表页面的一部分,这部分是请求的核心目标信息,被称为主内容main content,打开页面主要就是为了得到这个信息,在没有安装块block模块的情况下,页面只显示该信息,如果安装了块block模块,那么块模块会在主内容周围环绕其他信息,比如侧边栏、菜单栏、搜索栏等等;块模块将页面视为由多个区构成(区由主题来划分),这称为分区regions,每个分区中可以放置0个或多个块,每个块呈现一块信息,主内容一般放在主内容区中,要显示哪些信息块、怎么显示以及放在哪个区中显示是可以配置的,可在管理后台的区块配置(/admin/structure/block)中进行,这样就有了丰富多彩的页面了。
以上是宏观上的机制原理,在具体实现上当控制器返回渲染数组后,判断是否是一个局部信息(“#type”不为“page”),如果是那么将其作为主内容,然后派发“选择页面显示变体”事件,如果没有安装块模块,那么使用简单页面显示变体“simple_page”,此时只显示主内容,如果安装了块模块,那么将使用她提供的块页面显示变体“block_page”,该变体接收控制器返回的主内容渲染数组,然后将其和各种块内容组装为一个整页渲染数组(“#type”为“page”)并返回,此时已经得到整个页面的内容了,后续系统将继续执行占位替换、资源排序加载等等工作。
如果控制器直接返回了整页渲染数组,那么系统将跳过块模块的工作,直接继续后面的工作,那么控制器如何返回整页渲染数组呢?首先需要指定“#type”属性的值为“page”,其余部分可以是子元素(每个子元素对应一个分区的渲染数组,不必全部分区都要存在),或者可以是一个主题钩子,此时将钩子对应的模板内容渲染后作为整页内容,如果指定了钩子,那么代表分区渲染数组的子元素将失效,因此这两者是互斥的,除非在模板中使用了这些子元素(将整个数组作为上下文传递到模板中,并在模板中渲染了这些子元素)。
在块block模块中对各类信息块一直是操作的渲染数组,并不将其渲染成最终的html字符串,该渲染工作将在渲染整页渲染数组时在twig模板中进行(在模板中打印一个变量时,如果变量是数组那么将其当做渲染数组进行渲染输出,详见twig服务)
“选择页面显示变体”事件:
只要控制器返回的渲染数组不是整页渲染数组,那么html渲染器(服务id:main_content_renderer.html)将派发“选择页面显示变体”事件:render.page_display_variant.select,默认使用“simple_page”显示变体,但只要块block模块被安装了则将订阅她并无条件设置页面使用“block_page”显示变体
订阅器服务id:block.page_display_variant_subscriber
类:\Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
显示变体插件管理器:
页面显示变体是由显示变体插件管理器管理并实例化的:
服务id:plugin.manager.display_variant
类:Drupal\Core\Display\VariantManager
该插件管理器很简单,插件定义数据的修改钩子为“display_variant_plugin”,定义数据被缓存在“cache.discovery”缓存后端中。
自定义显示变体:
在模块的src/Plugin/DisplayVariant目录下,建立插件类,实现以下接口:
Drupal\Core\Display\VariantInterface
通常继承以下基类:
\Drupal\Core\Display\VariantBase
给出插件释文,清除缓存后插件将被自动收集
系统默认提供的简单页面变体是一个很好的参考:
\Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant
请见该插件的实现
“block_page”显示变体:
这是块模块参与页面渲染流程的入口,插件id:block_page,插件类定义:
\Drupal\block\Plugin\DisplayVariant\BlockPageVariant
由于她实现了容器工厂插件接口,所以在插件管理器中将通过她的create静态方法来实例化(见本系列插件篇下集)
其build()方法返回整页渲染数组,每个子元素对应一个页面分区的渲染数组,以分区机器名作为子元素名,这里将其称为分区渲染数组,每个分区渲染数组包含1个或多子元素,每个子元素对应一个块的渲染数组;相反的,如果分区内一个块也没有,该整页渲染数组将不会包含该分区渲染数组。
在理解该显示变体的工作逻辑前,我们需要先了解块系统。
块系统概述:
一个drupal页面是由多个块构成的,每个块提供一块信息,通常主要区域显示控制器返回的主内容,该区域叫做主内容块,其周边分布着其他块,所有的块由块系统管理,块系统主要由块插件和块实体两大部分构成,块插件用于构建块的内容,块实体属于配置实体,用于提供前者的配置数据,如显示条件、分区位置、插件参数等,这两者有机结合形成了块系统,每一个块插件(类)可以根据不同的配置实例化出对应的多个块(实例对象),每个实例的配置都不同,他们共享相同的初始配置,一旦实例化后各实例有对应的块实体来储存配置信息,因此在后台:管理》结构》区块布局中可以将一个块(对应程序中的块插件)同时放置到多个分区中,每个分区中的块对应程序中的一个块实例,每个实例负责产生要显示的内容(返回渲染数组),同一个块插件在不同分区中的块实例可以输出不同,这依据该实例的配置而定,配置信息主要来自放置区块时提供的配置表单,由块配置实体储存。
块布局是针对主题而定的,不同的主题块布局可以不一样
块插件:
系统中的块以插件方式呈现,由块插件管理器管理(见本系列插件主题):
服务id:plugin.manager.block
类:\Drupal\Core\Block\BlockManager
获取方式:\Drupal::service('plugin.manager.block')
该插件管理器比较简单,实现了插件管理器的:分类插件接口、上下文感知接口、回退插件接口
块插件定义的修改钩子为:'block'
所有的块插件类必须实现以下接口:
Drupal\Core\Block\BlockPluginInterface
该接口继承了很多接口,来看一下块插件具备的特性:
可配置:
通常块插件是需要配置信息的,因此实现接口:\Drupal\Component\Plugin\ConfigurablePluginInterface
有依赖:
配置可能有依赖所以插件有依赖,因此实现接口:\Drupal\Component\Plugin\DependentPluginInterface
提供配置表单:
在管理界面提供配置交互,需要表单,因此实现接口\Drupal\Core\Plugin\PluginFormInterface
内容是可缓存的:
块内容需要缓存提供性能,因此实现接口:\Drupal\Core\Cache\CacheableDependencyInterface
需要知道自己的插件定义元数据:
很多情况下需要知道插件自身的定义,因此实现接口:\Drupal\Component\Plugin\PluginInspectionInterface
可从其他插件派生:
因此实现接口:\Drupal\Component\Plugin\DerivativeInspectionInterface
上下文感知:
在默认提供的块插件基类(\Drupal\Core\Block\BlockBase)中实现了上下文感知接口:
\Drupal\Core\Plugin\ContextAwarePluginInterface
注意:并不是所有块插件都需要上下文(插件上下文见本系列插件下集),因此块插件接口并未继承该接口
可提供多种交互表单:
除配置表单外,有些块插件还需要多种表单交互,因此在默认提供的块插件基类中实现了以下接口:
\Drupal\Core\Plugin\PluginWithFormsInterface
注意:并不是所有块插件都需要多种表单交互,因此块插件接口并未继承该接口
自定义块插件:
定义一个实现了块插件接口(Drupal\Core\Block\BlockPluginInterface)的类,放置到模块的src/Plugin/Block目录中,给出释文信息即可
实际上系统已经为我们做了很多,提供了以下默认的块插件基类:
\Drupal\Core\Block\BlockBase
我们只需要继承她即可,在自定义类中不需要声明任何接口实现,只需要实现以下方法即可:
public function build()
该方法用于返回该块要显示的信息的渲染数组,其他方法在基类中已有默认实现,如果需要更多自定义,覆写基类方法即可,可参看系统提供的块作为示例。
块插件示例列举:
脚标块,最简单的块插件:
\Drupal\system\Plugin\Block\SystemPoweredByBlock
用于显示drupal脚标(版权标志)
用户登录块:
\Drupal\user\Plugin\Block\UserLoginBlock
提供用户登录表单
可在控制器中执行以下语句显示系统中所有的块:
\Drupal::service('plugin.manager.block') ->getDefinitions();
特殊的块:
备用块:
Drupal\Core\Block\Plugin\Block\Broken
用于在块找不到或不可用时,以该块代替,以显示提示消息
主内容块:
\Drupal\system\Plugin\Block\SystemMainBlock
用于包装控制器返回的主内容
标题块:
\Drupal\Core\Block\Plugin\Block\PageTitleBlock
用于显示页面标题
块插件派生:
块插件也可以像普通插件一样进行派生,从而间接得到一些块,比如系统提供的菜单块:
\Drupal\system\Plugin\Block\SystemMenuBlock
她将系统定义的每一个菜单映射为块,从而可以进行页面放置,关于菜单请见本系列菜单主题
块实体:
以上是块插件,她负责显示块的内容,下面来看一下块实体,她用于配置块插件,比如在哪个主题、哪个分区、什么条件下才显示,块实体类:\Drupal\block\Entity\Block
实现如下接口:
\Drupal\block\BlockInterface
\Drupal\Core\Entity\EntityWithPluginCollectionInterface
块实体储存处理器:Drupal\Core\Config\Entity\ConfigEntityStorage
这是一个比较简单的配置实体,关于实体请见本系列实体相关主题,在该实体中用到了插件集,下文将介绍块实体的一些重点内容。
块实体插件集:
块实体用到了插件系统提供的插件集对象以延迟实例化插件(详见本系列插件主题中集),在块实体内部使用了两个插件集:
一个集用于块插件,由于一个块实体对应一个块插件实例,因此使用了单插件实例集:
\Drupal\block\BlockPluginCollection
父类:\Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
她的插件信息数组存放在块实体的settings属性下
另一个集用于条件插件,实例化并管理多个条件插件:
\Drupal\Core\Condition\ConditionPluginCollection
父类:\Drupal\Core\Plugin\DefaultLazyPluginCollection
她的插件信息数组存放在块实体的visibility属性下
块插件和条件插件就在这两个插件集中实例化,这是充分理解插件集的很好列子
块实体命名:
也就是后台:管理》结构》区块布局页面中点击某个块的配置按钮后,在弹出框中标题的机读名称,该名字就是块配置实体的配置id,在新建时是可以自定义的(建立后不可更改),默认是块插件的以下方法的返回值:
getMachineNameSuggestion()
在块插件基类的该方法的中(\Drupal\Core\Block\BlockBase::getMachineNameSuggestion),以块插件释文中的admin_label经过音译转换服务(\Drupal::transliteration())处理后得到
如果以上得到的块实体id已经被使用,也就是说存在同一个块插件有多个实例的情况下,那么以追加序列号的方式解决,序列号从2开始,依次加1,保证唯一性,该规则在块默认添加表单中定义:
\Drupal\block\BlockForm::getUniqueMachineName
块插件如果需要特定的名字,那么需要覆写块插件基类的以上机器名建议方法,系统默认提供的很多块的配置实体采用“主题名+块插件id”方式。
块表单:
关于更多实体的表单相关知识,请查阅本系列实体表单相关主题,以下列出简单信息以供查阅:
添加、编辑表单:
表单类:Drupal\block\BlockForm
使用示例:
$entity = \Drupal::entityTypeManager()->getStorage('block')->create(['plugin' => $plugin_id, 'theme' => $theme]);
return \Drupal::service('entity.form_builder')->getForm($entity);
删除表单:\Drupal\block\Form\BlockDeleteForm
启用禁用操作(并非表单操作):\Drupal\block\Controller\BlockController::performOperation
块显示条件:
块系统采用条件插件来配置块的可见性,条件插件管理器为:
\Drupal::service('plugin.manager.condition');
由于该块内容比较重要,本系列已独立讲解,见本系列《条件插件》主题,使用示例请见块访问控制处理器:
\Drupal\block\BlockAccessControlHandler::checkAccess
块列表缓存标签:
获取方法:
\Drupal::entityTypeManager()->getDefinition('block')->getListCacheTags();
这是一个全局块列表缓存标签,失效该标签将导致所有具备块列表的页面失效,默认值为:config:block_list
可在块配置实体释文中指定(\Drupal\block\Entity\Block),如果没有指定默认采用以下格式:
'config:' .配置实体id . '_list'
详见:\Drupal\Core\Config\Entity\ConfigEntityType::__construct
块列表缓存标签就来自这个构造函数
块知识库:
服务id:block.repository
类:Drupal\block\BlockRepository
获取方法:\Drupal::service('block.repository');
相当于块的注册表,依据各活动主题从实体系统中查询出块实体,排好序并按分区返回
实现了以下接口:
\Drupal\block\BlockRepositoryInterface
只有一个方法:getVisibleBlocksPerRegion(array &$cacheable_metadata = [])
该方法的参数$cacheable_metadata以引用接收,用于向调用者传递分区的可缓存元数据,以便在分区中块的可见性发生变化时让缓存失效,是一个数组,键名为分区名,键值为可缓存元数据对象。
该方法返回一个数组,第一级键名是分区机器名,第二级键名是该分区下可见的块实体id,值为块配置实体,用于保存该块的配置信息,块视图构建器通过块实体产生该块的渲染数组
返回的数组中,每个分区里面的块已经经过了排序,排序逻辑为:首先按是否禁用的状态排序,其次是权重,最后按label字母排序;在该方法内已经做了块访问权限检查,不可访问的块不会被返回;如果块所在分区没有在主题中定义那么该块被丢弃
块实体由以下程序产生:
\Drupal\block\Controller\BlockAddController::blockAddConfigureForm
在内部由块实体表单(\Drupal\block\BlockForm)提交处理器进行存放,见本系列实体表单相关内容
bug:块知识库接收context.handler服务做参数,但并没有使用,在服务定义中需要清除掉,这被块访问控制处理器所使用,但不需要在这里传入
块访问控制处理器:
一个块是否应该被显示,通过以下代码判断:
$access = $block->access('view', NULL, TRUE); //$block是块实体对象
这实际上是执行了块访问控制处理器:
\Drupal\block\BlockAccessControlHandler:: access
块可见性访问检查分三个部分依次执行:
1、模块钩子hook_entity_access() 和 hook_ENTITY_TYPE_access(),参数为$entity, $operation, $account
示例如下(假设模块名为yunke_help):
function yunke_help_block_access($entity, $operation, $account){
if($entity->id()=="bartik_branding"){
return \Drupal\Core\Access\AccessResult::forbidden();
}
}
此时页面上站点名称将消失
2、块实体上保存的条件插件,所有条件都必须满足(and关系)
3、块插件本身的访问检查,也就是执行块插件(非块实体)的该方法:$block_plugin->access($account, TRUE);
只有所有条件通过后,才能显示,如果块插件或条件插件所需插件上下文对象得不到满足,那么将视为不能通过;访问结果以对象(\Drupal\Core\Access\AccessResultInterface)返回,从而带回缓存元数据,在更新时及时调整。
块视图构建器:
块视图构建器依据块实体返回块渲染数组,但并不是简单的直接返回块插件构建的渲染数组,实际上块插件构建的渲染数组是在该数组的#pre_render回调中取回,这样处理的目的是让其他模块有能力控制块,带来极大的灵活性。
块视图构建器是一个实体处理器,她的类定义保存在块配置实体的释文中(处理器根键下的"view_builder"键中),默认为:
Drupal\block\BlockViewBuilder
获取方法:
$viewBuilder=\Drupal::entityTypeManager()->getViewBuilder('block');
因为其是实体处理接口的子类,所以实例化时将调用她的createInstance静态方法。
使用方法如下:
$viewBuilder ->view($block);
这返回一个经过处理的块渲染数组,该数组构建过程如下:
第一步:先产生一个初级的渲染数组,如下:
$build[$entity_id] = [
'#cache' => [
'keys' => ['entity_view', 'block', $entity->id()],
'contexts' => Cache::mergeContexts(
$entity->getCacheContexts(),
$plugin->getCacheContexts()
),
'tags' => $cache_tags,
'max-age' => $plugin->getCacheMaxAge(),
],
'#weight' => $entity->getWeight(),
];
该数组主要是缓存元数据信息,然后系统派发如下钩子:
$this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
钩子函数如下:
hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block)
hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block)
默认安装情况下,系统中没有地方实现此钩子,在这两个钩子中模块可以添加修改缓存元数据,注意如果块不是主内容块或标题块,那么不可添加和#lazy_builder并存冲突的属性,但可以设置#lazy_builder,一经设置将以此为准,这两个钩子的处理结果优先级很高,系统后续都是采用数组的附加操作,也就是说该钩子处理后的渲染数组,只要已经存在某些数组键,那么将以她为准,后续流程不能覆写
此步骤中,如果块插件是需要插件上下文的,此时上下文还未注入
第二步:在该步,如果块插件需要上下文则执行注入操作,构建一个新的渲染数组:
$build = [
'#theme' => 'block',
'#attributes' => [],
// All blocks get a "Configure block" contextual link.
'#contextual_links' => [
'block' => [
'route_parameters' => ['block' => $entity->id()],
],
],
'#weight' => $entity->getWeight(),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
'#id' => $entity->id(),
'#pre_render' => [
static::class . '::preRender',
],
// Add the entity so that it can be used in the #pre_render method.
'#block' => $entity,
];
以上属性也见:template_preprocess_block(&$variables);
然后派发钩子:
$module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin);
钩子函数如下:
hook_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block)
hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block)
在派发这两个钩子时,如果块插件对象需要插件上下文,则已经注入,此时插件对象已可用,在钩子中可以添加#pre_render或#post_render回调来修改最后的块渲染数组
以上步骤返回的渲染数组直到实际渲染时才通过#pre_render回调从块插件中取回渲染数组(也就是执行块插件的 build()方法),取回内容被当做子元素存放在以上数组的content子键中。
注意在视图构建器中并不涉及权限检查
块列表构建器:
用于显示区块管理界面,也就是后台地址:/admin/structure/block所示的界面,块列表构建器类如下:
\Drupal\block\BlockListBuilder
列表构建器是系统较重要的内容,在多处被使用到,因此将在独立主题中讲解,块列表构建器向你展示了一个很好的案例。
补充:
1、如果一个块指定的分区不存在,该块又是启用的,那么将放入默认分区,也就是可见分区中的第一个,同时将该块禁用,见\Drupal\block\Entity\Block::preSave
2、块插件的build()方法在返回渲染数组时可仅返回缓存元数据而没有内容,此时插件不显示,但她返回的缓存元数据将发挥作用,这将使得在条件变化导致插件有内容时及时失效缓存的页面
3、控制器可以直接返回“#type”为“page”的渲染数组,此时将不会调用块模块,也就是说块模块不会参与执行流程,不被执行
4、在使用块插件时,如果其是\Drupal\Core\Plugin\ContextAwarePluginInterface的子类,那么从快实体中取回块插件对象后需要为其注入上下前文:
$block_plugin = $entity->getPlugin();
if ($block_plugin instanceof \Drupal\Core\Plugin\ContextAwarePluginInterface)
{
$contexts= $this->contextRepository->getRuntimeContexts(array_values($block_plugin->getContextMapping()));
$this->contextHandler->applyContextMapping($block_plugin, $contexts);
}