天天看點

雲客Drupal源碼分析之塊系統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);
}
           

我是雲客,【雲遊天下,做客四方】,聯系方式見首頁,歡迎轉載,但須注明出處

繼續閱讀