天天看點

雲客Drupal源碼分析之主題管理器themeManager

主題管理器主要作用是調用模闆或主題函數(主題函數不是主題鈎子函數,将在D9中移除)将渲染數組轉化為頁面字元串輸出、執行主題修改鈎子等,在閱讀本篇前,你應該先閱讀本系列的以下章節:

主題處理器:

服務id:theme_handler

類:Drupal\Core\Extension\ThemeHandler

和主題管理器隻一字之差,但功能完全不同,她是更底層的,用于提供主題擴充的掃描、安裝等

主題協商者:

服務id:theme.negotiator

類:Drupal\Core\Theme\ThemeNegotiator

用于判決系統采用哪一個主題作為目前請求的活動主題

主題初始化器:

服務id:theme.initialization

類:Drupal\Core\Theme\ThemeInitialization

加載主題相關檔案、解決繼承邏輯等

主題鈎子注冊:

服務id:theme.registry

類:Drupal\Core\Theme\Registry

管理注冊主題鈎子,是主題系統中較大的一個子系統

務必按以上順序閱讀完本系列對應章節,否則有些概念你可能了解不夠,如果閱讀完了恭喜可以繼續了。

主題管理器:

服務id:theme.manager

類:Drupal\Core\Theme\ThemeManager

擷取方法:\Drupal::theme();

主題管理器既用來派發主題修改鈎子,也用來渲染主題輸出,見下。

主題修改鈎子派發:

我們可以通過以下代碼派發子產品的資料修改鈎子:

\Drupal::moduleHandler()->alter($type, &$data, &$context1 = NULL, &$context2 = NULL);

但是她隻作用于子產品,主題定義的修改鈎子需要如下調用:

\Drupal::theme()->alter($type, &$data, &$context1 = NULL, &$context2 = NULL)

這隻調用目前請求使用的主題(包含其所有基主題)定義的修改鈎子,如果需要派發某一個特定主題的修改鈎子,那麼需要進行如下調用:

\Drupal::theme()->alterForTheme(ActiveTheme $theme, $type, &$data, &$context1 = NULL, &$context2 = NULL);

實際上前一個函數的工作是委派該函數執行的,參數如下:

$theme:見本系列主題初始化篇,ActiveTheme對象,代表要執行修改鈎的主題擴充(包含所有基主題)

$type:修改鈎子名,可以是一個字元串,也可以是由多個鈎子名組成的數組,此時她們全部被派發

$data:需要修改的資料,鈎子需要以引用方式接收

$context:後面兩個參數是傳遞給修改鈎子的額外可選參數,有些情況會用到

主題修改鈎子的函數名規則為:

主題機器名_鈎子名_alter

假設主題機器名為“yunke”,鈎子名為“form”,那麼函數為:

yunke_form_alter(&$data, &$context1, &$context2);

修改鈎子一般應該放置在主題的主擴充檔案中,主擴充檔案在主題初始化時被自動加載,如果放在其他檔案中,需要自行處理檔案加載問題

主題是有繼承的,派發修改鈎子時,在繼承鍊中應該是根主題先執行,最後執行活動主題,但很不幸的是目前的實作(drupal8.5)有bug,雲客已經送出了報告,是以請注意順序問題,目前有bug的實作是先執行直接基主題,然後依次到根主題,最後執行活動主題。

如果傳遞的修改鈎子名是數組,那麼在繼承鍊中,她們将在一個主題中全部執行完後,在執行下一個主題

渲染主題輸出:

主題管理器也被渲染器(\Drupal\Core\Render\Renderer)用來渲染設定了#theme、#theme_wrappers屬性的渲染數組(見本系列渲染器相關章節),假設$elements是一個渲染數組,那麼如果設定了$elements['#theme']且沒有設定$elements['#render_children']那麼就會啟動主題管理器進行渲染,調用如下:

$elements['#children'] = \Drupal::theme()->render($elements['#theme'], $elements);

渲染結果儲存在渲染數組的'#children'屬性中,主題管理器内部設定$elements['#render_children']為true以避免多次渲染形成無限遞歸

是否實作了主題鈎子除判斷渲染數組的#theme、#theme_wrappers屬性是否存在外,還要看能否真的找到主題鈎子,且傳回内容不為false,如果實作了主題鈎子,那麼主題管理器必須渲染所有子元素,此時渲染器不會追加$elements['#markup']的内容在輸出前面,也不會追加#prefix、#suffix,這些追加操作是主題鈎子的責任。

