Chris@0: root = $root; Chris@0: $this->themeNegotiator = $theme_negotiator; Chris@0: $this->themeInitialization = $theme_initialization; Chris@0: $this->moduleHandler = $module_handler; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the theme registry. Chris@0: * Chris@0: * @param \Drupal\Core\Theme\Registry $theme_registry Chris@0: * The theme registry. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setThemeRegistry(Registry $theme_registry) { Chris@0: $this->themeRegistry = $theme_registry; Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getActiveTheme(RouteMatchInterface $route_match = NULL) { Chris@0: if (!isset($this->activeTheme)) { Chris@0: $this->initTheme($route_match); Chris@0: } Chris@0: return $this->activeTheme; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function hasActiveTheme() { Chris@0: return isset($this->activeTheme); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function resetActiveTheme() { Chris@0: $this->activeTheme = NULL; Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setActiveTheme(ActiveTheme $active_theme) { Chris@0: $this->activeTheme = $active_theme; Chris@0: if ($active_theme) { Chris@0: $this->themeInitialization->loadActiveTheme($active_theme); Chris@0: } Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function render($hook, array $variables) { Chris@0: static $default_attributes; Chris@0: Chris@0: $active_theme = $this->getActiveTheme(); Chris@0: Chris@0: // If called before all modules are loaded, we do not necessarily have a Chris@0: // full theme registry to work with, and therefore cannot process the theme Chris@0: // request properly. See also \Drupal\Core\Theme\Registry::get(). Chris@0: if (!$this->moduleHandler->isLoaded() && !defined('MAINTENANCE_MODE')) { Chris@0: throw new \Exception('The theme implementations may not be rendered until all modules are loaded.'); Chris@0: } Chris@0: Chris@0: $theme_registry = $this->themeRegistry->getRuntime(); Chris@0: Chris@0: // If an array of hook candidates were passed, use the first one that has an Chris@0: // implementation. Chris@0: if (is_array($hook)) { Chris@0: foreach ($hook as $candidate) { Chris@0: if ($theme_registry->has($candidate)) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: $hook = $candidate; Chris@0: } Chris@0: // Save the original theme hook, so it can be supplied to theme variable Chris@0: // preprocess callbacks. Chris@0: $original_hook = $hook; Chris@0: Chris@0: // If there's no implementation, check for more generic fallbacks. Chris@0: // If there's still no implementation, log an error and return an empty Chris@0: // string. Chris@0: if (!$theme_registry->has($hook)) { Chris@0: // Iteratively strip everything after the last '__' delimiter, until an Chris@0: // implementation is found. Chris@0: while ($pos = strrpos($hook, '__')) { Chris@0: $hook = substr($hook, 0, $pos); Chris@0: if ($theme_registry->has($hook)) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: if (!$theme_registry->has($hook)) { Chris@0: // Only log a message when not trying theme suggestions ($hook being an Chris@0: // array). Chris@0: if (!isset($candidate)) { Chris@0: \Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]); Chris@0: } Chris@0: // There is no theme implementation for the hook passed. Return FALSE so Chris@0: // the function calling Chris@0: // \Drupal\Core\Theme\ThemeManagerInterface::render() can differentiate Chris@0: // between a hook that exists and renders an empty string, and a hook Chris@0: // that is not implemented. Chris@0: return FALSE; Chris@0: } Chris@0: } Chris@0: Chris@0: $info = $theme_registry->get($hook); Chris@0: Chris@0: // If a renderable array is passed as $variables, then set $variables to Chris@0: // the arguments expected by the theme function. Chris@0: if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) { Chris@0: $element = $variables; Chris@0: $variables = []; Chris@0: if (isset($info['variables'])) { Chris@0: foreach (array_keys($info['variables']) as $name) { Chris@0: if (isset($element["#$name"]) || array_key_exists("#$name", $element)) { Chris@0: $variables[$name] = $element["#$name"]; Chris@0: } Chris@0: } Chris@0: } Chris@0: else { Chris@0: $variables[$info['render element']] = $element; Chris@0: // Give a hint to render engines to prevent infinite recursion. Chris@0: $variables[$info['render element']]['#render_children'] = TRUE; Chris@0: } Chris@0: } Chris@0: Chris@0: // Merge in argument defaults. Chris@0: if (!empty($info['variables'])) { Chris@0: $variables += $info['variables']; Chris@0: } Chris@0: elseif (!empty($info['render element'])) { Chris@0: $variables += [$info['render element'] => []]; Chris@0: } Chris@0: // Supply original caller info. Chris@0: $variables += [ Chris@0: 'theme_hook_original' => $original_hook, Chris@0: ]; Chris@0: Chris@0: // Set base hook for later use. For example if '#theme' => 'node__article' Chris@0: // is called, we run hook_theme_suggestions_node_alter() rather than Chris@0: // hook_theme_suggestions_node__article_alter(), and also pass in the base Chris@0: // hook as the last parameter to the suggestions alter hooks. Chris@0: if (isset($info['base hook'])) { Chris@0: $base_theme_hook = $info['base hook']; Chris@0: } Chris@0: else { Chris@0: $base_theme_hook = $hook; Chris@0: } Chris@0: Chris@0: // Invoke hook_theme_suggestions_HOOK(). Chris@0: $suggestions = $this->moduleHandler->invokeAll('theme_suggestions_' . $base_theme_hook, [$variables]); Chris@0: // If the theme implementation was invoked with a direct theme suggestion Chris@0: // like '#theme' => 'node__article', add it to the suggestions array before Chris@0: // invoking suggestion alter hooks. Chris@0: if (isset($info['base hook'])) { Chris@0: $suggestions[] = $hook; Chris@0: } Chris@0: Chris@0: // Invoke hook_theme_suggestions_alter() and Chris@0: // hook_theme_suggestions_HOOK_alter(). Chris@0: $hooks = [ Chris@0: 'theme_suggestions', Chris@0: 'theme_suggestions_' . $base_theme_hook, Chris@0: ]; Chris@0: $this->moduleHandler->alter($hooks, $suggestions, $variables, $base_theme_hook); Chris@0: $this->alter($hooks, $suggestions, $variables, $base_theme_hook); Chris@0: Chris@0: // Check if each suggestion exists in the theme registry, and if so, Chris@0: // use it instead of the base hook. For example, a function may use Chris@0: // '#theme' => 'node', but a module can add 'node__article' as a suggestion Chris@0: // via hook_theme_suggestions_HOOK_alter(), enabling a theme to have Chris@0: // an alternate template file for article nodes. Chris@0: foreach (array_reverse($suggestions) as $suggestion) { Chris@0: if ($theme_registry->has($suggestion)) { Chris@0: $info = $theme_registry->get($suggestion); Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: // Include a file if the theme function or variable preprocessor is held Chris@0: // elsewhere. Chris@0: if (!empty($info['includes'])) { Chris@0: foreach ($info['includes'] as $include_file) { Chris@0: include_once $this->root . '/' . $include_file; Chris@0: } Chris@0: } Chris@0: Chris@0: // Invoke the variable preprocessors, if any. Chris@0: if (isset($info['base hook'])) { Chris@0: $base_hook = $info['base hook']; Chris@0: $base_hook_info = $theme_registry->get($base_hook); Chris@0: // Include files required by the base hook, since its variable Chris@0: // preprocessors might reside there. Chris@0: if (!empty($base_hook_info['includes'])) { Chris@0: foreach ($base_hook_info['includes'] as $include_file) { Chris@0: include_once $this->root . '/' . $include_file; Chris@0: } Chris@0: } Chris@0: if (isset($base_hook_info['preprocess functions'])) { Chris@0: // Set a variable for the 'theme_hook_suggestion'. This is used to Chris@0: // maintain backwards compatibility with template engines. Chris@0: $theme_hook_suggestion = $hook; Chris@0: } Chris@0: } Chris@0: if (isset($info['preprocess functions'])) { Chris@0: foreach ($info['preprocess functions'] as $preprocessor_function) { Chris@0: if (function_exists($preprocessor_function)) { Chris@0: $preprocessor_function($variables, $hook, $info); Chris@0: } Chris@0: } Chris@0: // Allow theme preprocess functions to set $variables['#attached'] and Chris@0: // $variables['#cache'] and use them like the corresponding element Chris@0: // properties on render arrays. In Drupal 8, this is the (only) officially Chris@0: // supported method of attaching bubbleable metadata from preprocess Chris@0: // functions. Assets attached here should be associated with the template Chris@0: // that we are preprocessing variables for. Chris@0: $preprocess_bubbleable = []; Chris@0: foreach (['#attached', '#cache'] as $key) { Chris@0: if (isset($variables[$key])) { Chris@0: $preprocess_bubbleable[$key] = $variables[$key]; Chris@0: } Chris@0: } Chris@0: // We do not allow preprocess functions to define cacheable elements. Chris@0: unset($preprocess_bubbleable['#cache']['keys']); Chris@0: if ($preprocess_bubbleable) { Chris@0: // @todo Inject the Renderer in https://www.drupal.org/node/2529438. Chris@0: \Drupal::service('renderer')->render($preprocess_bubbleable); Chris@0: } Chris@0: } Chris@0: Chris@0: // Generate the output using either a function or a template. Chris@0: $output = ''; Chris@0: if (isset($info['function'])) { Chris@0: if (function_exists($info['function'])) { Chris@0: // Theme functions do not render via the theme engine, so the output is Chris@0: // not autoescaped. However, we can only presume that the theme function Chris@0: // has been written correctly and that the markup is safe. Chris@0: $output = Markup::create($info['function']($variables)); Chris@0: } Chris@0: } Chris@0: else { Chris@0: $render_function = 'twig_render_template'; Chris@0: $extension = '.html.twig'; Chris@0: Chris@0: // The theme engine may use a different extension and a different Chris@0: // renderer. Chris@0: $theme_engine = $active_theme->getEngine(); Chris@0: if (isset($theme_engine)) { Chris@0: if ($info['type'] != 'module') { Chris@0: if (function_exists($theme_engine . '_render_template')) { Chris@0: $render_function = $theme_engine . '_render_template'; Chris@0: } Chris@0: $extension_function = $theme_engine . '_extension'; Chris@0: if (function_exists($extension_function)) { Chris@0: $extension = $extension_function(); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // In some cases, a template implementation may not have had Chris@0: // template_preprocess() run (for example, if the default implementation Chris@0: // is a function, but a template overrides that default implementation). Chris@0: // In these cases, a template should still be able to expect to have Chris@0: // access to the variables provided by template_preprocess(), so we add Chris@0: // them here if they don't already exist. We don't want the overhead of Chris@0: // running template_preprocess() twice, so we use the 'directory' variable Chris@0: // to determine if it has already run, which while not completely Chris@0: // intuitive, is reasonably safe, and allows us to save on the overhead of Chris@0: // adding some new variable to track that. Chris@0: if (!isset($variables['directory'])) { Chris@0: $default_template_variables = []; Chris@0: template_preprocess($default_template_variables, $hook, $info); Chris@0: $variables += $default_template_variables; Chris@0: } Chris@0: if (!isset($default_attributes)) { Chris@0: $default_attributes = new Attribute(); Chris@0: } Chris@0: foreach (['attributes', 'title_attributes', 'content_attributes'] as $key) { Chris@0: if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) { Chris@0: if ($variables[$key]) { Chris@0: $variables[$key] = new Attribute($variables[$key]); Chris@0: } Chris@0: else { Chris@0: // Create empty attributes. Chris@0: $variables[$key] = clone $default_attributes; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Render the output using the template file. Chris@0: $template_file = $info['template'] . $extension; Chris@0: if (isset($info['path'])) { Chris@0: $template_file = $info['path'] . '/' . $template_file; Chris@0: } Chris@0: // Add the theme suggestions to the variables array just before rendering Chris@0: // the template for backwards compatibility with template engines. Chris@0: $variables['theme_hook_suggestions'] = $suggestions; Chris@0: // For backwards compatibility, pass 'theme_hook_suggestion' on to the Chris@0: // template engine. This is only set when calling a direct suggestion like Chris@0: // '#theme' => 'menu__shortcut_default' when the template exists in the Chris@0: // current theme. Chris@0: if (isset($theme_hook_suggestion)) { Chris@0: $variables['theme_hook_suggestion'] = $theme_hook_suggestion; Chris@0: } Chris@0: $output = $render_function($template_file, $variables); Chris@0: } Chris@0: Chris@0: return ($output instanceof MarkupInterface) ? $output : (string) $output; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Initializes the active theme for a given route match. Chris@0: * Chris@0: * @param \Drupal\Core\Routing\RouteMatchInterface $route_match Chris@0: * The current route match. Chris@0: */ Chris@0: protected function initTheme(RouteMatchInterface $route_match = NULL) { Chris@0: // Determine the active theme for the theme negotiator service. This includes Chris@0: // the default theme as well as really specific ones like the ajax base theme. Chris@0: if (!$route_match) { Chris@0: $route_match = \Drupal::routeMatch(); Chris@0: } Chris@0: if ($route_match instanceof StackedRouteMatchInterface) { Chris@0: $route_match = $route_match->getMasterRouteMatch(); Chris@0: } Chris@0: $theme = $this->themeNegotiator->determineActiveTheme($route_match); Chris@0: $this->activeTheme = $this->themeInitialization->initTheme($theme); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: * Chris@0: * @todo Should we cache some of these information? Chris@0: */ Chris@0: public function alterForTheme(ActiveTheme $theme, $type, &$data, &$context1 = NULL, &$context2 = NULL) { Chris@0: // Most of the time, $type is passed as a string, so for performance, Chris@0: // normalize it to that. When passed as an array, usually the first item in Chris@0: // the array is a generic type, and additional items in the array are more Chris@0: // specific variants of it, as in the case of array('form', 'form_FORM_ID'). Chris@0: if (is_array($type)) { Chris@0: $extra_types = $type; Chris@0: $type = array_shift($extra_types); Chris@0: // Allow if statements in this function to use the faster isset() rather Chris@0: // than !empty() both when $type is passed as a string, or as an array with Chris@0: // one item. Chris@0: if (empty($extra_types)) { Chris@0: unset($extra_types); Chris@0: } Chris@0: } Chris@0: Chris@18: $theme_keys = array_keys($theme->getBaseThemeExtensions()); Chris@0: $theme_keys[] = $theme->getName(); Chris@0: $functions = []; Chris@0: foreach ($theme_keys as $theme_key) { Chris@0: $function = $theme_key . '_' . $type . '_alter'; Chris@0: if (function_exists($function)) { Chris@0: $functions[] = $function; Chris@0: } Chris@0: if (isset($extra_types)) { Chris@0: foreach ($extra_types as $extra_type) { Chris@0: $function = $theme_key . '_' . $extra_type . '_alter'; Chris@0: if (function_exists($function)) { Chris@0: $functions[] = $function; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($functions as $function) { Chris@0: $function($data, $context1, $context2); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { Chris@0: $theme = $this->getActiveTheme(); Chris@0: $this->alterForTheme($theme, $type, $data, $context1, $context2); Chris@0: } Chris@0: Chris@0: }