雲客Drupal源碼分析之實體視圖構建器EntityViewBuilder

實體視圖顯示和表單顯示在實現上有許多相似之處,許多類都共用了相同基類,有許多概念和知識已經在本系列的實體表單顯示主題中介紹過,如顯示模式及其定義等,本篇不再重複介紹,將假設讀者已閱讀過實體表單顯示主題;視圖這個詞可能會讓人感覺生澀,可以將其理解爲查看、顯示,實體視圖構建就是產生實體的查看頁面或數據。

從節點視圖說起:
在drupal中最常見的是節點查看,網址如:/node/1,查看節點的路由名爲:
  entity.node.canonical
此路由在以下方法中動態定義:
  \Drupal\node\Entity\NodeRouteProvider::getRoutes
對應的路由控制器:\Drupal\node\Controller\NodeViewController::view
該控制器繼承自以下類:
  \Drupal\Core\Entity\Controller\EntityViewController
該類是一個通用實體視圖控制器類,如果路由定義中沒有設置控制器,只設置了“_entity_view”,那麼就會直接使用該類的view方法做控制器(該方法的參數已在路由系統中得到轉化,請見參數轉換相關主題),“_entity_view”在以下路由增強器中進行處理:
  \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer::enhanceEntityView
以上“_entity_view”的值是如下格式:
  實體類型id.視圖模式名
其中實體類型id是必不可少的,和視圖模式以點號分隔,如果省略點號及視圖模式名,則默認使用全文視圖模式:“full”,但在默認情況下,full模式並沒有開啓,此時將回退使用default視圖模式,如果開啓了則full模式優先,注意這和默認表單模式直接採用“default”不同
這樣的用法可參見用戶實體的entity.user.canonical路由

由以上視圖控制器類可見真正產生用於顯示實體的渲染數組的是實體視圖構建器,見下。

視圖模式:
顯示模式分爲表單模式和視圖模式,詳見本系列實體表單顯示主題,和表單模式一樣,並不是所有實體類型都能運用視圖模式,條件是實體類型定義中設置了根鍵field_ui_base_route且不能爲空,且定義了視圖構建器。詳見以下類:
  \Drupal\field_ui\Controller\EntityDisplayModeController
這意味着只有內容實體類型才能設置顯示模式,默認安裝中有六個實體類型可以設置,實體類型id如下:

block_content(自定義區塊)
comment(評論)
contact_message(聯繫信息)
node(內容)
taxonomy_term(分類術語)
user(用戶)

他們都是內容實體類型,在drupal中內容實體類型繼承自可字段化實體類型,視圖顯示模式即是用於管理哪些字段被顯示及如何顯示

實體視圖構建器:
實體視圖構建器在實體釋文中定義,用於構建顯示實體的渲染數組,獲取方法如下:

$viewBuilder=\Drupal::entityTypeManager()->getViewBuilder($entityTypeID);

獲取後產生顯示渲染數組:

$renderArr =$viewBuilder->view($entity, $view_mode);

要顯示一個節點實體,可以在控制器中執行以下代碼:

   $node = \Drupal::entityTypeManager()->getStorage('node')->load(34);
   $viewBuilder=\Drupal::entityTypeManager()->getViewBuilder('node');
   return $renderArr =$viewBuilder->view($node);

和表單不同的是也可以同時渲染多個實體:

   $nodes[] = \Drupal::entityTypeManager()->getStorage('node')->load(34);
   $nodes[] = \Drupal::entityTypeManager()->getStorage('node')->load(37);
   $viewBuilder=\Drupal::entityTypeManager()->getViewBuilder('node');
   return $renderArr =$viewBuilder->viewMultiple($nodes);

視圖構建器是一種實體處理器,定義在實體釋文處理器鍵下的“view_builder”鍵中,按照處理器流程實例化,即如果實現了處理器接口,將從以下靜態方法得到實例對象:
  \Drupal\Core\Entity\EntityHandlerInterface::createInstance
視圖構建器需要實現接口:
  \Drupal\Core\Entity\EntityViewBuilderInterface
系統提供了默認實體視圖構建器基類:
  \Drupal\Core\Entity\EntityViewBuilder
實體的視圖構建器通常需要繼承她,如節點視圖構建器:
  \Drupal\node\NodeViewBuilder

實體視圖構建概述:
也就是顯示實體的渲染數組構建,實體視圖構建器既可以一次構建一個實體的視圖,也可以一次構建多個,傳入的實體對象必須是相同的實體類型,但bundle可以不同,換句話說視圖構建器是針對特定實體類型工作的,在構建過程中模塊可以通過鉤子爲每一個實體指定特定的視圖模式;視圖構建器除了針對實體進行視圖構建外,還可以針對實體字段以及字段中某個值單獨構建,這在進行多實體類型組合顯示時特別有用。

