function ThemeManager::render

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

Generates themed output.

See the Default theme implementations topic for details.

Parameters

string|string[] $hook: The name of the theme hook to call or an array of names of theme hooks to call.

array $variables: An associative array of theme variables.

Return value

string|\Drupal\Component\Render\MarkupInterface The rendered output, or a Markup object.

Overrides ThemeManagerInterface::render

File

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

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();
  $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);
  $invoke_map = $theme_registry->getPreprocessInvokes();
  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);
  $deprecated_suggestions = [];
  if (isset($suggestions['__DEPRECATED'])) {
    $deprecated_suggestions = $suggestions['__DEPRECATED'];
    unset($suggestions['__DEPRECATED']);
  }
  // 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);
      if (isset($deprecated_suggestions[$suggestion])) {
        @trigger_error($deprecated_suggestions[$suggestion], E_USER_DEPRECATED);
      }
      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;
    }
  }
  // Set default variables before preprocess hooks.
  $variables += $this->getDefaultTemplateVariables();
  // When theming a render element, merge its #attributes into
  // $variables['attributes'].
  if (isset($info['render element'])) {
    $key = $info['render element'];
    if (isset($variables[$key]['#attributes'])) {
      $variables['attributes'] = AttributeHelper::mergeCollections($variables['attributes'], $variables[$key]['#attributes']);
    }
  }
  // Invoke initial preprocess callbacks.
  if (!empty($info['initial preprocess'])) {
    $callable = $info['initial preprocess'];
    try {
      if (!is_callable($callable)) {
        $callable = $this->callableResolver
          ->getCallableFromDefinition($callable);
      }
      $callable($variables, $hook, $info);
    } catch (\InvalidArgumentException $e) {
      \Drupal::logger('theme')->warning('Preprocess callback is not valid: %error.', [
        '%error' => $e->getMessage(),
      ]);
    }
  }
  $invoke_preprocess_callback = function (mixed $preprocessor_function) use ($invoke_map, &$variables, $hook, $info) : mixed {
    // Preprocess hooks are stored as strings resembling functions.
    // This is for backwards compatibility and may represent OOP
    // implementations as well.
    if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) {
      // The invoke map has either a module or a theme key.
      if (isset($invoke_map[$preprocessor_function]['module'])) {
        // Invoke module preprocess functions.
        $handler = $this->moduleHandler;
        $name = $invoke_map[$preprocessor_function]['module'];
        $invoke_hook = $invoke_map[$preprocessor_function]['hook'];
      }
      else {
        // Invoke theme preprocess functions.
        $handler = $this;
        $name = $invoke_map[$preprocessor_function]['theme'];
        $invoke_hook = $invoke_map[$preprocessor_function]['hook'];
      }
      $handler->invoke($name, $invoke_hook, [
        &$variables,
        $hook,
        $info,
      ]);
    }
    elseif (is_callable($preprocessor_function)) {
      call_user_func_array($preprocessor_function, [
        &$variables,
        $hook,
        $info,
      ]);
    }
    return $variables;
  };
  // Global preprocess functions are always called, after initial and
  // template preprocess and before regular module and theme preprocess
  // callbacks. template preprocess callbacks are deprecated but still
  // supported, so they need to be called before the first non-template
  // preprocess callback, and if that doesn't happen, after the loop.
  $global_preprocess = $theme_registry->getGlobalPreprocess();
  $global_preprocess_called = FALSE;
  // Invoke preprocess hooks.
  if (isset($info['preprocess functions'])) {
    foreach ($info['preprocess functions'] as $preprocessor_function) {
      // If global preprocess functions have not been called yet and this is
      // not a template preprocess function, invoke them now.
      if (!$global_preprocess_called && is_string($preprocessor_function) && !str_starts_with($preprocessor_function, 'template_')) {
        $global_preprocess_called = TRUE;
        foreach ($global_preprocess as $global_preprocess_callback) {
          $invoke_preprocess_callback($global_preprocess_callback);
        }
      }
      $invoke_preprocess_callback($preprocessor_function);
    }
  }
  // If global process hasn't been invoked yet, do that now.
  if (!$global_preprocess_called) {
    foreach ($global_preprocess as $global_preprocess_callback) {
      $invoke_preprocess_callback($global_preprocess_callback);
    }
  }
  // Allow theme preprocess functions to set $variables['#attached'] and
  // $variables['#cache'] and use them like the corresponding element
  // properties on render arrays. This is the 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);
  }
  // Module provided templates must use the Twig engine.
  if ($info['type'] != 'module') {
    $theme_engine = $active_theme->getEngine();
  }
  else {
    $theme_engine = 'twig';
  }
  if ($theme_engine_service = $this->getThemeEngine($theme_engine)) {
    $render_function = [
      $theme_engine_service,
      'renderTemplate',
    ];
    $extension = '';
  }
  else {
    // @todo Remove in Drupal 12 in https://www.drupal.org/project/drupal/issues/3555931
    $render_function = $theme_engine . '_render_template';
    if (!function_exists($render_function)) {
      $render_function = 'twig_render_template';
    }
    $extension_function = $theme_engine . '_extension';
    if (function_exists($extension_function)) {
      $extension = $extension_function();
    }
    else {
      $extension = '.html.twig';
    }
  }
  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 to expose it to the template engine, for example to
  // display debug information.
  $variables['theme_hook_suggestions'] = $suggestions;
  $variables['theme_hook_suggestions__DEPRECATED'] = $deprecated_suggestions;
  // 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;
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.