Problem/Motivation
The current theme system is a complicated mix of sub systems that pass data back and forth to one another in ways that confuses new users, and causes bugs. @see #939462: Specific preprocess functions for theme hook suggestions are not invoked
Proposed resolution
Separate each type of thing that happens during the theme system's preprocess phase into separate stages, called in the correct order, and only once. We have identified three things that currently happen in preprocess:
- Variables passed in via hook_theme are used to generate variables that will eventually be printed in templates
- Variables are added or changed before being printed in templates
- The template being used can be changed. (uhm, WHAT?)
The theme system should be refactored to work more like Form API. There should be a stage for definition, and a separate stage for variable alteration, instead of doing all these things in the same step. The ability to change the template being used doesn't belong here and MUST be split out into a separate earlier phase in the theme system preparation tasks, saving resources by not preparing and altering variables that will never be used.
The five basic phases are as follows:
- Phase 1: Build the theme registry
- Phase 2: Callback suggestions alter
- Phase 3: Prepare variables
- Phase 4: Alter variables
- Phase 5: Render output
The initial work done towards solving this problem at DrupalCon Portland by Fabianx and jenlampton can be found here.
Phase 1: Build the theme registry #
The process we use to build the theme registry is updated in the following ways:
- A theme's callbacks, default arguments, and template file name are defined in
hook_theme
. This remains unchanged from D7. - Let modules and themes define suggestions for base hooks, by adding
hook_theme_suggestions
. This will allow preprocess functions to run for the suggestion without having to define a template or specific preprocess function. - Let modules and themes define a render function for specific template suggestions by adding
hook_theme_render_TEMPLATE_SUGGESTION
to replacetheme_THEMEID__suggestion
and[module]_THEMEID
; the highest priority implementation wins. - Priority will be determined by a single
theme_stack
. A theme in this stack may have an associated theme engine. This simplifies things by replacing the distinction between base themes and themes. - Processing in the registry will be executed in the following order: Modules first, Theme Stack second.
Processing in the registry will execute the following steps, for each module and theme:
- Invoke
hook_theme()
. - Invoke
hook_theme_suggestions()
where suggestions may be registered and base_hook may be set, if it exists, otherwise ignore it. The precedence on suggestions is structured such that suggestions added later overrule earlier ones. - Foreach of those
$theme_ids
returned in hook_theme (not the suggestions) execute the following:- Call
theme_retrieve_render_functions([hook], $theme_id)
on each themes produced from- Looks for
hook_theme_render_THEME_ID()
andhook_theme_render_THEME_ID__*
- Registers suggestions if needed and sets
base_hook
to$theme_id
and'function' => found function
.
- Looks for
- Call
- For the whole module or theme call:
- Call
theme_retrieve_templates($engine, [hook], $path, $hooks)
- Foreach found template matching of of $hooks, the engine sets
'template' => 'path'
and'function' => 'engine_render_function'
.
- Foreach found template matching of of $hooks, the engine sets
- Call
The above process changes are beneficial in the following ways:
- Functions will now be found during registry build, they just need to be in the "include" or file defining hook_theme(). If hook_theme() can be called, so can the theme or render functions. This is the biggest and most important change to the status quo, which currently uses the engine to search for the theme functions (WHAT?).
- Theme engines will set render_function directly. No more need to special case the template case.
- There will be one single theme stack, listed in reverse order. No more need to special case base_themes.
- Later implementations overwrite earlier implementations completely.
- The theme registry will now be much simpler to build because a function will be defined for each hook, base hooks may now be nested, and the first hook to implement 'function' wins - regardless whether it is a theme function or a template rendered via engine function.
Phase 2: Callback suggestions alter #
#1751194: Introduce hook_theme_suggestions[_HOOK]() and hook_theme_suggestions[_HOOK]_alter()
hook_theme_suggestions_alter()
hook_theme_suggestions_TEMPLATE_SUGGESTION_alter()
This is a new phase. The callback_suggestions_alter phase will be the only way to add, change, or remove callback and template suggestions. This alter phase replaces $variables['theme_hook_suggestions']
in preprocess. This phase also allows altering the default "found suggestion", so for a node of type article, "article" might be changed to "page", for example.
Phase 3: Prepare variables #
#2035055: Introduce hook_theme_prepare[_alter]() and remove hook_preprocess_HOOK()
hook_theme_prepare()
hook_theme_prepare_TEMPLATE_SUGGESTION()
This is a new phase. The theme_prepare phase is where theme callback arguments are converted into variables to be rendered in the template. Modules that invoke this hook should only add information to the list of variables in this phase, similar to how hook_menu()
just adds information.
Examples of things that MAY be done inside a hook_theme_prepare implementation include:
- load a node from a nid argument and add it or its members as a variable usable in a template
- create a URL from a path argument
- create an image src from a URI
Since sanitization happens when the variables are printed in the template by Twig, there should NOT be any calls to check_plain here.
In Drupal core, this phase might be implemented in the following way:
$input = $variables;
$variables = module_invoke_all('theme_prepare', $input); // Modules can add, but not change others' things.
$variables['#original'] = $input; // Original pristine inputs from earlier phases are stored, so they can be used later.
Phase 4: Alter Variables #
#2035055: Introduce hook_theme_prepare[_alter]() and remove hook_preprocess_HOOK()
hook_theme_prepare_alter()
hook_theme_prepare_TEMPLATE_SUGGESTION_alter()
This is a replacement for the current preprocess layer. The hook_theme_prepare_alter phase allows for altering the variables that were prepared in Phase 3, similar to hook_form_alter
and hook_form_FORM_ID_alter
. The big difference here (and the reason for renaming this function) is that we don't want modules that defined their own theme callbacks in hook_theme to use this phase to alter those variables into a printable state; instead they should use Phase 3 for that.
Phase 5: Render/Output #
The final phase may have three possible outcomes:
- variables are inserted into a template (sanitized) | template -> HTML
- variables are concatenated into a string (sanitized) | theme function -> HTML
- variables may be returned (non-sanitized). This special case may not end up being necessary, but it would be possible by adding a TRUE/FALSE flag as a third parameter to return these variables as an array. @todo Discuss use cases and implementation.
Remaining tasks
All 4 "API Changes", below.
User interface changes
None, likely.
API changes
- Create a new hook:
hook_theme_suggestions_alter()
@see #1751194: Introduce hook_theme_suggestions[_HOOK]() and hook_theme_suggestions[_HOOK]_alter() - Create two new hooks:
hook_theme_prepare()
andhook_theme_prepare_alter()
@see #2035055: Introduce hook_theme_prepare[_alter]() and remove hook_preprocess_HOOK() - Split things happening in all existing preprocess functions into one of these three distinct stages instead @see #2060773: [META] Migrate all hook_preprocess() functions to hook_theme_prepare()
- Remove the old preprocess layer @see #2060783: Remove the preprocess layer.
Related Issues
- Registered hook suggestions MAY need to declare additional variables. #1222254: Remove theme_task_list() and call theme('item_list__tasks') instead.
- #1899454: [meta] Refactor Render API
- #1886448: Rewrite the theme registry into a proper service
- #1751194: Introduce hook_theme_suggestions[_HOOK]() and hook_theme_suggestions[_HOOK]_alter()
- #2035055: Introduce hook_theme_prepare[_alter]() and remove hook_preprocess_HOOK()