實體視圖構建器在構建實現流程上,利用了渲染管道中的“#pre_render”回調,以此回調運行爲時間點,可將構造視爲分兩步完成,第一步僅包含必要的基本數據,此時稱爲默認渲染數組,或基本渲染數組,該概念名稱將在下文多次提及,此時還未進行實體字段渲染,控制器返回的正是該數組,第二步是在控制器返回後,系統在渲染器中執行“#pre_render”回調完成的,這期間才構造實體字段的渲染數組,字段渲染數組由實體視圖顯示對象構造,本系列將單獨講解,回調運行完成後纔得到完整的實體渲染數組;整個構建過程中將派發多個鉤子以便模塊可以控制渲染結果。
由上可見實體視圖構建器可以輸出實體列表頁,這和drupal中另一個概念容易混淆:列表構建器,後者也是一種實體處理器,定義在實體釋文處理器鍵下的“list_builder”中,路由可用“_entity_list”,主要用於實體管理目的,而非實體顯示,可在控制器中執行以下代碼看一看列表構建器產生的輸出:

return $this->entityManager()->getListBuilder('node')->render();

她雖然也顯示了一些實體字段信息,但和視圖是不一樣的,如附帶了操作鏈接,本系列將單獨講解。

實體視圖渲染數組結構:
單個實體的視圖渲染數組結構如下(基本數組指控制器返回的數組):
#{$entityTypeId}:鍵值爲實體對象,鍵名如“#node”,在基本數組中存在
#view_mode:實際用於渲染的視圖模式,已經過鉤子調整,在基本數組中存在
#cache:緩存屬性,使用實體對象的緩存屬性,但添加了緩存標籤:$entityTypeId . '_view',通過該標籤可失效所有視圖緩存;緩存屬性有些情況下可能不存在,比如在節點預覽模式中的實體顯示,如果可緩存則會在其下設置keys鍵,利用渲染緩存提高性能
#theme: 如果存在以實體類型id作爲名字的主題鉤子,將在基本數組中添加該鍵,值爲實體類型id
#weight:顯示排序序號
#contextual_links:值爲上下文鏈接,僅在完整數組中存在(基本數組中沒有)
字段名:由視圖顯示對象構建,僅在完整數組中存在(基本數組中沒有)

查看實體的完整視圖渲染數組可在控制器中執行以下代碼:
批量查看:

        $node[] = \Drupal::entityTypeManager()->getStorage('node')->load(37);
        $viewBuilder = \Drupal::entityTypeManager()->getViewBuilder('node');
        $renderArr = $viewBuilder->viewMultiple($node);
        unset($renderArr['#pre_render']);
        $renderArr = $viewBuilder->buildMultiple($renderArr);

查看單個實體:

        $node = \Drupal::entityTypeManager()->getStorage('node')->load(34);
        $viewBuilder=\Drupal::entityTypeManager()->getViewBuilder('node');
        $renderArr =$viewBuilder->view($node);
        unset($renderArr['#pre_render']);
        $renderArr = $viewBuilder->build($renderArr);

提示:如果你不是使用斷點調試工具查看$renderArr而是直接打印,很可能會耗盡內存而無法顯示,可安裝“yunke_help”模塊,用其提供的打印函數查看

默認實體視圖構建器基類方法解釋:
public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL)
渲染多個實體,多個實體以數組方式傳入,按傳入的數組順序渲染,並以此順序返回渲染數組,實體會以傳入的語言優先顯示,其次使用內容語言協商的結果,針對每一個實體系統派發以下兩個修改鉤子:
  $entityType . '_build_defaults'
  'entity_build_defaults'
第一個針對具體的實體類型,第二個針對所有實體類型,默認沒有模塊實現,形式如下:
  hook_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode)