渲染流程:

調用入口:

\Drupal::theme()->render($hook, $elements);

其中$hook可以是單個主題鈎子,也可以是一個主題鈎子構成的數組,如果是數組的話将從第一個到最後一個依次查找,使用第一個有效的鈎子,由此步确定出一個傳入的原始鈎子(數組找完也沒有有效實作時,那麼采用最後一個元素值作為原始鈎子)。

如果原始鈎子沒有被實作,那麼檢視她是否包含雙下劃線“__”,如果包含,那麼說明鈎子是擴充鈎,那麼再向根方向逐級查找,使用第一個找到的鈎子;如果依然沒有實作的鈎子,那麼傳回false,渲染結束并在非數組的情況下記錄日志。如果找到,那麼由此确定出一個主題實作的初始鈎子(初始鈎和原始鈎差別:初始鈎相比可能是更上一級的鈎子,她必須經過注冊,而原始鈎不要求一定是注冊過的,這兩個概念在下文多次用到,注意差別)。

得到初始鈎子後,确定變量數組(用于傳遞給模闆或主題函數),渲染數組中如果有鈎子定義中的變量,則将值提取出來覆寫鈎子定義的預設值,否則采用預設值,同時将原始鈎(注意不是初始鈎)以'theme_hook_original'作為鍵名儲存到變量數組中,注意如果渲染數組中有鈎子定義中沒有定義的變量,它們不會被傳遞,換句話說模闆或主題函數隻接收鈎子定義中聲明的變量以及預處理函數附加的變量。

确定基本鈎,也就是鈎子定義中的'base hook'值,如果沒有該值,說明初始鈎本身就是基本鈎,以基本鈎進行主題鈎子建議,經過主題鈎子建議後(見下),得到真正要使用的鈎子定義。

根據鈎子定義加載相關檔案,并執行預處理函數:

preprocessor_function(&$variables, $hook, $info);

預處理函數應該以引用方式接收變量,以便進行修改,系統忽略其傳回值,第二個參數是初始鈎名(不是原始鈎),$info是鈎子建議後的定義(不一定是初始鈎的定義)

允許預處理函數附加可冒泡中繼資料到渲染數組(通過在變量中設定'#attached', '#cache'方式添加),這可以讓渲染上下文對象跟蹤到預處理函數額外添加的附件和緩存屬性,但緩存屬性中不允許緩存'keys',有也會被删除(不會抛出異常)

如果主題鈎子定義采用主題函數,那麼執行她産生輸出,傳回Markup對象,渲染結束,否則進行以下模闆渲染邏輯。

如果采用模闆方式,那麼根據采用的模闆引擎确定渲染函數及模闆擴充。預設使用的是twig,擴充為“.html.twig”,她的渲染函數是位于core/themes/engines/twig/twig.engine檔案中的以下函數:

twig_render_template($template_file, array $variables)

在模闆實作中確定運作了以下函數(該函數主要用于添加通用模闆變量,介紹見下文):

template_preprocess($default_template_variables, $hook, $info);

在主題鈎子注冊過程中,有些特殊情況下該預處理函數可能沒有被收集到,比如鈎子原始實作為主題函數方式,後又被覆寫為模闆方式,那麼将丢失。

(該處實作為什麼不判斷該函數是否在$info['preprocess functions']中呢?其實也是可以的,但通過'directory'去判斷保證模闆總是有該變量,性能開銷也小)

處理屬性包:'attributes', 'title_attributes', 'content_attributes',如果渲染數組或鈎子定義中存在他們,有值時将包裝到屬性包,無值時指派為空屬性包

以theme_hook_suggestions作為變量名附加鈎子建議數組(在模闆渲染函數中可用于調試目的)

以theme_hook_suggestion作為變量名附加初始鈎子名(不是原始鈎子)

調用渲染函數$render_function($template_file, $variables);,預設的twig為(前文已提到):

twig_render_template

主題鈎子建議:

主題鈎子建議函數(或可叫做主題鈎子建議鈎子,後統稱為建議函數)是根據基本鈎(base hook)命名的函數,她給出基本鈎子可使用的所有擴充鈎子,這稱為“建議”,以數組傳回,該函數隻由子產品定義(主題可定義建議修改鈎子,見下),各子產品的建議函數傳回結果被合并,定義基本鈎的子產品通常應該為其定義預設的建議函數,以便使用更為合适的模闆;定義及命名如下:

