In Drupal, with themes, you can override default template output by copying, pasting, and customizing those templates. It works, but how? I thought I always knew how this worked. But I realized I didn't when I dove into supporting Drupal 7 themes with Retrofit.
I know Drupal builds the theme registry and theme hook suggestions. In doing so, it scans templates, and part of that process includes scanning the active theme for template and theme hook template implementations. But when reading the \Drupal\Core\Theme\Registry
code, I was coming up blank.
Components of Drupal's theme registry
The theme registry in Drupal contains all theming information about defined templates and preprocess hooks. There is a base registry of theme hooks defined by modules. Then, there are versions of the registry for individual themes. That is because each theme may implement preprocess hooks or template overrides.
\Drupal\Core\Theme\Registry
– Maintains all theme hook, preprocess, template, etc. information\Drupal\Core\Utility\ThemeRegistry
– runtime registry cache collector, decorator of\Drupal\Core\Theme\Registry
and instantiated by\Drupal\Core\Theme\Registry::getRuntime
creating a theme-specific subset of the registry.\Drupal\Core\Template\Loader\ThemeRegistryLoader
– Twig loader that loads templates based on theThemeRegistry
runtime registry.
It's somewhat confusing. But it all revolves around \Drupal\Core\Theme\Registry
. The \Drupal\Core\Utility\ThemeRegistry
class is a way to try and optimize caching for the registry data.
Building the theme registry
The magic happens in \Drupal\Core\Theme\Registry::build
. The theme registry will first try to retrieve cached registry data about theme information provided by modules, consistent across any theme being used.
if ($cached = $this->cache->get('theme_registry:build:modules')) {
$cache = $cached->data;
}
If that results in a cache miss, Drupal invokes hook_theme
for all installed modules to create the base theme registry. The processExtension
method invokes the extensions' hook_theme
implementation and discovers any preprocess function hooks provided by that extension.
$this->moduleHandler->invokeAllWith('theme', function (callable $callback, string $module) use (&$cache) {
$this->processExtension($cache, $module, 'module', $module, $this->moduleList->getPath($module));
});
$this->cache->set("theme_registry:build:modules", $cache, Cache::PERMANENT, ['theme_registry']);
The next part of building the registry considers the current theme, its theme engine, and base themes. This is where I got confused and a bit lost. The code is calling processExtension
, again.
foreach (array_reverse($this->theme->getBaseThemeExtensions()) as $base) {
$base_path = $base->getPath();
if ($this->theme->getEngine()) {
$this->processExtension($cache, $this->theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path);
}
$this->processExtension($cache, $base->getName(), 'base_theme', $base->getName(), $base_path);
}
The theme's engine (Twig) is then processed. Again, it is calling processExtension
, and I didn't think anything of it.
if ($this->theme->getEngine()) {
$this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath());
}
Then the theme itself is processed.
$this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath());
I was perplexed. I know themes do not need to invoke hook_theme
to provide template overrides. They're automatically detected. Everything I read within processExtension
for themes was primarily handling their preprocess hooks.
Then I took a step back. I fired up Xdebug and truly stepped through the process. And I realized I overlooked a crucial part of the processing — handling of the theme_engine
.
The twig_theme
hook implementation
Located in the twig.engine
file is the twig_theme
theme hook implementation. It gets invoked when processExtension
is called for theme_engine
the extension.
function twig_theme($existing, $type, $theme, $path) {
return drupal_find_theme_templates($existing, '.html.twig', $path);
}
The drupal_find_theme_templates
function lives in includes/theme.inc
. It scans the directories of all themes available to Drupal and scans their directories for template files based on the given extension, .html.twig
. The templates are then matched in two ways
- Convert template file names with
-
to_
for the function naming scheme of theme hooks - Check if the template file name matches the template file name in an existing theme hook.
If a match exists, the theme registry will use that template file rather than the default template.
Once I discovered this tidbit, I could support PHPTemplate template overrides with Retrofit by overriding logic in processExtension
.
if ($type === 'theme_engine') {
$templates = drupal_find_theme_templates($cache, '.tpl.php', $path);
foreach ($templates as $theme_hook => $info) {
$cache[$theme_hook]['phptemplate'] = $info['path'] . '/' . $info['template'] . '.tpl.php';
$cache[$theme_hook]['template'] = 'theme-phptemplate';
$cache[$theme_hook]['path'] = '@retrofit';
}
}
Want more? Sign up for my weekly newsletter