Commit da8ea3bf authored by alexpott's avatar alexpott

Issue #2346937 by dawehner, larowlan, Wim Leers, claudiu.cristea, msonnabaum:...

Issue #2346937 by dawehner, larowlan, Wim Leers, claudiu.cristea, msonnabaum: Implement a Renderer service; reduces drupal_render / _theme service container calls
parent 040dc5ff
......@@ -667,7 +667,7 @@ services:
- { name: event_subscriber }
main_content_renderer.html:
class: Drupal\Core\Render\MainContent\HtmlRenderer
arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler']
arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer']
tags:
- { name: render.main_content_renderer, format: html }
main_content_renderer.ajax:
......@@ -700,6 +700,7 @@ services:
arguments: ['@content_negotiation', '@title_resolver']
bare_html_page_renderer:
class: Drupal\Core\Render\BareHtmlPageRenderer
arguments: ['@renderer']
private_key:
class: Drupal\Core\PrivateKey
arguments: ['@state']
......@@ -994,7 +995,7 @@ services:
class: Zend\Feed\Writer\Extension\WellFormedWeb\Renderer\Entry
theme.manager:
class: Drupal\Core\Theme\ThemeManager
arguments: ['@theme.registry', '@theme.negotiator', '@theme.initialization', '@request_stack']
arguments: ['@app.root', '@theme.registry', '@theme.negotiator', '@theme.initialization', '@request_stack', '@module_handler']
theme.initialization:
class: Drupal\Core\Theme\ThemeInitialization
arguments: ['@app.root', '@theme_handler', '@state']
......@@ -1097,3 +1098,6 @@ services:
arguments: ['@module_handler']
tags:
- { name: mime_type_guesser }
renderer:
class: Drupal\Core\Render\Renderer
arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info']
This diff is collapsed.
......@@ -156,307 +156,6 @@ function list_themes($refresh = FALSE) {
return $theme_handler->listInfo();
}
/**
* Generates themed output (internal use only).
*
* _theme() is an internal function. Do not call this function directly as it
* will prevent the following items from working correctly:
* - Render caching.
* - JavaScript and CSS asset attachment.
* - Pre / post render hooks.
* - Defaults provided by hook_element_info(), including attached assets.
* Instead, build a render array with a #theme key, and either return the
* array (where possible) or call drupal_render() to convert it to HTML.
*
* All requests for themed output must go through this function, which is
* invoked as part of the @link theme_render drupal_render() process @endlink.
* The appropriate theme function is indicated by the #theme property
* of a renderable array. _theme() examines the request and routes it to the
* appropriate @link themeable theme function or template @endlink, by checking
* the theme registry.
*
* @param $hook
* The name of the theme hook to call. If the name contains a
* double-underscore ('__') and there isn't an implementation for the full
* name, the part before the '__' is checked. This allows a fallback to a
* more generic implementation. For example, if _theme('links__node', ...) is
* called, but there is no implementation of that theme hook, then the
* 'links' implementation is used. This process is iterative, so if
* _theme('links__contextual__node', ...) is called, _theme() checks for the
* following implementations, and uses the first one that exists:
* - links__contextual__node
* - links__contextual
* - links
* This allows themes to create specific theme implementations for named
* objects and contexts of otherwise generic theme hooks. The $hook parameter
* may also be an array, in which case the first theme hook that has an
* implementation is used. This allows for the code that calls _theme() to
* explicitly specify the fallback order in a situation where using the '__'
* convention is not desired or is insufficient.
* @param $variables
* An associative array of variables to merge with defaults from the theme
* registry, pass to preprocess functions for modification, and finally, pass
* to the function or template implementing the theme hook. Alternatively,
* this can be a renderable array, in which case, its properties are mapped to
* variables expected by the theme hook implementations.
*
* @return string|false
* An HTML string representing the themed output or FALSE if the passed $hook
* is not implemented.
*
* @see drupal_render()
* @see themeable
* @see hook_theme()
* @see template_preprocess()
*/
function _theme($hook, $variables = array()) {
static $default_attributes;
$module_handler = \Drupal::moduleHandler();
$active_theme = \Drupal::theme()->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 (!$module_handler->isLoaded() && !defined('MAINTENANCE_MODE')) {
throw new Exception(t('_theme() may not be called until all modules are loaded.'));
}
/** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */
$theme_registry = \Drupal::service('theme.registry')->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.', array('%hook' => $hook));
}
// There is no theme implementation for the hook passed. Return FALSE so
// the function calling _theme() 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 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 = array();
if (isset($info['variables'])) {
foreach (array_keys($info['variables']) as $name) {
if (isset($element["#$name"]) || 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 += array($info['render element'] => array());
}
// Supply original caller info.
$variables += array(
'theme_hook_original' => $original_hook,
);
// Set base hook for later use. For example if '#theme' => 'node__article'
// is called, we run hook_theme_suggestions_node_alter() rather than
// hook_theme_suggestions_node__article_alter(), and also pass in the base
// hook as the last parameter to the suggestions alter hooks.
if (isset($info['base hook'])) {
$base_theme_hook = $info['base hook'];
}
else {
$base_theme_hook = $hook;
}
// Invoke hook_theme_suggestions_HOOK().
$suggestions = $module_handler->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
// If _theme() was invoked with a direct theme suggestion like
// '#theme' => 'node__article', add it to the suggestions array before
// invoking suggestion alter hooks.
if (isset($info['base hook'])) {
$suggestions[] = $hook;
}
// Invoke hook_theme_suggestions_alter() and
// hook_theme_suggestions_HOOK_alter().
$hooks = array(
'theme_suggestions',
'theme_suggestions_' . $base_theme_hook,
);
$module_handler->alter($hooks, $suggestions, $variables, $base_theme_hook);
\Drupal::theme()->alter($hooks, $suggestions, $variables, $base_theme_hook);
// Check if each suggestion exists in the theme registry, and if so,
// use it instead of the hook that _theme() was called with. For example, a
// function may call _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 theme function or variable preprocessor is held
// elsewhere.
if (!empty($info['includes'])) {
foreach ($info['includes'] as $include_file) {
include_once \Drupal::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 \Drupal::root() . '/' . $include_file;
}
}
// Replace the preprocess functions with those from the base hook.
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;
$info['preprocess functions'] = $base_hook_info['preprocess functions'];
}
}
if (isset($info['preprocess functions'])) {
foreach ($info['preprocess functions'] as $preprocessor_function) {
if (function_exists($preprocessor_function)) {
$preprocessor_function($variables, $hook, $info);
}
}
// Allow theme preprocess functions to set $variables['#attached'] and use
// it like the #attached property on render arrays. In Drupal 8, this is the
// (only) officially supported method of attaching assets from preprocess
// functions. Assets attached here should be associated with the template
// that we're preprocessing variables for.
if (isset($variables['#attached'])) {
$preprocess_attached = ['#attached' => $variables['#attached']];
drupal_render($preprocess_attached);
}
}
// Generate the output using either a function or a template.
$output = '';
if (isset($info['function'])) {
if (function_exists($info['function'])) {
$output = SafeMarkup::set($info['function']($variables));
}
}
else {
$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 = array();
template_preprocess($default_template_variables, $hook, $info);
$variables += $default_template_variables;
}
if (!isset($default_attributes)) {
$default_attributes = new Attribute();
}
foreach (array('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 (string) $output;
}
/**
* Allows themes and/or theme engines to discover overridden theme functions.
*
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -25,14 +26,24 @@ class EntityViewController implements ContainerInjectionInterface {
*/
protected $entityManager;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Creates an EntityViewController object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(EntityManagerInterface $entity_manager) {
public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer) {
$this->entityManager = $entity_manager;
$this->renderer = $renderer;
}
/**
......@@ -40,7 +51,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')
$container->get('entity.manager'),
$container->get('renderer')
);
}
......@@ -79,7 +91,7 @@ public function view(EntityInterface $_entity, $view_mode = 'full', $langcode =
$build = $this->entityManager->getTranslationFromContext($_entity)
->get($label_field)
->view($view_mode);
$page['#title'] = drupal_render($build);
$page['#title'] = $this->renderer->render($build);
}
}
......
......@@ -12,6 +12,23 @@
*/
class BareHtmlPageRenderer implements BareHtmlPageRendererInterface {
/**
* The renderer service.
*
* @var \Drupal\Core\Render\Renderer
*/
protected $renderer;
/**
* Constructs a new BareHtmlPageRenderer.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(RendererInterface $renderer) {
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
......@@ -36,12 +53,12 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr
// \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() for more
// information about this; the exact same pattern is used there and
// explained in detail there.
drupal_render_root($html['page']);
$this->renderer->render($html['page'], TRUE);
// Add the bare minimum of attachments from the system module and the
// current maintenance theme.
system_page_attachments($html['page']);
return drupal_render($html);
return $this->renderer->render($html);
}
}
......@@ -14,6 +14,7 @@
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderEvents;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
......@@ -54,6 +55,13 @@ class HtmlRenderer implements MainContentRendererInterface {
*/
protected $moduleHandler;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new HtmlRenderer.
*
......@@ -65,12 +73,15 @@ class HtmlRenderer implements MainContentRendererInterface {
* The event dispatcher.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler) {
public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer) {
$this->titleResolver = $title_resolver;
$this->displayVariantManager = $display_variant_manager;
$this->eventDispatcher = $event_dispatcher;
$this->moduleHandler = $module_handler;
$this->renderer = $renderer;
}
/**
......@@ -108,14 +119,14 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
// and hence may not execute any #post_render_cache_callbacks (because they
// might add yet more assets to be attached), and therefore it must be
// rendered with drupal_render(), not drupal_render_root().
drupal_render_root($html['page']);
$this->renderer->render($html['page'], TRUE);
if (isset($html['page_top'])) {
drupal_render_root($html['page_top']);
$this->renderer->render($html['page_top'], TRUE);
}
if (isset($html['page_bottom'])) {
drupal_render_root($html['page_bottom']);
$this->renderer->render($html['page_bottom'], TRUE);
}
$content = drupal_render($html);
$content = $this->renderer->render($html);
// Store the cache tags associated with this page in a X-Drupal-Cache-Tags
// header. Also associate the "rendered" cache tag. This allows us to
......@@ -177,7 +188,7 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
// ::renderContentIntoResponse().
// @todo Remove this once https://www.drupal.org/node/2359901 lands.
if (!empty($main_content)) {
drupal_render($main_content, FALSE);
$this->renderer->render($main_content, FALSE);
$main_content = [
'#markup' => $main_content['#markup'],
'#attached' => $main_content['#attached'],
......
<?php
/**
* @file
* Contains \Drupal\Core\Render\Renderer.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Component\Utility\NestedArray;
/**
* Turns a render array into a HTML string.
*/
class Renderer implements RendererInterface {
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $theme;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The element info.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* Constructs a new Renderer.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme
* The theme manager.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info) {
$this->controllerResolver = $controller_resolver;
$this->theme = $theme;
$this->elementInfo = $element_info;
}
/**
* {@inheritdoc}
*/
public function renderRoot(&$elements) {
return $this->render($elements, TRUE);
}
/**
* {@inheritdoc}
*/
public function render(&$elements, $is_root_call = FALSE) {
static $stack;
$update_stack = function(&$element) use (&$stack) {
// The latest frame represents the bubbleable data for the subtree.
$frame = $stack->top();
// Update the frame, but also update the current element, to ensure it
// contains up-to-date information in case it gets render cached.
$frame->tags = $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $frame->tags);
$frame->attached = $element['#attached'] = drupal_merge_attached($element['#attached'], $frame->attached);
$frame->postRenderCache = $element['#post_render_cache'] = NestedArray::mergeDeep($element['#post_render_cache'], $frame->postRenderCache);
};
$bubble_stack = function() use (&$stack) {
// If there's only one frame on the stack, then this is the root call, and
// we can't bubble up further. Reset the stack for the next root call.
if ($stack->count() === 1) {
$stack = NULL;
return;
}
// Merge the current and the parent stack frame.
$current = $stack->pop();
$parent = $stack->pop();
$current->tags = Cache::mergeTags($current->tags, $parent->tags);
$current->attached = drupal_merge_attached($current->attached, $parent->attached);
$current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
$stack->push($current);
};
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
$elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
}
$elements['#access'] = call_user_func($elements['#access_callback'], $elements);
}
// Early-return nothing if user does not have access.
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
return '';
}
// Do not print elements twice.
if (!empty($elements['#printed'])) {
return '';
}