1. 程式人生 > >雲客Drupal8原始碼分析之塊系統block

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

我是雲客,【雲遊天下,做客四方】,微訊號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137