雲客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);
}

 

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章