hook_theme_suggestions_HOOK($variables)

也就是:子產品機器名_theme_suggestions_基本鈎子名($variables)

她接收變量數組作為參數值(可引用方式),其中鈎子名隻可使用基本鈎,不可使用擴充鈎(差別見本系列主題鈎子注冊);建議中應該包含基本鈎,初始鈎總被自動加入,原始鈎可以從$variables['theme_hook_original']中擷取

系統隻為具體的鈎子實作建議函數,不存在針對所有鈎子實作的建議函數,也就是說如下的函數不存在:

子產品機器名_theme_suggestions

建議函數傳回的建議數組不需要關心優先順序,因為此階段可由多個子產品一起參與,無法保證整體的順序,順序的調整應該在建議修改鈎子中進行,見下。

當收集完所有的鈎子建議後系統派發修改鈎,可選的在修改鈎中添加或删除鈎子建議,但必須在修改鈎中實作排序,建議數組後面的元素比前面的元素優先級更高,如果你是一位子產品開發者,定義了基本鈎,應該在實作建議函數的同時實作修改函數(修改鈎子),用修改函數保證鈎子順序。

子產品或主題可實作針對所有主題鈎子的修改鈎,也可實作針對某具體鈎子的,實作如下:

hook_theme_suggestions_alter(&$suggestions, $variables, $base_hook)

通用修改鈎,針對全部主題鈎子修改建議

hook_theme_suggestions_HOOK_alter(&$suggestions, $variables, $base_hook)

具體修改鈎,針對具體主題鈎子修改建議

執行順序是子產品、基主題然後是活動主題,後面的可以覆寫前面的,是以活動主題優先級最高,在同一個子產品或主題中,通用修改鈎先執行,是以具體修改鈎可以進行覆寫,後者優先級更高

修改鈎都應該以引用方式接收存在的建議數組,當處理完成後,系統認為建議數組中越靠後的鈎子優先級越高,第一個元素最低,是以從最後一個開始查找,以找到的第一個被注冊的鈎子為準進行主題實作。

非常重要的幾個主題鈎子建議函數:

節點子產品的主題鈎子建議函數:

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function node_theme_suggestions_node(array $variables) {
  $suggestions = [];
  $node = $variables['elements']['#node'];
  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');

  $suggestions[] = 'node__' . $sanitized_view_mode;
  $suggestions[] = 'node__' . $node->bundle();
  $suggestions[] = 'node__' . $node->bundle() . '__' . $sanitized_view_mode;
  $suggestions[] = 'node__' . $node->id();
  $suggestions[] = 'node__' . $node->id() . '__' . $sanitized_view_mode;

  return $suggestions;
}
           

整頁面鈎子建議:

C:\root\drupal\core\includes\theme.inc中的theme_get_suggestions函數

子元素渲染:

或許你已經注意到了傳遞給模闆的變量或者預處理函數處理的變量都還是渲染數組,而他們在模闆中以直接輸出的方式輸出了,這是怎麼回事呢?模闆中用于直接輸出的變量不應該是标量類型麼?在原生twig中确實如此,drupal裡能這樣處理是因為她為twig添加了擴充,當模闆中直接列印一個數組時,将被當做渲染數組,再次調用drupal渲染器,關于這一點請見本系列後續的twig服務相關主題,為了靈活可互操作,輸出内容在系統中總是盡量保持渲染數組方式的,子元素的渲染回路延伸到了模闆引擎内部。

預處理函數附加attached和緩存處理:

預處理函數可以向傳遞給主題函數或模闆的變量中添加額外的變量資料,是以這些資料的緩存屬性也應該影響到渲染結果的緩存性,此外還有附加資源問題,預處理函數可以在變量數組中設定'#attached', '#cache'鍵名的方式解決,可将'#attached', '#cache'視為和在渲染數組中一樣,在渲染流程中,他們會被合并到渲染上下文堆棧中,達到和在渲染數組中指定一樣的效果。

開發者需要特别注意該問題,傳遞了額外資料也需要傳遞她們。

template_preprocess預處理函數:

