transmuter-1.0.0-alpha2/src/ThemeRegistryBuilder.php
src/ThemeRegistryBuilder.php
<?php
namespace Drupal\transmuter;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\transmuter\Plugin\TransmuterManagerInterface;
/**
* The default implementation of the theme registry helper.
*/
class ThemeRegistryBuilder implements ThemeRegistryBuilderInterface {
/**
* Create a new instance of the ThemeRegistryHelper class.
*
* @param \Drupal\transmuter\Plugin\TransmuterManagerInterface $transmuterManager
* The element transmuter plugin manager.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
*/
public function __construct(
protected TransmuterManagerInterface $transmuterManager,
protected ThemeManagerInterface $themeManager,
) {}
/**
* {@inheritdoc}
*/
public function alterRegistry(array &$themeRegistry): void {
/** @var \Drupal\Core\Theme\ActiveTheme $activeTheme */
$activeTheme = $this->themeManager->getActiveTheme();
$themeName = $activeTheme->getName();
$themes = array_keys($activeTheme->getBaseThemeExtensions());
$themes[] = $themeName;
// Create a pattern that inserts the module provided transmuters before
// any theme hook, or before a preprocess hook that is more specific.
$modulePattern = '/^(?:' . implode('|', $themes) . ')_preprocess_{{HOOK}}|_preprocess_{{HOOK}}__\w/';
// Apply module defined transmuter plugins separately and before theme
// any defined plugins to ensure that plugin defined hooks are added before
// the theme hooks, so theme implementations can inherit these base hooks.
// @see self::addMissingThemeHooks()
$moduleDefs = $this->transmuterManager->getModuleDefinitions();
$this->doAlter($themeRegistry, $moduleDefs, $modulePattern);
// Apply plugins defined by the currently active theme.
$themeDefs = $this->transmuterManager->getDefinitionsByTheme($themeName);
$this->doAlter($themeRegistry, $themeDefs, '/_preprocess_{{HOOK}}__\w/');
}
/**
* Sort plugin definitions by the theme hook they implement.
*
* Returns the transmuter plugin IDs sorted by the theme hook preprocess
* plugin applies to.
*
* @param \Drupal\transmuter\Plugin\TransmuterDefinition[] $definitions
* The transmuter definitions to sort. Typically these are either
* definitions from modules OR the active theme.
*
* @return string[][]
* Preprocess plugin IDs sorted by the theme hook them belong to.
*/
protected function sortDefinitionsByHook(array $definitions): array {
$sorted = [];
foreach ($definitions as $id => $def) {
foreach ($def->getHooks() as $hook) {
$sorted[$hook][$id] = $id;
}
}
return $sorted;
}
/**
* Add transmuter plugin from $definitions to the theme registry.
*
* @param array $themeRegistry
* Reference to the theme registry. This will be altered to include the
* calls to the element transmuter plugins.
* @param \Drupal\transmuter\Plugin\TransmuterDefinition[] $definitions
* Transmuter plugin definitions to insert into the $themeRegistry.
* @param string $pattern
* Regular expression to use when finding the place to insert theme
* transmuters. Use the placeholder "{{HOOK}}" to be replaced with the
* current hook being placed. When a matching function name is found, the
* transmuter will be placed before the matching preprocess function.
*/
protected function doAlter(array &$themeRegistry, array $definitions, $pattern): void {
// Get the module defined preprocess plugins, and sorted by their hooks.
$byHook = $this->sortDefinitionsByHook($definitions);
// Create a pattern that inserts the module provided tranmuters before
// any theme hook, or before a preprocess hook that is more specific.
foreach ($themeRegistry as $hook => &$info) {
$this->addHookTransmuters($byHook, $hook, $info, $pattern);
}
// Add any hooks that are not already in the theme registry.
// This happens when theme hook is a hook suggestion only defined by a
// transmuter plugin. If a template or a hook_preprocess_HOOK() is defined
// the theme hook will already be defined.
foreach (array_diff_key($byHook, $themeRegistry) as $hook => $pluginIds) {
foreach ($pluginIds as $pluginId) {
$baseHook = $definitions[$pluginId]->getBaseHook();
if ($baseHook && isset($themeRegistry[$baseHook])) {
$themeRegistry[$hook] = $themeRegistry[$baseHook];
$themeRegistry[$hook]['base hook'] = $baseHook;
$themeRegistry[$hook]['preprocess functions'][] = new Transmuter($hook, $pluginIds);
continue(2);
}
}
// Using ungreedy search, we peel back the last theme pattern separator.
$prefixes = explode('__', $hook);
array_pop($prefixes);
// We are looking for the longest prefix matching hook or closest
// theme hook to base our new hook from. If we can't find a base hook
// definition, the hook can't be added, but any valid theme definition
// should at least have a theme function or template file and be defined.
while ($prefixes) {
$prefix = implode('__', $prefixes);
if (!empty($themeRegistry[$prefix])) {
$themeRegistry[$hook] = $themeRegistry[$prefix];
$themeRegistry[$hook]['base hook'] = $prefix;
$themeRegistry[$hook]['preprocess functions'][] = new Transmuter($hook, $pluginIds);
break;
}
array_pop($prefixes);
}
}
}
/**
* Inserts the transmuter based on the priority of the theme hook.
*
* This method adds the $transmuter into $tranmuters before the first
* match of $insertPattern. This method also advances the internal pointer
* of $existing the preprocess callback array, while searching for the place
* to add the transmuter.
*
* @param \Drupal\transmuter\Transmuter $transmuter
* The transmuter instance to insert.
* @param array $transmuters
* Current list of transmuters to insert the transmuter into.
* @param array $existing
* Reference to an array with the existing theme preprocessor callbacks.
* The internal pointer of this array is advanced to position of the
* inserted transmuter.
* @param string $insertPattern
* Regular expression to use when finding the place to insert theme
* preprocessors. Use the placeholder "{{HOOK}}" to be replaced with the
* current hook being placed. When a matching function name is found, the
* transmuter will be placed before the matching preprocess function.
*
* @see static::addHookTransmuters()
*/
private function insertTransmuter(Transmuter $transmuter, array &$transmuters, array &$existing, $insertPattern): void {
$pattern = str_replace('{{HOOK}}', preg_quote($transmuter->getHook()), $insertPattern);
while ($callback = current($existing)) {
// If matching the insert pattern, then place the transmuter here,
// but _DO NOT_ advance the existing function position. The next
// transmuter may still need to be placed before the next function.
if (preg_match($pattern, $callback)) {
$transmuters[] = $transmuter;
return;
}
$transmuters[] = $callback;
next($existing);
}
// Reached end of list, just add the rest to the end of list.
$transmuters[] = $transmuter;
}
/**
* Build a Transmuter item for the theme hooks.
*
* @param string[][] $definitions
* Transmuter plugin definitions to attempt to apply to the theme registry.
* @param string $hook
* The theme hook name.
* @param array $info
* Theme definition information from the theme registry.
* @param string $insertPattern
* Regular expression to use when finding the place to insert theme
* transmuters. Use the placeholder "{{HOOK}}" to be replaced with the
* current hook being placed. When a matching function name is found, the
* transmuter will be placed before the matching preprocess function.
*/
protected function addHookTransmuters(array $definitions, $hook, array &$info, $insertPattern): void {
$transmuters = [];
$prefixes = explode('__', $hook);
$targetHook = reset($prefixes);
$existing = $info['preprocess functions'] ?? [];
reset($existing);
// Apply any transmuter plugins defined for the base hook.
// If not already the base hook, a base hook should be inserted before
// any of the other hook implementations.
$baseHook = $info['base hook'] ?? NULL;
if ($baseHook && $baseHook !== $targetHook && !empty($definitions[$baseHook])) {
$transmuter = new Transmuter($baseHook, $definitions[$baseHook]);
$this->insertTransmuter($transmuter, $transmuters, $existing, $insertPattern);
}
do {
if (!empty($definitions[$targetHook])) {
$transmuter = new Transmuter($targetHook, $definitions[$targetHook]);
$this->insertTransmuter($transmuter, $transmuters, $existing, $insertPattern);
}
$prefix = next($prefixes);
$targetHook .= '__' . $prefix;
} while ($prefix);
// If tranmuters were added, put the remaining $existing callbacks
// to the end of the preprocess list, and set the new set of
// $transmuterss as the "preprocess functions" of this theme hook.
if (!empty($transmuters)) {
while ($callback = current($existing)) {
$transmuters[] = $callback;
next($existing);
}
$info['preprocess functions'] = $transmuters;
}
}
}