其中$build爲顯示實體的基本渲染數組,僅包含基本的共用數據(見上),此時尚未渲染字段(該操作在“#pre_render”回調中進行),派發鉤子的目的便是爲了模塊可以向基本數組添加數據;$entity爲實體對象,$view_mode爲原始請求的視圖模式,注意這不一定是實際被使用的視圖模式,因爲在實際渲染過程中模塊可以修改她,後者保存在$buil['#view_mode']中

protected function isViewModeCacheable($view_mode)
判斷某個視圖模式的渲染結果是否能被緩存,該值最初來源於“entity_view_mode”實體,如節點全文視圖模式的查看代碼如下:
  \Drupal::entityTypeManager()->getStorage('entity_view_mode')->load("node.full")->get("cache");
這原始來自於以下變量:
  \Drupal\Core\Entity\EntityDisplayModeBase::$cache
默認爲true能緩存,在後臺管理配置頁面沒有UI設置接口,這是一個需要改進的地方,但模塊可以在“entity_view_mode”實體保存時通過相關鉤子去改變該值,也可以在使用過程中通過以下修改鉤子去改變:
  entity_view_mode_info
但須注意:該修改鉤子是在實體顯示知識庫中派發的,如果模塊沒有通過知識庫去獲取值,將不起作用,在規範情況下模塊應當通過知識庫獲取

protected function getBuildDefaults(EntityInterface $entity, $view_mode)
返回實體最初的默認(基本)渲染數組,後續可被鉤子修改,並不包含字段的渲染數據,在構造該渲染數組前,會派發以下修改鉤子以允許模塊改變單個實體的視圖模式:
  'entity_view_mode'
鉤子函數如下:
  hook_entity_view_mode_alter(&$view_mode, EntityInterface $entity, $context)
其中$view_mode爲原始請求的視圖模式(字符串值),$entity爲實體對象,$context派發時爲一個空數組;該鉤子意味着在顯示實體列表時,她們可以各自使用不同的顯示模式,這帶來極大的控制細膩度,比如node實體類型,相同bundle都可以採用不同模式來顯示,甚至依據實體的某個屬性值來進行不同顯示;該方法返回的基本渲染數組包含實體對象、實際使用的實體模式、緩存屬性、可選的主題鉤子。
運行時主題註冊表是主題註冊表的優化(見本系列主題鉤子註冊),還沒有被請求的主題鉤子在其中存在,但值爲NULL,其has方法將返回true;也就是說只要註冊了以實體類型id作爲名字的主題鉤子將添加#theme鍵,反之不添加。

public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL)
public function build(array $build)

這兩個方法一起渲染單個實體對象,注意設置#pre_render的微妙關係

public function buildMultiple(array $build_list)
將基本默認渲染數組轉變爲完整渲染數組,主要工作是渲染字段和派發鉤子;在渲染管道的#pre_render回調中執行,參數是viewMultiple方法返回的結果(如果有延遲構建或元素類型補充則可能不同),因此須用以下代碼來返回子元素的鍵名:
  $children = Element::children($build_list);
這將排除#號開始的屬性鍵名;每一個子元素對應一個實體的基本渲染數組,其最重要的有兩個鍵名:
  #{$entityTypeId}:鍵值爲實體對象
  #view_mode:實際用於渲染的視圖模式
在該方法中每個實體對象在其字段渲染完成後將派發以下兩個鉤子:
  {$entityTypeId}_view(如node_view)
  entity_view
參數爲:[&$build_list[$key], $entity, $display, $view_mode],也就是:
實體的渲染數組、實體對象、實體視圖顯示對象、視圖模式
鉤子運行後將添加上下文鏈接,運行alterBuild方法,排序字段,最後將運行這兩個鉤子的修改鉤子,參數和順序不變,僅少了$view_mode參數

public function buildComponents(array &$build, array $entities, array $displays, $view_mode)
調用視圖顯示對象構造實體字段的渲染數組,經過該方法後實體字段的渲染數組已經產生,參數解釋如下:
$build爲viewMultiple方法返回的結果(未進行字段渲染的基本渲染數組列表),鍵名爲渲染序號
$entities爲被渲染的實體對象,鍵名爲渲染序號,有相同的視圖模式和實體類型id,但bundle可以不同
$displays爲對應實體將使用的視圖顯示對象數組,鍵名爲bundle名,有相同的原始請求視圖模式
$view_mode爲使用的視圖模式,字符串
該方法會派發以下鉤子:
  entity_prepare_view
鉤子原型:
  hook_entity_prepare_view($entity_type_id, array $entities, array $displays, $view_mode)
該鉤子在實體字段被渲染前執行,主要目的是讓模塊可以通過她修改實體對象,當然也可以修改顯示對象,但往往修改顯示對象使用修改鉤子entity_view_display更爲合適
注意:基本渲染數組將採用“+”操作和視圖顯示對象構造的實體字段的渲染數組合並,基本渲染數組優先級更高,這意味着模塊可以通過viewMultiple方法中的鉤子修改基本數組以強制顯示某字段爲特定內容

在節點實體的該方法中對語言字段進行了固定顯示,在顯示配置中設置的格式化器將無效,併爲僞字段links添加了內容:如摘要視圖模式中的“閱讀更多”鏈接,模塊可以通過修改鉤子“node_links”添加更多內容,該修改鉤子如下:
   hook_node_links_alter(array &$links, NodeInterface $entity, array &$context)
