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渲染器(服务idmain_content_renderer.html)将派发“选择页面显示变体”事件:render.page_display_variant.select,默认使用“simple_page”显示变体,但只要块block模块被安装了则将订阅她并无条件设置页面使用“block_page”显示变体

订阅器服务idblock.page_display_variant_subscriber

类:\Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber

 

显示变体插件管理器:

页面显示变体是由显示变体插件管理器管理并实例化的:

服务idplugin.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”显示变体:

这是块模块参与页面渲染流程的入口,插件idblock_page,插件类定义:

\Drupal\block\Plugin\DisplayVariant\BlockPageVariant

由于她实现了容器工厂插件接口,所以在插件管理器中将通过她的create静态方法来实例化(见本系列插件篇下集)

build()方法返回整页渲染数组,每个子元素对应一个页面分区的渲染数组,以分区机器名作为子元素名,这里将其称为分区渲染数组,每个分区渲染数组包含1个或多子元素,每个子元素对应一个块的渲染数组;相反的,如果分区内一个块也没有,该整页渲染数组将不会包含该分区渲染数组。

在理解该显示变体的工作逻辑前,我们需要先了解块系统。

 

块系统概述:

一个drupal页面是由多个块构成的,每个块提供一块信息,通常主要区域显示控制器返回的主内容,该区域叫做主内容块,其周边分布着其他块,所有的块由块系统管理,块系统主要由块插件和块实体两大部分构成,块插件用于构建块的内容,块实体属于配置实体,用于提供前者的配置数据,如显示条件、分区位置、插件参数等,这两者有机结合形成了块系统,每一个块插件(类)可以根据不同的配置实例化出对应的多个块(实例对象),每个实例的配置都不同,他们共享相同的初始配置,一旦实例化后各实例有对应的块实体来储存配置信息,因此在后台:管理》结构》区块布局中可以将一个块(对应程序中的块插件)同时放置到多个分区中,每个分区中的块对应程序中的一个块实例,每个实例负责产生要显示的内容(返回渲染数组),同一个块插件在不同分区中的块实例可以输出不同,这依据该实例的配置而定,配置信息主要来自放置区块时提供的配置表单,由块配置实体储存。

块布局是针对主题而定的,不同的主题块布局可以不一样

 

块插件:

系统中的块以插件方式呈现,由块插件管理器管理(见本系列插件主题):

服务idplugin.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

块列表缓存标签就来自这个构造函数

 

块知识库:

服务idblock.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);
}

 

本书共119小节:

评论 (写第一个评论)