該預處理函數位于:core/includes/theme.inc,作用是為所有的模闆附加通用變量(主題函數方式預設不加),通用的模闆變量如下:

[
    'attributes' => [],
    'title_attributes' => [],
    'content_attributes' => [],
    'title_prefix' => [],
    'title_suffix' => [],
    'db_is_active' => !defined('MAINTENANCE_MODE'),
    'is_admin' => FALSE,
    'logged_in' => FALSE,
    'directory' = \Drupal::theme()->getActiveTheme()->getPath();
  ]
           

子產品可以實作template_preprocess_default_variables修改鈎子去修改添加預設模闆變量,如使用者子產品:

user_template_preprocess_default_variables_alter(&$variables)

在一個請求中所有模闆的預設變量被緩存在中央靜态函數drupal_static(__FUNCTION__)中,是以開發者也可以通過如下方式去改變預設值:

$default_variables= &drupal_static(“template_preprocess”);

如果傳遞給模闆的變量已經有某個值,那麼優先級高于預設值;如果設定了變量'attributes',那麼她将和預設值合并。

注意:在該函數的注釋中提到它是預處理階段執行的第一個函數,這是不正确的,雖然大多數情況是如此但有另外,預處理函數不要依賴執行順序

contextual_preprocess預處理函數:

在系統預設安裝時,針對所有主題鈎子的通用預處理函數隻有兩個,除上文講到的template_preprocess外,就是該函數了,用于處理上下文連接配接,位于:

\core\modules\contextual\contextual.module

你可以自行看看

屬性處理:

在傳遞給模闆的變量數組中,鍵名'attributes', 'title_attributes', 'content_attributes'有特定含義,表示屬性,會自動被系統包裝到屬性對象中,這些鍵名不能用作他用

引擎渲染函數:

引擎渲染函數是主題系統調用模闆引擎的入口,命名規則:

引擎名_render_template

她接收模闆檔案路徑(從系統根目錄開始的路徑,包含檔案名及擴充名),以及模闆變量,預設的twig引擎函數如下:

twig_render_template($template_file, array $variables)

位于:core/themes/engines/twig/twig.engine

調試資訊就在該函數中添加的,注意:

THEME HOOK: 的值是原始鈎子,不是初始鈎子

FILE NAME SUGGESTIONS:的值中第一個優先級最高,目前被選用的模闆以'x' 開頭,否則以'*'開頭

BEGIN OUTPUT from以及END OUTPUT from值相同,模闆全路徑,從系統根目錄開始包含檔案名及擴充名

定義主題鈎子并實作她:

首先自定義一個子產品,目錄為\modules\custom\yunke,機器名為“yunke”,在yunke.theme檔案中定義主題鈎子注冊函數:

function yunke_theme()
{
    return [
        'yunke' => [
            'variables' => ['title' => null, 'msg' => null],
        ],
    ];
}
           

這樣就注冊了一個叫做“yunke”的主題鈎子,然後在\modules\custom\yunke\templates中建立該主題鈎子的預設模闆檔案,檔案名為:“yunke.html.twig”,内容如下:

{% if title %}
  <h1>{{ title }}</h1>
{% endif %}
<div>{{ msg }}子產品中的模闆</div>
           

然後我們建立一個主題擴充,目錄為:

\themes\custom\yunke_theme

機器名定為:yunke_theme

在info檔案中指定:

base theme: bartik

這樣我們可以在預設的bartik主題基礎上實驗我們的效果

然後定義一個實作“yunke”主題鈎子的模闆,目錄:\themes\custom\yunke_theme\templates,檔案名為:“yunke.html.twig”,内容如下:

{% if title %}
  <h1>{{ title }}</h1>
{% endif %}
<div>{{ msg }}主題中的模闆,主題生效中</div>
           

然後定義一個控制器,傳回如下内容:

return [
            "#title"=>"yunke title",
            "#msg"=>"yunke msg",
            "#theme"=>"yunke",
        ];
           

現在就可以看到效果了,通路這個控制器,看看結果

然後将上文自定義的主題擴充開啟并設為預設,再看看效果

(注意:在該示例中雲客假設你已經會進行基本的子產品和主題開發、安裝、設定路由)

補充:

占位符的替換是和css/jss等在同一個階段處理的,請見本系列後續的《響應附屬處理attachments_processor》

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

下一篇: regexp_replace

繼續閱讀