77. 主题管理器themeManager

主题管理器主要作用是调用模板或主题函数(主题函数不是主题钩子函数,将在D9中移除)将渲染数组转化为页面字符串输出、执行主题修改钩子等,在阅读本篇前,你应该先阅读本系列的以下章节:

主题处理器:

服务idtheme_handler

类:Drupal\Core\Extension\ThemeHandler

和主题管理器只一字之差,但功能完全不同,她是更底层的,用于提供主题扩展的扫描、安装等

主题协商者:

服务idtheme.negotiator

类:Drupal\Core\Theme\ThemeNegotiator

用于判决系统采用哪一个主题作为当前请求的活动主题

主题初始化器:

服务idtheme.initialization

类:Drupal\Core\Theme\ThemeInitialization

加载主题相关文件、解决继承逻辑等

主题钩子注册:

服务idtheme.registry

类:Drupal\Core\Theme\Registry

管理注册主题钩子,是主题系统中较大的一个子系统

 

务必按以上顺序阅读完本系列对应章节,否则有些概念你可能理解不够,如果阅读完了恭喜可以继续了。

 

主题管理器:

服务idtheme.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

 

 

本书共94小节:

评论 (写第一个评论)