Same name and namespace in other branches
  1. 8.9.x core/lib/Drupal/Core/Theme/ThemeManager.php \Drupal\Core\Theme\ThemeManager::render()
  2. 9 core/lib/Drupal/Core/Theme/ThemeManager.php \Drupal\Core\Theme\ThemeManager::render()

File

core/lib/Drupal/Core/Theme/ThemeManager.php, line 129

Class

ThemeManager
Provides the default implementation of a theme manager.

Namespace

Drupal\Core\Theme

Code

public function render($hook, array $variables) {
  static $default_attributes;
  $active_theme = $this
    ->getActiveTheme();

  // If called before all modules are loaded, we do not necessarily have a
  // full theme registry to work with, and therefore cannot process the theme
  // request properly. See also \Drupal\Core\Theme\Registry::get().
  if (!$this->moduleHandler
    ->isLoaded() && !defined('MAINTENANCE_MODE')) {
    throw new \Exception('The theme implementations may not be rendered until all modules are loaded.');
  }
  $theme_registry = $this->themeRegistry
    ->getRuntime();

  // If an array of hook candidates were passed, use the first one that has an
  // implementation.
  if (is_array($hook)) {
    foreach ($hook as $candidate) {
      if ($theme_registry
        ->has($candidate)) {
        break;
      }
    }
    $hook = $candidate;
  }

  // Save the original theme hook, so it can be supplied to theme variable
  // preprocess callbacks.
  $original_hook = $hook;

  // If there's no implementation, check for more generic fallbacks.
  // If there's still no implementation, log an error and return an empty
  // string.
  if (!$theme_registry
    ->has($hook)) {

    // Iteratively strip everything after the last '__' delimiter, until an
    // implementation is found.
    while ($pos = strrpos($hook, '__')) {
      $hook = substr($hook, 0, $pos);
      if ($theme_registry
        ->has($hook)) {
        break;
      }
    }
    if (!$theme_registry
      ->has($hook)) {

      // Only log a message when not trying theme suggestions ($hook being an
      // array).
      if (!isset($candidate)) {
        \Drupal::logger('theme')
          ->warning('Theme hook %hook not found.', [
          '%hook' => $hook,
        ]);
      }

      // There is no theme implementation for the hook passed. Return FALSE so
      // the function calling
      // \Drupal\Core\Theme\ThemeManagerInterface::render() can differentiate
      // between a hook that exists and renders an empty string, and a hook
      // that is not implemented.
      return FALSE;
    }
  }
  $info = $theme_registry
    ->get($hook);
  if (isset($info['deprecated'])) {
    @trigger_error($info['deprecated'], E_USER_DEPRECATED);
  }

  // If a renderable array is passed as $variables, then set $variables to
  // the arguments expected by the theme function.
  if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
    $element = $variables;
    $variables = [];
    if (isset($info['variables'])) {
      foreach (array_keys($info['variables']) as $name) {
        if (\array_key_exists("#{$name}", $element)) {
          $variables[$name] = $element["#{$name}"];
        }
      }
    }
    else {
      $variables[$info['render element']] = $element;

      // Give a hint to render engines to prevent infinite recursion.
      $variables[$info['render element']]['#render_children'] = TRUE;
    }
  }

  // Merge in argument defaults.
  if (!empty($info['variables'])) {
    $variables += $info['variables'];
  }
  elseif (!empty($info['render element'])) {
    $variables += [
      $info['render element'] => [],
    ];
  }

  // Supply original caller info.
  $variables += [
    'theme_hook_original' => $original_hook,
  ];
  $suggestions = $this
    ->buildThemeHookSuggestions($hook, $info['base hook'] ?? '', $variables);

  // Check if each suggestion exists in the theme registry, and if so,
  // use it instead of the base hook. For example, a function may use
  // '#theme' => 'node', but a module can add 'node__article' as a suggestion
  // via hook_theme_suggestions_HOOK_alter(), enabling a theme to have
  // an alternate template file for article nodes.
  foreach (array_reverse($suggestions) as $suggestion) {
    if ($theme_registry
      ->has($suggestion)) {
      $info = $theme_registry
        ->get($suggestion);
      break;
    }
  }

  // Include a file if the variable preprocessor is held elsewhere.
  if (!empty($info['includes'])) {
    foreach ($info['includes'] as $include_file) {
      include_once $this->root . '/' . $include_file;
    }
  }

  // Invoke the variable preprocessors, if any.
  if (isset($info['base hook'])) {
    $base_hook = $info['base hook'];
    $base_hook_info = $theme_registry
      ->get($base_hook);

    // Include files required by the base hook, since its variable
    // preprocessors might reside there.
    if (!empty($base_hook_info['includes'])) {
      foreach ($base_hook_info['includes'] as $include_file) {
        include_once $this->root . '/' . $include_file;
      }
    }
    if (isset($base_hook_info['preprocess functions'])) {

      // Set a variable for the 'theme_hook_suggestion'. This is used to
      // maintain backwards compatibility with template engines.
      $theme_hook_suggestion = $hook;
    }
  }
  if (isset($info['preprocess functions'])) {
    foreach ($info['preprocess functions'] as $preprocessor_function) {
      if (is_callable($preprocessor_function)) {
        call_user_func_array($preprocessor_function, [
          &$variables,
          $hook,
          $info,
        ]);
      }
    }

    // Allow theme preprocess functions to set $variables['#attached'] and
    // $variables['#cache'] and use them like the corresponding element
    // properties on render arrays. In Drupal 8, this is the (only) officially
    // supported method of attaching bubbleable metadata from preprocess
    // functions. Assets attached here should be associated with the template
    // that we are preprocessing variables for.
    $preprocess_bubbleable = [];
    foreach ([
      '#attached',
      '#cache',
    ] as $key) {
      if (isset($variables[$key])) {
        $preprocess_bubbleable[$key] = $variables[$key];
      }
    }

    // We do not allow preprocess functions to define cacheable elements.
    unset($preprocess_bubbleable['#cache']['keys']);
    if ($preprocess_bubbleable) {

      // @todo Inject the Renderer in https://www.drupal.org/node/2529438.
      \Drupal::service('renderer')
        ->render($preprocess_bubbleable);
    }
  }

  // Generate the output using a template.
  $render_function = 'twig_render_template';
  $extension = '.html.twig';

  // The theme engine may use a different extension and a different
  // renderer.
  $theme_engine = $active_theme
    ->getEngine();
  if (isset($theme_engine)) {
    if ($info['type'] != 'module') {
      if (function_exists($theme_engine . '_render_template')) {
        $render_function = $theme_engine . '_render_template';
      }
      $extension_function = $theme_engine . '_extension';
      if (function_exists($extension_function)) {
        $extension = $extension_function();
      }
    }
  }

  // In some cases, a template implementation may not have had
  // template_preprocess() run (for example, if the default implementation
  // is a function, but a template overrides that default implementation).
  // In these cases, a template should still be able to expect to have
  // access to the variables provided by template_preprocess(), so we add
  // them here if they don't already exist. We don't want the overhead of
  // running template_preprocess() twice, so we use the 'directory' variable
  // to determine if it has already run, which while not completely
  // intuitive, is reasonably safe, and allows us to save on the overhead of
  // adding some new variable to track that.
  if (!isset($variables['directory'])) {
    $default_template_variables = [];
    template_preprocess($default_template_variables, $hook, $info);
    $variables += $default_template_variables;
  }
  if (!isset($default_attributes)) {
    $default_attributes = new Attribute();
  }
  foreach ([
    'attributes',
    'title_attributes',
    'content_attributes',
  ] as $key) {
    if (isset($variables[$key]) && !$variables[$key] instanceof Attribute) {
      if ($variables[$key]) {
        $variables[$key] = new Attribute($variables[$key]);
      }
      else {

        // Create empty attributes.
        $variables[$key] = clone $default_attributes;
      }
    }
  }

  // Render the output using the template file.
  $template_file = $info['template'] . $extension;
  if (isset($info['path'])) {
    $template_file = $info['path'] . '/' . $template_file;
  }

  // Add the theme suggestions to the variables array just before rendering
  // the template for backwards compatibility with template engines.
  $variables['theme_hook_suggestions'] = $suggestions;

  // For backwards compatibility, pass 'theme_hook_suggestion' on to the
  // template engine. This is only set when calling a direct suggestion like
  // '#theme' => 'menu__shortcut_default' when the template exists in the
  // current theme.
  if (isset($theme_hook_suggestion)) {
    $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
  }
  $output = $render_function($template_file, $variables);
  return $output instanceof MarkupInterface ? $output : (string) $output;
}