這屬於節點實體專用修改鉤子,其他實體類型不可使用,參數如下:
  $links:爲鏈接渲染數組
  $entity:節點實體對象
  $context:爲數組['view_mode' => $view_mode,'langcode' => $langcode,];
添加的這些鏈接內容將在視圖顯示配置中所示位置出現

protected function addContextualLinks(array &$build, EntityInterface $entity)
爲實體的渲染數組添加上下文鏈接,上下文鏈接在前端頁面中以link標籤的方式提供與本實體相關的一些鏈接或信息,如:
  <link rel="canonical" href="/zh-hans/node/37" />
這些上下文鏈接可以被前端js程序使用,也可供搜索引擎識別

protected function alterBuild(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode)
實體的單個渲染數組構建完成後運行,默認沒有實現

public function resetCache(array $entities = NULL)
失效所有和傳入實體的視圖相關的緩存,包括列表視圖,參數爲實體對象數組,如果沒有傳遞參數,將失效這個實體類型的所有視圖

public function viewField(FieldItemListInterface $items, $display_options = [])
單獨渲染實體的一個字段對象,當多個實體的字段組合顯示時特別有用,如視圖模塊,參數$items是實體的某個字段對象,來自$entity->get($fieldName),參數$display_options指示如何渲染,分兩種情況,如果是一個字符串值,將當做視圖模式看待,按其指示的方法渲染(如不存在將回退到默認視圖模式),如果是一個數組,將當做顯示對象中該字段組件配置數據看待,此時將新建一個臨時的顯示對象,模式名爲默認的'_custom',並將執行:
  $display->setComponent($field_name, $display_options);
可通過以下代碼查看$display_options選項的數組內容:

 $display =\Drupal::entityTypeManager()->getStorage('entity_view_display')->load("node.article.default");
 $display_options=$display->getComponent('body');
 print_r($display_options);die;

鍵名解釋如下:
label:標籤label的位置信息,inline、above、hidden之一,默認above
type:字符串值,格式化器的插件id
settings:格式化器的配置數據,默認爲格式化器指定的默認值
weight:渲染輸出的權重值,默認爲0
region:分區信息,通常爲content代表可見
third_party_settings:第三方模塊的設置,鍵名爲模塊名

public function viewFieldItem(FieldItemInterface $item, $display = [])
單獨渲染實體字段對象中的一條值,當多個實體的字段組合顯示時特別有用,如視圖模塊,參數$item是字段對象中的一條值,如$entity->get($fieldName)->get(0),參數$display同viewField方法的$display_options

protected function getSingleFieldDisplay($entity, $field_name, $display_options)
獲取實體某個字段對象的顯示對象,參數$display_options同viewField方法的$display_options相同,注意當其爲字符串值時,返回的顯示對象中其他字段的顯示配置數據已被刪除

構建一個實體列表頁:
在本篇最後雲客爲讀者提供一個實用示例程序,可藉助實體查詢API的強大功能顯示所需的任何內容,請在控制器中執行:

        $limit = 3; //每頁顯示數量 值爲false將全部顯示
        $storage = \Drupal::entityTypeManager()->getStorage('node');
        $query = $storage->getQuery();//獲取實體查詢對象
        $query->sort('changed', 'DESC');//按時間倒序顯示,最新的在最前
        $query->currentRevision(); //僅選擇每個實體的當前版本
        $query->condition('status', \Drupal\node\NodeInterface::PUBLISHED);//僅顯示發佈的內容
        //顯示什麼取決於這裏設置的條件,更多請參考本系列實體查詢主題
        if ($limit) {
            $query->pager($limit);
        }
        $entity_ids = $query->execute();
        $entities = $storage->loadMultiple($entity_ids);
        $viewBuilder = \Drupal::entityTypeManager()->getViewBuilder('node');
        $build['nodes'] = $viewBuilder->viewMultiple($entities, 'teaser'); //構建摘要列表視圖
        if ($limit) {
            $build['pager'] = [
                '#type' => 'pager', //構建分頁器
            ];
        }
        return $build;

補充:
1、視圖構建器並不考慮權限問題,這需要調用者考慮
2、實體視圖顯示對象及格式化器將單獨講解,在視圖構建器中的使用入口如下:
構建多個實體的字段渲染數組:
  $display->buildMultiple($entities);
該方法渲染多個有相同實體類型、bundle、視圖模式的實體,保持傳入鍵名不變
構建單個實體的字段渲染數組:
  $display->build($entity);
傳入的實體必須屬於顯示對象對應的實體類型和budle

 

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

 

 

 

 

 

 

 

 

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