From da8ea3bfaa86cae97d5765968f696875c259f137 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Fri, 21 Nov 2014 09:48:25 +0000 Subject: [PATCH] Issue #2346937 by dawehner, larowlan, Wim Leers, claudiu.cristea, msonnabaum: Implement a Renderer service; reduces drupal_render / _theme service container calls --- core/core.services.yml | 8 +- core/includes/common.inc | 542 +----------------- core/includes/theme.inc | 301 ---------- .../Controller/EntityViewController.php | 18 +- .../Core/Render/BareHtmlPageRenderer.php | 21 +- .../Core/Render/MainContent/HtmlRenderer.php | 23 +- core/lib/Drupal/Core/Render/Renderer.php | 390 +++++++++++++ .../Drupal/Core/Render/RendererInterface.php | 244 ++++++++ core/lib/Drupal/Core/Theme/ThemeManager.php | 273 ++++++++- .../node/src/Controller/NodeController.php | 20 +- .../Plugin/views/field/FieldPluginBase.php | 33 +- .../Controller/EntityViewControllerTest.php | 2 +- 12 files changed, 1017 insertions(+), 858 deletions(-) create mode 100644 core/lib/Drupal/Core/Render/Renderer.php create mode 100644 core/lib/Drupal/Core/Render/RendererInterface.php diff --git a/core/core.services.yml b/core/core.services.yml index b0fdda00be91..99ae337fc60b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -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'] diff --git a/core/includes/common.inc b/core/includes/common.inc index 001c9e7a7ba0..d694df10a32a 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -22,7 +22,6 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Render\RenderStackFrame; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; @@ -2365,509 +2364,25 @@ function drupal_pre_render_links($element) { /** * Renders final HTML given a structured array tree. * - * Calls drupal_render() in such a way that #post_render_cache callbacks are - * applied. - * - * Should therefore only be used in occasions where the final rendering is - * happening, just before sending a Response: - * - system internals that are responsible for rendering the final HTML - * - render arrays for non-HTML responses, such as feeds - * - * @param array $elements - * The structured array describing the data to be rendered. - * - * @return string - * The rendered HTML. + * @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use the + * 'renderer' service instead. * - * @see drupal_render() + * @see \Drupal\Core\Render\RendererInterface::renderRoot() */ function drupal_render_root(&$elements) { - return drupal_render($elements, TRUE); + return \Drupal::service('renderer')->renderRoot($elements); } /** * Renders HTML given a structured array tree. * - * Renderable arrays have two kinds of key/value pairs: properties and children. - * Properties have keys starting with '#' and their values influence how the - * array will be rendered. Children are all elements whose keys do not start - * with a '#'. Their values should be renderable arrays themselves, which will - * be rendered during the rendering of the parent array. The markup provided by - * the children is typically inserted into the markup generated by the parent - * array. - * - * An important aspect of rendering is the bubbling of rendering metadata: cache - * tags, attached assets and #post_render_cache metadata all need to be bubbled - * up. That information is needed once the rendering to a HTML string is - * completed: the resulting HTML for the page must know by which cache tags it - * should be invalidated, which (CSS and JavaScript) assets must be loaded, and - * which #post_render_cache callbacks should be executed. A stack data structure - * is used to perform this bubbling. - * - * The process of rendering an element is recursive unless the element defines - * an implemented theme hook in #theme. During each call to drupal_render(), the - * outermost renderable array (also known as an "element") is processed using - * the following steps: - * - If this element has already been printed (#printed = TRUE) or the user - * does not have access to it (#access = FALSE), then an empty string is - * returned. - * - If no stack data structure has been created yet, it is done now. Next, - * an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack. - * - If this element has #cache defined then the cached markup for this - * element will be returned if it exists in drupal_render()'s cache. To use - * drupal_render() caching, set the element's #cache property to an - * associative array with one or several of the following keys: - * - 'keys': An array of one or more keys that identify the element. If - * 'keys' is set, the cache ID is created automatically from these keys. - * Cache keys may either be static (just strings) or tokens (placeholders - * that are converted to static keys by the @cache_contexts service, - * depending on the request). See drupal_render_cid_create(). - * - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is - * required. If 'cid' is set, 'keys' is ignored. Use only if you have - * special requirements. - * - 'expire': Set to one of the cache lifetime constants. - * - 'bin': Specify a cache bin to cache the element in. Default is - * 'default'. - * When there is a render cache hit, there is no rendering work left to be - * done, so the stack must be updated. The empty (and topmost) frame that - * was just pushed onto the stack is updated with all bubbleable rendering - * metadata from the element retrieved from render cache. Then, this stack - * frame is bubbled: the two topmost frames are popped from the stack, they - * are merged, and the result is pushed back onto the stack. - * - If this element has #type defined and the default attributes for this - * element have not already been merged in (#defaults_loaded = TRUE) then - * the defaults for this type of element, defined in hook_element_info(), - * are merged into the array. #defaults_loaded is set by functions that - * process render arrays and call element_info() before passing the array to - * drupal_render(), such as \Drupal::formBuilder()->doBuildForm() in the - * Form API. - * - If this element has an array of #pre_render functions defined, they are - * called sequentially to modify the element before rendering. After all the - * #pre_render functions have been called, #printed is checked a second time - * in case a #pre_render function flags the element as printed. - * If #printed is set, we return early and hence no rendering work is left - * to be done, similarly to a render cache hit. Once again, the empty (and - * topmost) frame that was just pushed onto the stack is updated with all - * bubbleable rendering metadata from the element whose #printed = TRUE. - * Then, this stack frame is bubbled: the two topmost frames are popped from - * the stack, they are merged, and the result is pushed back onto the stack. - * - The child elements of this element are sorted by weight using uasort() in - * \Drupal\Core\Render\Element::children(). Since this is expensive, when - * passing already sorted elements to drupal_render(), for example from a - * database query, set $elements['#sorted'] = TRUE to avoid sorting them a - * second time. - * - The main render phase to produce #children for this element takes place: - * - If this element has #theme defined and #theme is an implemented theme - * hook/suggestion then _theme() is called and must render both the element - * and its children. If #render_children is set, _theme() will not be - * called. #render_children is usually only set internally by _theme() so - * that we can avoid the situation where drupal_render() called from - * within a theme preprocess function creates an infinite loop. - * - If this element does not have a defined #theme, or the defined #theme - * hook is not implemented, or #render_children is set, then - * drupal_render() is called recursively on each of the child elements of - * this element, and the result of each is concatenated onto #children. - * This is skipped if #children is not empty at this point. - * - Once #children has been rendered for this element, if #theme is not - * implemented and #markup is set for this element, #markup will be - * prepended to #children. - * - If this element has #states defined then JavaScript state information is - * added to this element's #attached attribute by drupal_process_states(). - * - If this element has #attached defined then any required libraries, - * JavaScript, CSS, or other custom data are added to the current page by - * drupal_process_attached(). - * - If this element has an array of #theme_wrappers defined and - * #render_children is not set, #children is then re-rendered by passing the - * element in its current state to _theme() successively for each item in - * #theme_wrappers. Since #theme and #theme_wrappers hooks often define - * variables with the same names it is possible to explicitly override each - * attribute passed to each #theme_wrappers hook by setting the hook name as - * the key and an array of overrides as the value in #theme_wrappers array. - * For example, if we have a render element as follows: - * @code - * array( - * '#theme' => 'image', - * '#attributes' => array('class' => array('foo')), - * '#theme_wrappers' => array('container'), - * ); - * @endcode - * and we need to pass the class 'bar' as an attribute for 'container', we - * can rewrite our element thus: - * @code - * array( - * '#theme' => 'image', - * '#attributes' => array('class' => array('foo')), - * '#theme_wrappers' => array( - * 'container' => array( - * '#attributes' => array('class' => array('bar')), - * ), - * ), - * ); - * @endcode - * - If this element has an array of #post_render functions defined, they are - * called sequentially to modify the rendered #children. Unlike #pre_render - * functions, #post_render functions are passed both the rendered #children - * attribute as a string and the element itself. - * - If this element has #prefix and/or #suffix defined, they are concatenated - * to #children. - * - The rendering of this element is now complete. The next step will be - * render caching. So this is the perfect time to update the the stack. At - * this point, children of this element (if any), have been rendered also, - * and if there were any, their bubbleable rendering metadata will have been - * bubbled up into the stack frame for the element that is currently being - * rendered. The render cache item for this element must contain the - * bubbleable rendering metadata for this element and all of its children. - * However, right now, the topmost stack frame (the one for this element) - * currently only contains the metadata for the children. Therefore, the - * topmost stack frame is updated with this element's metadata, and then the - * element's metadata is replaced with the metadata in the topmost stack - * frame. This element now contains all bubbleable rendering metadata for - * this element and all its children, so it's now ready for render caching. - * - If this element has #cache defined, the rendered output of this element - * is saved to drupal_render()'s internal cache. This includes the changes - * made by #post_render. - * - If this element has an array of #post_render_cache functions defined, or - * any of its children has (which we would know thanks to the stack having - * been updated just before the render caching step), they are called - * sequentially to replace placeholders in the final #markup and extend - * #attached. Placeholders must contain a unique token, to guarantee that - * e.g. samples of placeholders are not replaced also. - * But, since #post_render_cache callbacks add attach additional assets, the - * correct bubbling of those must once again be taken into account. This - * final stage of rendering should be considered as if it were the parent of - * the current element, because it takes that as its input, and then alters - * its #markup. Hence, just before calling the #post_render_cache callbacks, - * a new empty frame is pushed onto the stack, where all assets #attached - * during the execution of those callbacks will end up in. Then, after the - * execution of those callbacks, we merge that back into the element. - * Note that these callbacks run always: when hitting the render cache, when - * missing, or when render caching is not used at all. This is done to allow - * any Drupal module to customize other render arrays without breaking the - * render cache if it is enabled, and to not require it to use other logic - * when render caching is disabled. - * - Just before finishing the rendering of this element, this element's stack - * frame (the topmost one) is bubbled: the two topmost frames are popped - * from the stack, they are merged and the result is pushed back onto the - * stack. - * So if this element e.g. was a child element, then a new frame was pushed - * onto the stack element at the beginning of rendering this element, it was - * updated when the rendering was completed, and now we merge it with the - * frame for the parent, so that the parent now has the bubbleable rendering - * metadata for its child. - * - #printed is set to TRUE for this element to ensure that it is only - * rendered once. - * - The final value of #children for this element is returned as the rendered - * output. - * - * @param array $elements - * The structured array describing the data to be rendered. - * @param bool $is_root_call - * (Internal use only.) Whether this is a recursive call or not. See - * drupal_render_root(). - * - * @return string - * The rendered HTML. + * @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use the + * 'renderer' service instead. * - * @throws \LogicException - * If a root call to drupal_render() does not result in an empty stack, this - * indicates an erroneous drupal_render() root call (a root call within a root - * call, which makes no sense). Therefore, a logic exception is thrown. - * @throws \Exception - * If a #pre_render callback throws an exception, it is caught to reset the - * stack used for bubbling rendering metadata, and then the exception is re- - * thrown. - * - * @see element_info() - * @see _theme() - * @see drupal_process_states() - * @see drupal_process_attached() - * @see drupal_render_root() - */ -function drupal_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); - }; - /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */ - $controller_resolver = \Drupal::service('controller_resolver'); - if (!isset($elements['#access']) && isset($elements['#access_callback'])) { - if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { - $elements['#access_callback'] = $controller_resolver->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 ''; - } - - if (!isset($stack)) { - $stack = new \SplStack(); - } - $stack->push(new RenderStackFrame()); - - // Try to fetch the prerendered element from cache, run any #post_render_cache - // callbacks and return the final markup. - if (isset($elements['#cache'])) { - $cached_element = drupal_render_cache_get($elements); - if ($cached_element !== FALSE) { - $elements = $cached_element; - // Only when we're not in a root (non-recursive) drupal_render() call, - // #post_render_cache callbacks must be executed, to prevent breaking the - // render cache in case of nested elements with #cache set. - if ($is_root_call) { - _drupal_render_process_post_render_cache($elements); - } - $elements['#markup'] = SafeMarkup::set($elements['#markup']); - // The render cache item contains all the bubbleable rendering metadata for - // the subtree. - $update_stack($elements); - // Render cache hit, so rendering is finished, all necessary info collected! - $bubble_stack(); - return $elements['#markup']; - } - } - - // If the default values for this element have not been loaded yet, populate - // them. - if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { - $elements += element_info($elements['#type']); - } - - // Make any final changes to the element before it is rendered. This means - // that the $element or the children can be altered or corrected before the - // element is rendered into the final text. - if (isset($elements['#pre_render'])) { - foreach ($elements['#pre_render'] as $callable) { - if (is_string($callable) && strpos($callable, '::') === FALSE) { - $callable = $controller_resolver->getControllerFromDefinition($callable); - } - // Since #pre_render callbacks may be used for generating a render array's - // content, and we might be rendering the main content for the page, it is - // possible that a #pre_render callback throws an exception that will - // cause a different page to be rendered (e.g. throwing - // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will - // cause the 404 page to be rendered). That page might also use - // drupal_render(), but if exceptions aren't caught here, the stack will - // be left in an inconsistent state. - // Hence, catch all exceptions and reset the stack and re-throw them. - try { - $elements = call_user_func($callable, $elements); - } - catch (\Exception $e) { - // Reset stack and re-throw exception. - $stack = NULL; - throw $e; - } - } - } - - // Defaults for bubbleable rendering metadata. - $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); - $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); - $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); - - // Allow #pre_render to abort rendering. - if (!empty($elements['#printed'])) { - // The #printed element contains all the bubbleable rendering metadata for - // the subtree. - $update_stack($elements); - // #printed, so rendering is finished, all necessary info collected! - $bubble_stack(); - return ''; - } - - // Add any JavaScript state information associated with the element. - if (!empty($elements['#states'])) { - drupal_process_states($elements); - } - - // Get the children of the element, sorted by weight. - $children = Element::children($elements, TRUE); - - // Initialize this element's #children, unless a #pre_render callback already - // preset #children. - if (!isset($elements['#children'])) { - $elements['#children'] = ''; - } - - // @todo Simplify after https://drupal.org/node/2273925 - if (isset($elements['#markup'])) { - $elements['#markup'] = SafeMarkup::set($elements['#markup']); - } - - // Assume that if #theme is set it represents an implemented hook. - $theme_is_implemented = isset($elements['#theme']); - // Check the elements for insecure HTML and pass through sanitization. - if (isset($elements)) { - $markup_keys = array( - '#description', - '#field_prefix', - '#field_suffix', - ); - foreach ($markup_keys as $key) { - if (!empty($elements[$key]) && is_scalar($elements[$key])) { - $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); - } - } - } - - // Call the element's #theme function if it is set. Then any children of the - // element have to be rendered there. If the internal #render_children - // property is set, do not call the #theme function to prevent infinite - // recursion. - if ($theme_is_implemented && !isset($elements['#render_children'])) { - $elements['#children'] = \Drupal::theme()->render($elements['#theme'], $elements); - - // If _theme() returns FALSE this means that the hook in #theme was not - // found in the registry and so we need to update our flag accordingly. This - // is common for theme suggestions. - $theme_is_implemented = ($elements['#children'] !== FALSE); - } - - // If #theme is not implemented or #render_children is set and the element has - // an empty #children attribute, render the children now. This is the same - // process as drupal_render_children() but is inlined for speed. - if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { - foreach ($children as $key) { - $elements['#children'] .= drupal_render($elements[$key]); - } - $elements['#children'] = SafeMarkup::set($elements['#children']); - } - - // If #theme is not implemented and the element has raw #markup as a - // fallback, prepend the content in #markup to #children. In this case - // #children will contain whatever is provided by #pre_render prepended to - // what is rendered recursively above. If #theme is implemented then it is - // the responsibility of that theme implementation to render #markup if - // required. Eventually #theme_wrappers will expect both #markup and - // #children to be a single string as #children. - if (!$theme_is_implemented && isset($elements['#markup'])) { - $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); - } - - // Let the theme functions in #theme_wrappers add markup around the rendered - // children. - // #states and #attached have to be processed before #theme_wrappers, because - // the #type 'page' render array from drupal_prepare_page() would render the - // $page and wrap it into the html.html.twig template without the attached - // assets otherwise. - // If the internal #render_children property is set, do not call the - // #theme_wrappers function(s) to prevent infinite recursion. - if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { - foreach ($elements['#theme_wrappers'] as $key => $value) { - // If the value of a #theme_wrappers item is an array then the theme hook - // is found in the key of the item and the value contains attribute - // overrides. Attribute overrides replace key/value pairs in $elements for - // only this _theme() call. This allows #theme hooks and #theme_wrappers - // hooks to share variable names without conflict or ambiguity. - $wrapper_elements = $elements; - if (is_string($key)) { - $wrapper_hook = $key; - foreach ($value as $attribute => $override) { - $wrapper_elements[$attribute] = $override; - } - } - else { - $wrapper_hook = $value; - } - - $elements['#children'] = \Drupal::theme()->render($wrapper_hook, $wrapper_elements); - } - } - - // Filter the outputted content and make any last changes before the - // content is sent to the browser. The changes are made on $content - // which allows the outputted text to be filtered. - if (isset($elements['#post_render'])) { - foreach ($elements['#post_render'] as $callable) { - if (is_string($callable) && strpos($callable, '::') === FALSE) { - $callable = $controller_resolver->getControllerFromDefinition($callable); - } - $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); - } - } - - // We store the resulting output in $elements['#markup'], to be consistent - // with how render cached output gets stored. This ensures that - // #post_render_cache callbacks get the same data to work with, no matter if - // #cache is disabled, #cache is enabled, there is a cache hit or miss. - $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; - $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; - - $elements['#markup'] = $prefix . $elements['#children'] . $suffix; - - // We've rendered this element (and its subtree!), now update the stack. - $update_stack($elements); - - // Cache the processed element if #cache is set. - if (isset($elements['#cache'])) { - drupal_render_cache_set($elements['#markup'], $elements); - } - - // Only when we're in a root (non-recursive) drupal_render() call, - // #post_render_cache callbacks must be executed, to prevent breaking the - // render cache in case of nested elements with #cache set. - // - // By running them here, we ensure that: - // - they run when #cache is disabled, - // - they run when #cache is enabled and there is a cache miss. - // Only the case of a cache hit when #cache is enabled, is not handled here, - // that is handled earlier in drupal_render(). - if ($is_root_call) { - // We've already called $update_stack() earlier, which updated both the - // element and current stack frame. However, - // _drupal_render_process_post_render_cache() can both change the element - // further and create and render new child elements, so provide a fresh - // stack frame to collect those additions, merge them back to the element, - // and then update the current frame to match the modified element state. - $stack->push(new RenderStackFrame()); - _drupal_render_process_post_render_cache($elements); - $post_render_additions = $stack->pop(); - $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags); - $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached); - $elements['#post_render_cache'] = NestedArray::mergeDeep($elements['#post_render_cache'], $post_render_additions->postRenderCache); - if ($stack->count() !== 1) { - throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); - } - } - - // Rendering is finished, all necessary info collected! - $bubble_stack(); - - $elements['#printed'] = TRUE; - $elements['#markup'] = SafeMarkup::set($elements['#markup']); - return $elements['#markup']; + * @see \Drupal\Core\Render\RendererInterface::render() + */ +function drupal_render(&$elements, $is_recursive_call = FALSE) { + return \Drupal::service('renderer')->render($elements, $is_recursive_call); } /** @@ -3105,43 +2620,6 @@ function drupal_render_cache_generate_placeholder($callback, array &$context) { return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>'; } -/** - * Processes #post_render_cache callbacks. - * - * #post_render_cache callbacks may modify: - * - #markup: to replace placeholders - * - #attached: to add libraries or JavaScript settings - * - * Note that in either of these cases, #post_render_cache callbacks are - * implicitly idempotent: a placeholder that has been replaced can't be replaced - * again, and duplicate attachments are ignored. - * - * @param array &$elements - * The structured array describing the data being rendered. - * - * @see drupal_render() - * @see drupal_render_collect_post_render_cache - */ -function _drupal_render_process_post_render_cache(array &$elements) { - if (isset($elements['#post_render_cache'])) { - /** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */ - $controller_resolver = \Drupal::service('controller_resolver'); - - // Call all #post_render_cache callbacks, passing the provided context. - foreach (array_keys($elements['#post_render_cache']) as $callback) { - if (strpos($callback, '::') === FALSE) { - $callable = $controller_resolver->getControllerFromDefinition($callback); - } - else { - $callable = $callback; - } - foreach ($elements['#post_render_cache'][$callback] as $context) { - $elements = call_user_func_array($callable, array($elements, $context)); - } - } - } -} - /** * Creates the cache ID for a renderable element. * diff --git a/core/includes/theme.inc b/core/includes/theme.inc index d094c173976b..34dbdc2a9399 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -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. * diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php index 2b55e241178d..156e749521f8 100644 --- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php @@ -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); } } diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php index ca7aa1827a11..f528409750c8 100644 --- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php +++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php @@ -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); } } diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index be94ee3d7b8d..323fd871c36f 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -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'], diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php new file mode 100644 index 000000000000..5fb494e52b9c --- /dev/null +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -0,0 +1,390 @@ +<?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 ''; + } + + if (!isset($stack)) { + $stack = new \SplStack(); + } + $stack->push(new RenderStackFrame()); + + // Try to fetch the prerendered element from cache, run any + // #post_render_cache callbacks and return the final markup. + if (isset($elements['#cache'])) { + $cached_element = drupal_render_cache_get($elements); + if ($cached_element !== FALSE) { + $elements = $cached_element; + // Only when we're not in a root (non-recursive) drupal_render() call, + // #post_render_cache callbacks must be executed, to prevent breaking + // the render cache in case of nested elements with #cache set. + if ($is_root_call) { + $this->processPostRenderCache($elements); + } + $elements['#markup'] = SafeMarkup::set($elements['#markup']); + // The render cache item contains all the bubbleable rendering metadata + // for the subtree. + $update_stack($elements); + // Render cache hit, so rendering is finished, all necessary info + // collected! + $bubble_stack(); + return $elements['#markup']; + } + } + + // If the default values for this element have not been loaded yet, populate + // them. + if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { + $elements += $this->elementInfo->getInfo($elements['#type']); + } + + // Make any final changes to the element before it is rendered. This means + // that the $element or the children can be altered or corrected before the + // element is rendered into the final text. + if (isset($elements['#pre_render'])) { + foreach ($elements['#pre_render'] as $callable) { + if (is_string($callable) && strpos($callable, '::') === FALSE) { + $callable = $this->controllerResolver->getControllerFromDefinition($callable); + } + // Since #pre_render callbacks may be used for generating a render + // array's content, and we might be rendering the main content for the + // page, it is possible that a #pre_render callback throws an exception + // that will cause a different page to be rendered (e.g. throwing + // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will + // cause the 404 page to be rendered). That page might also use + // drupal_render(), but if exceptions aren't caught here, the stack will + // be left in an inconsistent state. + // Hence, catch all exceptions and reset the stack and re-throw them. + try { + $elements = call_user_func($callable, $elements); + } + catch (\Exception $e) { + // Reset stack and re-throw exception. + $stack = NULL; + throw $e; + } + } + } + + // Defaults for bubbleable rendering metadata. + $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); + $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); + $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); + + // Allow #pre_render to abort rendering. + if (!empty($elements['#printed'])) { + // The #printed element contains all the bubbleable rendering metadata for + // the subtree. + $update_stack($elements); + // #printed, so rendering is finished, all necessary info collected! + $bubble_stack(); + return ''; + } + + // Add any JavaScript state information associated with the element. + if (!empty($elements['#states'])) { + drupal_process_states($elements); + } + + // Get the children of the element, sorted by weight. + $children = Element::children($elements, TRUE); + + // Initialize this element's #children, unless a #pre_render callback + // already preset #children. + if (!isset($elements['#children'])) { + $elements['#children'] = ''; + } + + // @todo Simplify after https://drupal.org/node/2273925 + if (isset($elements['#markup'])) { + $elements['#markup'] = SafeMarkup::set($elements['#markup']); + } + + // Assume that if #theme is set it represents an implemented hook. + $theme_is_implemented = isset($elements['#theme']); + // Check the elements for insecure HTML and pass through sanitization. + if (isset($elements)) { + $markup_keys = array( + '#description', + '#field_prefix', + '#field_suffix', + ); + foreach ($markup_keys as $key) { + if (!empty($elements[$key]) && is_scalar($elements[$key])) { + $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); + } + } + } + + // Call the element's #theme function if it is set. Then any children of the + // element have to be rendered there. If the internal #render_children + // property is set, do not call the #theme function to prevent infinite + // recursion. + if ($theme_is_implemented && !isset($elements['#render_children'])) { + $elements['#children'] = $this->theme->render($elements['#theme'], $elements); + + // If ThemeManagerInterface::render() returns FALSE this means that the + // hook in #theme was not found in the registry and so we need to update + // our flag accordingly. This is common for theme suggestions. + $theme_is_implemented = ($elements['#children'] !== FALSE); + } + + // If #theme is not implemented or #render_children is set and the element + // has an empty #children attribute, render the children now. This is the + // same process as Renderer::render() but is inlined for speed. + if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { + foreach ($children as $key) { + $elements['#children'] .= $this->render($elements[$key]); + } + $elements['#children'] = SafeMarkup::set($elements['#children']); + } + + // If #theme is not implemented and the element has raw #markup as a + // fallback, prepend the content in #markup to #children. In this case + // #children will contain whatever is provided by #pre_render prepended to + // what is rendered recursively above. If #theme is implemented then it is + // the responsibility of that theme implementation to render #markup if + // required. Eventually #theme_wrappers will expect both #markup and + // #children to be a single string as #children. + if (!$theme_is_implemented && isset($elements['#markup'])) { + $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); + } + + // Let the theme functions in #theme_wrappers add markup around the rendered + // children. + // #states and #attached have to be processed before #theme_wrappers, + // because the #type 'page' render array from drupal_prepare_page() would + // render the $page and wrap it into the html.html.twig template without the + // attached assets otherwise. + // If the internal #render_children property is set, do not call the + // #theme_wrappers function(s) to prevent infinite recursion. + if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { + foreach ($elements['#theme_wrappers'] as $key => $value) { + // If the value of a #theme_wrappers item is an array then the theme + // hook is found in the key of the item and the value contains attribute + // overrides. Attribute overrides replace key/value pairs in $elements + // for only this ThemeManagerInterface::render() call. This allows + // #theme hooks and #theme_wrappers hooks to share variable names + // without conflict or ambiguity. + $wrapper_elements = $elements; + if (is_string($key)) { + $wrapper_hook = $key; + foreach ($value as $attribute => $override) { + $wrapper_elements[$attribute] = $override; + } + } + else { + $wrapper_hook = $value; + } + + $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); + } + } + + // Filter the outputted content and make any last changes before the content + // is sent to the browser. The changes are made on $content which allows the + // outputted text to be filtered. + if (isset($elements['#post_render'])) { + foreach ($elements['#post_render'] as $callable) { + if (is_string($callable) && strpos($callable, '::') === FALSE) { + $callable = $this->controllerResolver->getControllerFromDefinition($callable); + } + $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); + } + } + + // We store the resulting output in $elements['#markup'], to be consistent + // with how render cached output gets stored. This ensures that + // #post_render_cache callbacks get the same data to work with, no matter if + // #cache is disabled, #cache is enabled, there is a cache hit or miss. + $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; + $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; + + $elements['#markup'] = $prefix . $elements['#children'] . $suffix; + + // We've rendered this element (and its subtree!), now update the stack. + $update_stack($elements); + + // Cache the processed element if #cache is set. + if (isset($elements['#cache'])) { + drupal_render_cache_set($elements['#markup'], $elements); + } + + // Only when we're in a root (non-recursive) drupal_render() call, + // #post_render_cache callbacks must be executed, to prevent breaking the + // render cache in case of nested elements with #cache set. + // + // By running them here, we ensure that: + // - they run when #cache is disabled, + // - they run when #cache is enabled and there is a cache miss. + // Only the case of a cache hit when #cache is enabled, is not handled here, + // that is handled earlier in Renderer::render(). + if ($is_root_call) { + // We've already called $update_stack() earlier, which updated both the + // element and current stack frame. However, + // Renderer::processPostRenderCache() can both change the element + // further and create and render new child elements, so provide a fresh + // stack frame to collect those additions, merge them back to the element, + // and then update the current frame to match the modified element state. + $stack->push(new RenderStackFrame()); + $this->processPostRenderCache($elements); + $post_render_additions = $stack->pop(); + $elements['#cache']['tags'] = Cache::mergeTags($elements['#cache']['tags'], $post_render_additions->tags); + $elements['#attached'] = drupal_merge_attached($elements['#attached'], $post_render_additions->attached); + $elements['#post_render_cache'] = NestedArray::mergeDeep($elements['#post_render_cache'], $post_render_additions->postRenderCache); + if ($stack->count() !== 1) { + throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); + } + } + + // Rendering is finished, all necessary info collected! + $bubble_stack(); + + $elements['#printed'] = TRUE; + $elements['#markup'] = SafeMarkup::set($elements['#markup']); + return $elements['#markup']; + } + + /** + * Processes #post_render_cache callbacks. + * + * #post_render_cache callbacks may modify: + * - #markup: to replace placeholders + * - #attached: to add libraries or JavaScript settings + * + * Note that in either of these cases, #post_render_cache callbacks are + * implicitly idempotent: a placeholder that has been replaced can't be + * replaced again, and duplicate attachments are ignored. + * + * @param array &$elements + * The structured array describing the data being rendered. + * + * @see drupal_render_collect_post_render_cache + */ + protected function processPostRenderCache(array &$elements) { + if (isset($elements['#post_render_cache'])) { + + // Call all #post_render_cache callbacks, passing the provided context. + foreach (array_keys($elements['#post_render_cache']) as $callback) { + if (strpos($callback, '::') === FALSE) { + $callable = $this->controllerResolver->getControllerFromDefinition($callback); + } + else { + $callable = $callback; + } + foreach ($elements['#post_render_cache'][$callback] as $context) { + $elements = call_user_func_array($callable, array($elements, $context)); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php new file mode 100644 index 000000000000..a6ec2ed16d83 --- /dev/null +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -0,0 +1,244 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\RendererInterface. + */ + +namespace Drupal\Core\Render; + +/** + * Defines an interface for turning a render array into a string. + */ +interface RendererInterface { + + /** + * Renders final HTML given a structured array tree. + * + * Calls ::render() in such a way that #post_render_cache callbacks are + * applied. + * + * Should therefore only be used in occasions where the final rendering is + * happening, just before sending a Response: + * - system internals that are responsible for rendering the final HTML + * - render arrays for non-HTML responses, such as feeds + * + * @param array $elements + * The structured array describing the data to be rendered. + * + * @return string + * The rendered HTML. + * + * @see ::render() + */ + public function renderRoot(&$elements); + + /** + * Renders HTML given a structured array tree. + * + * Renderable arrays have two kinds of key/value pairs: properties and + * children. Properties have keys starting with '#' and their values influence + * how the array will be rendered. Children are all elements whose keys do not + * start with a '#'. Their values should be renderable arrays themselves, + * which will be rendered during the rendering of the parent array. The markup + * provided by the children is typically inserted into the markup generated by + * the parent array. + * + * An important aspect of rendering is the bubbling of rendering metadata: + * cache tags, attached assets and #post_render_cache metadata all need to be + * bubbled up. That information is needed once the rendering to a HTML string + * is completed: the resulting HTML for the page must know by which cache tags + * it should be invalidated, which (CSS and JavaScript) assets must be loaded, + * and which #post_render_cache callbacks should be executed. A stack data + * structure is used to perform this bubbling. + * + * The process of rendering an element is recursive unless the element defines + * an implemented theme hook in #theme. During each call to + * Renderer::render(), the outermost renderable array (also known as an + * "element") is processed using the following steps: + * - If this element has already been printed (#printed = TRUE) or the user + * does not have access to it (#access = FALSE), then an empty string is + * returned. + * - If no stack data structure has been created yet, it is done now. Next, + * an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack. + * - If this element has #cache defined then the cached markup for this + * element will be returned if it exists in Renderer::render()'s cache. To + * use Renderer::render() caching, set the element's #cache property to an + * associative array with one or several of the following keys: + * - 'keys': An array of one or more keys that identify the element. If + * 'keys' is set, the cache ID is created automatically from these keys. + * Cache keys may either be static (just strings) or tokens + * (placeholders that are converted to static keys by the + * @cache_contexts service, depending on the request). See + * drupal_render_cid_create(). + * - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is + * required. If 'cid' is set, 'keys' is ignored. Use only if you have + * special requirements. + * - 'expire': Set to one of the cache lifetime constants. + * - 'bin': Specify a cache bin to cache the element in. Default is + * 'default'. + * When there is a render cache hit, there is no rendering work left to be + * done, so the stack must be updated. The empty (and topmost) frame that + * was just pushed onto the stack is updated with all bubbleable rendering + * metadata from the element retrieved from render cache. Then, this stack + * frame is bubbled: the two topmost frames are popped from the stack, + * they are merged, and the result is pushed back onto the stack. + * - If this element has #type defined and the default attributes for this + * element have not already been merged in (#defaults_loaded = TRUE) then + * the defaults for this type of element, defined in hook_element_info(), + * are merged into the array. #defaults_loaded is set by functions that + * process render arrays and call element_info() before passing the array + * to Renderer::render(), such as form_builder() in the Form API. + * - If this element has an array of #pre_render functions defined, they are + * called sequentially to modify the element before rendering. After all + * the #pre_render functions have been called, #printed is checked a + * second time in case a #pre_render function flags the element as + * printed. If #printed is set, we return early and hence no rendering + * work is left to be done, similarly to a render cache hit. Once again, + * the empty (and topmost) frame that was just pushed onto the stack is + * updated with all bubbleable rendering metadata from the element whose + * #printed = TRUE. + * Then, this stack frame is bubbled: the two topmost frames are popped + * from the stack, they are merged, and the result is pushed back onto the + * stack. + * - The child elements of this element are sorted by weight using uasort() + * in \Drupal\Core\Render\Element::children(). Since this is expensive, + * when passing already sorted elements to Renderer::render(), for example + * from a database query, set $elements['#sorted'] = TRUE to avoid sorting + * them a second time. + * - The main render phase to produce #children for this element takes + * place: + * - If this element has #theme defined and #theme is an implemented theme + * hook/suggestion then ThemeManagerInterface::render() is called and + * must render both the element and its children. If #render_children is + * set, ThemeManagerInterface::render() will not be called. + * #render_children is usually only set internally by + * ThemeManagerInterface::render() so that we can avoid the situation + * where Renderer::render() called from within a theme preprocess + * function creates an infinite loop. + * - If this element does not have a defined #theme, or the defined #theme + * hook is not implemented, or #render_children is set, then + * Renderer::render() is called recursively on each of the child + * elements of this element, and the result of each is concatenated onto + * #children. This is skipped if #children is not empty at this point. + * - Once #children has been rendered for this element, if #theme is not + * implemented and #markup is set for this element, #markup will be + * prepended to #children. + * - If this element has #states defined then JavaScript state information + * is added to this element's #attached attribute by + * drupal_process_states(). + * - If this element has #attached defined then any required libraries, + * JavaScript, CSS, or other custom data are added to the current page by + * drupal_process_attached(). + * - If this element has an array of #theme_wrappers defined and + * #render_children is not set, #children is then re-rendered by passing + * the element in its current state to ThemeManagerInterface::render() + * successively for each item in #theme_wrappers. Since #theme and + * #theme_wrappers hooks often define variables with the same names it is + * possible to explicitly override each attribute passed to each + * #theme_wrappers hook by setting the hook name as the key and an array + * of overrides as the value in #theme_wrappers array. + * For example, if we have a render element as follows: + * @code + * array( + * '#theme' => 'image', + * '#attributes' => array('class' => array('foo')), + * '#theme_wrappers' => array('container'), + * ); + * @endcode + * and we need to pass the class 'bar' as an attribute for 'container', we + * can rewrite our element thus: + * @code + * array( + * '#theme' => 'image', + * '#attributes' => array('class' => array('foo')), + * '#theme_wrappers' => array( + * 'container' => array( + * '#attributes' => array('class' => array('bar')), + * ), + * ), + * ); + * @endcode + * - If this element has an array of #post_render functions defined, they + * are called sequentially to modify the rendered #children. Unlike + * #pre_render functions, #post_render functions are passed both the + * rendered #children attribute as a string and the element itself. + * - If this element has #prefix and/or #suffix defined, they are + * concatenated to #children. + * - The rendering of this element is now complete. The next step will be + * render caching. So this is the perfect time to update the the stack. At + * this point, children of this element (if any), have been rendered also, + * and if there were any, their bubbleable rendering metadata will have + * been bubbled up into the stack frame for the element that is currently + * being rendered. The render cache item for this element must contain the + * bubbleable rendering metadata for this element and all of its children. + * However, right now, the topmost stack frame (the one for this element) + * currently only contains the metadata for the children. Therefore, the + * topmost stack frame is updated with this element's metadata, and then + * the element's metadata is replaced with the metadata in the topmost + * stack frame. This element now contains all bubbleable rendering + * metadata for this element and all its children, so it's now ready for + * render caching. + * - If this element has #cache defined, the rendered output of this element + * is saved to Renderer::render()'s internal cache. This includes the + * changes made by #post_render. + * - If this element has an array of #post_render_cache functions defined, + * or any of its children has (which we would know thanks to the stack + * having been updated just before the render caching step), they are + * called sequentially to replace placeholders in the final #markup and + * extend #attached. Placeholders must contain a unique token, to + * guarantee that e.g. samples of placeholders are not replaced also. But, + * since #post_render_cache callbacks add attach additional assets, the + * correct bubbling of those must once again be taken into account. This + * final stage of rendering should be considered as if it were the parent + * of the current element, because it takes that as its input, and then + * alters its #markup. Hence, just before calling the #post_render_cache + * callbacks, a new empty frame is pushed onto the stack, where all assets + * #attached during the execution of those callbacks will end up in. Then, + * after the execution of those callbacks, we merge that back into the + * element. Note that these callbacks run always: when hitting the render + * cache, when missing, or when render caching is not used at all. This is + * done to allow any Drupal module to customize other render arrays + * without breaking the render cache if it is enabled, and to not require + * it to use other logic when render caching is disabled. + * - Just before finishing the rendering of this element, this element's + * stack frame (the topmost one) is bubbled: the two topmost frames are + * popped from the stack, they are merged and the result is pushed back + * onto the stack. + * So if this element e.g. was a child element, then a new frame was + * pushed onto the stack element at the beginning of rendering this + * element, it was updated when the rendering was completed, and now we + * merge it with the frame for the parent, so that the parent now has the + * bubbleable rendering metadata for its child. + * - #printed is set to TRUE for this element to ensure that it is only + * rendered once. + * - The final value of #children for this element is returned as the + * rendered output. + * + * @param array $elements + * The structured array describing the data to be rendered. + * @param bool $is_root_call + * (Internal use only.) Whether this is a recursive call or not. See + * ::renderRoot(). + * + * @return string + * The rendered HTML. + * + * @throws \LogicException + * If a root call to ::render() does not result in an empty stack, this + * indicates an erroneous ::render() root call (a root call within a + * root call, which makes no sense). Therefore, a logic exception is thrown. + * @throws \Exception + * If a #pre_render callback throws an exception, it is caught to reset the + * stack used for bubbling rendering metadata, and then the exception is re- + * thrown. + * + * @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo() + * @see \Drupal\Core\Theme\ThemeManagerInterface::render() + * @see drupal_process_states() + * @see drupal_process_attached() + * @see ::renderRoot() + */ + public function render(&$elements, $is_root_call = FALSE); + +} diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index f9e983a7e81d..a100374e95d0 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -10,6 +10,9 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\StackedRouteMatchInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Template\Attribute; +use Drupal\Component\Utility\SafeMarkup; /** * Provides the default implementation of a theme manager. @@ -47,9 +50,18 @@ class ThemeManager implements ThemeManagerInterface { */ protected $requestStack; + /** + * The app root. + * + * @var string + */ + protected $root; + /** * Constructs a new ThemeManager object. * + * @param string $root + * The app root. * @param \Drupal\Core\Theme\Registry $theme_registry * The theme registry. * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator @@ -58,19 +70,22 @@ class ThemeManager implements ThemeManagerInterface { * The theme initialization. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ - public function __construct(Registry $theme_registry, ThemeNegotiatorInterface $theme_negotiator, ThemeInitialization $theme_initialization, RequestStack $request_stack) { + public function __construct($root, Registry $theme_registry, ThemeNegotiatorInterface $theme_negotiator, ThemeInitialization $theme_initialization, RequestStack $request_stack, ModuleHandlerInterface $module_handler) { + $this->root = $root; $this->themeNegotiator = $theme_negotiator; $this->themeRegistry = $theme_registry; $this->themeInitialization = $theme_initialization; $this->requestStack = $request_stack; + $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public function render($hook, array $variables) { - return _theme($hook, $variables); + return $this->theme($hook, $variables); } /** @@ -109,6 +124,260 @@ public function setActiveTheme(ActiveTheme $active_theme) { return $this; } + /** + * Generates themed output (internal use only). + * + * @see \Drupal\Core\Render\RendererInterface::render(); + */ + protected function theme($hook, $variables = array()) { + 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(t('_theme() may not be called 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.', 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 = $this->moduleHandler->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, + ); + $this->moduleHandler->alter($hooks, $suggestions, $variables, $base_theme_hook); + $this->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 $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; + } + } + // 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; + } + /** * Initializes the active theme for a given route match. * diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php index 33c5989751bb..6ee2017f08e1 100644 --- a/core/modules/node/src/Controller/NodeController.php +++ b/core/modules/node/src/Controller/NodeController.php @@ -12,6 +12,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Url; use Drupal\node\NodeTypeInterface; use Drupal\node\NodeInterface; @@ -29,21 +30,34 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa */ protected $dateFormatter; + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Constructs a NodeController object. * * @param \Drupal\Core\Datetime\DateFormatter $date_formatter * The date formatter service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. */ - public function __construct(DateFormatter $date_formatter) { + public function __construct(DateFormatter $date_formatter, RendererInterface $renderer) { $this->dateFormatter = $date_formatter; + $this->renderer = $renderer; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('date.formatter')); + return new static( + $container->get('date.formatter'), + $container->get('renderer') + ); } @@ -112,7 +126,7 @@ public function add(NodeTypeInterface $node_type) { */ public function revisionShow($node_revision) { $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision); - $node_view_controller = new NodeViewController($this->entityManager); + $node_view_controller = new NodeViewController($this->entityManager, $this->renderer); $page = $node_view_controller->view($node); unset($page['nodes'][$node->id()]['#cache']); return $page; diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index f13b8dbfc5ea..8d21d8571707 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -94,6 +94,13 @@ abstract class FieldPluginBase extends HandlerBase { */ protected $linkGenerator; + /** + * Stores the render API renderer. + * + * @var \Drupal\Core\Render\Renderer + */ + protected $renderer; + /** * Overrides Drupal\views\Plugin\views\HandlerBase::init(). */ @@ -907,7 +914,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#items' => $items, '#list_type' => $type, ); - $output .= drupal_render($item_list); + $output .= $this->getRenderer()->render($item_list); } } } @@ -1153,7 +1160,7 @@ public function advancedRender(ResultRow $values) { else { $value = $this->render($values); if (is_array($value)) { - $value = drupal_render($value); + $value = $this->getRenderer()->render($value); } $this->last_render = $value; $this->original_value = $value; @@ -1166,7 +1173,7 @@ public function advancedRender(ResultRow $values) { foreach ($raw_items as $count => $item) { $value = $this->render_item($count, $item); if (is_array($value)) { - $value = drupal_render($value); + $value = $this->getRenderer()->render($value); } $this->last_render = $value; $this->original_value = $this->last_render; @@ -1184,7 +1191,7 @@ public function advancedRender(ResultRow $values) { } if (is_array($value)) { - $value = drupal_render($value); + $value = $this->getRenderer()->render($value); } // This happens here so that renderAsLink can get the unaltered value of // this field as a token rather than the altered value. @@ -1640,7 +1647,7 @@ protected function addSelfTokens(&$tokens, $item) { } protected function documentSelfTokens(&$tokens) { } /** - * Pass values to drupal_render() using $this->themeFunctions() as #theme. + * Pass values to $this->getRenderer()->render() using $this->themeFunctions() as #theme. * * @param \Drupal\views\ResultRow $values * Holds single row of a view's result set. @@ -1655,7 +1662,7 @@ function theme(ResultRow $values) { '#field' => $this, '#row' => $values, ); - return drupal_render($build); + return $this->getRenderer()->render($build); } public function themeFunctions() { @@ -1745,6 +1752,20 @@ protected function linkGenerator() { } return $this->linkGenerator; } + + /** + * Returns the render API renderer. + * + * @return \Drupal\Core\Render\Renderer + */ + protected function getRenderer() { + if (!isset($this->renderer)) { + $this->renderer = \Drupal::service('renderer'); + } + + return $this->renderer; + } + } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php b/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php index 3389858d2c2a..4f6d33d9c62f 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php @@ -67,7 +67,7 @@ public function testView() { // Initialize the controller to test. - $controller = new EntityViewController($entity_manager); + $controller = new EntityViewController($entity_manager, $this->getMock('Drupal\Core\Render\RendererInterface')); // Test the view method. $this->assertEquals($controller->view($entity, 'full'), 'Output from rendering the entity'); -- GitLab