.../EarlyRenderingControllerWrapper.php | 30 ++++---- core/lib/Drupal/Core/Render/RenderContext.php | 60 +++++++++++++++ core/lib/Drupal/Core/Render/Renderer.php | 90 ++++++---------------- core/lib/Drupal/Core/Render/RendererInterface.php | 17 ++-- 4 files changed, 110 insertions(+), 87 deletions(-) diff --git a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php index 702dd02..a39c261 100644 --- a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php +++ b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php @@ -9,6 +9,7 @@ use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; @@ -30,16 +31,17 @@ * This event subscriber wraps all controller executions in a closure that sets * up a render context. Consequently, any drupal_render() invocations during the * actual controller's execution will have their bubbleable metadata (assets & - * cacheability) get set on the stack for that render context. + * cacheability) stored on that render context. * - * If the stack is empty, then the controller either did not do any rendering at - * all, or used the RendererInterface::renderRoot() or ::renderPlain() methods. - * In that case, no bubbleable metadata is lost. + * If the render context is empty, then the controller either did not do any + * rendering at all, or used the RendererInterface::renderRoot() or + * ::renderPlain() methods. In that case, no bubbleable metadata is lost. * - * If the stack is not empty, then the controller did use drupal_render(), and - * bubbleable metadata is lost. But, rather then letting it go lost, the closure - * will update the render array returned by the controller by merging the "lost" - * bubbleable metadata onto the render array. Disaster averted. + * If the render context is not empty, then the controller did use + * drupal_render(), and bubbleable metadata is lost. But, rather then letting it + * go lost, the closure will update the render array returned by the controller + * by merging the "lost" bubbleable metadata onto the render array. Disaster + * averted. * * In other words: this just exists to ease the transition to Drupal 8: it * allows controllers that return render arrays (the bulk) to still do early @@ -94,21 +96,21 @@ public function onController(FilterControllerEvent $event) { $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller); $event->setController(function() use ($controller, $arguments) { - $stack = new \SplStack(); + $context = new RenderContext(); - $response = $this->renderer->executeInRenderContext($stack, function() use ($controller, $arguments) { + $response = $this->renderer->executeInRenderContext($context, function() use ($controller, $arguments) { // Now call the actual controller, just like HttpKernel does. return call_user_func_array($controller, $arguments); }); // If early rendering happened, i.e. if code in the controller called - // drupal_render(), then the bubbleable metadata for that currently lives - // on the stack of the current render context. - if (!$stack->isEmpty()) { + // drupal_render(), then the bubbleable metadata for that is stored in + // the current render context. + if (!$context->isEmpty()) { if (is_array($response)) { // Merging the "lost" bubbleable metadata onto the render array // returned by the controller. - $early_rendering_bubbleable_metadata = $stack->pop(); + $early_rendering_bubbleable_metadata = $context->pop(); BubbleableMetadata::createFromRenderArray($response) ->merge($early_rendering_bubbleable_metadata) ->applyTo($response); diff --git a/core/lib/Drupal/Core/Render/RenderContext.php b/core/lib/Drupal/Core/Render/RenderContext.php new file mode 100644 index 0000000..e255005 --- /dev/null +++ b/core/lib/Drupal/Core/Render/RenderContext.php @@ -0,0 +1,60 @@ +pop(); + // Update the frame, but also update the current element, to ensure it + // contains up-to-date information in case it gets render cached. + $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame); + $updated_frame->applyTo($element); + $this->push($updated_frame); + } + + /** + * Bubbles the stack. + * + * Whenever another level in the render array has been rendered, the stack + * must be bubbled, to merge its rendering metadata with that of the parent + * element. + */ + public function bubble() { + // If there's only one frame on the stack, then this is the root call, and + // we can't bubble up further. ::renderRoot() will reset the stack, but we + // must not reset it here to allow users of ::executeInRenderContext() to + // access the stack directly. + if ($this->count() === 1) { + return; + } + + // Merge the current and the parent stack frame. + $current = $this->pop(); + $parent = $this->pop(); + $this->push($current->merge($parent)); + } + +} diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index e5412ad..1ae87a0 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -57,11 +57,11 @@ class Renderer implements RendererInterface { protected $rendererConfig; /** - * The stack containing bubbleable rendering metadata. + * The render context. * - * @var \SplStack|null + * @var \Drupal\Core\Render\RenderContext|null */ - protected static $stack; + protected $context; /** * Whether we're currently in a ::renderRoot() call. @@ -102,9 +102,9 @@ public function renderRoot(&$elements) { throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.'); } - // Render in its own render stack. + // Render in its own render context. $this->isRenderingRoot = TRUE; - $output = $this->executeInRenderContext(new \SplStack(), function () use (&$elements) { + $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { return $this->render($elements, TRUE); }); $this->isRenderingRoot = FALSE; @@ -118,7 +118,7 @@ public function renderRoot(&$elements) { * @todo Rename to ::renderInIsolation() */ public function renderPlain(&$elements, $is_root_call = TRUE) { - return $this->executeInRenderContext(new \SplStack(), function () use (&$elements, $is_root_call) { + return $this->executeInRenderContext(new RenderContext(), function () use (&$elements, $is_root_call) { return $this->render($elements, $is_root_call); }); } @@ -206,10 +206,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } - if (!isset(static::$stack)) { - throw new \LogicException("Render stack is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); + if (!isset($this->context)) { + throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."); } - static::$stack->push(new BubbleableMetadata()); + $this->context->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely @@ -250,10 +250,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // The render cache item contains all the bubbleable rendering metadata // for the subtree. - $this->updateStack($elements); + $this->context->update($elements); // Render cache hit, so rendering is finished, all necessary info // collected! - $this->bubbleStack(); + $this->context->bubble(); return $elements['#markup']; } } @@ -351,9 +351,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. - $this->updateStack($elements); + $this->context->update($elements); // #printed, so rendering is finished, all necessary info collected! - $this->bubbleStack(); + $this->context->bubble(); return ''; } @@ -479,8 +479,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $elements['#markup'] = $prefix . $elements['#children'] . $suffix; - // We've rendered this element (and its subtree!), now update the stack. - $this->updateStack($elements); + // We've rendered this element (and its subtree!), now update the context. + $this->context->update($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. @@ -502,13 +502,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); - if (static::$stack->count() !== 1) { + if ($this->context->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! - $this->bubbleStack(); + $this->context->bubble(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); @@ -518,65 +518,25 @@ protected function doRender(&$elements, $is_root_call = FALSE) { /** * {@inheritdoc} */ - public function executeInRenderContext(\SplStack $stack, callable $callable) { - // Store the current render stack. - $current_stack = static::$stack; + public function executeInRenderContext(RenderContext $context, callable $callable) { + // Store the current render context. + $current_context = $this->context; - // Set the provided stack and call the callable, this will use that stack. - static::$stack = $stack; + // Set the provided context and call the callable, it will use that context. + $this->context = $context; $result = $callable(); // @todo Convert to an assertion in https://www.drupal.org/node/2408013 - if (static::$stack->count() > 1) { + if ($this->context->count() > 1) { throw new \LogicException('Bubbling failed.'); } - // Restore the original render stack. - static::$stack = $current_stack; + // Restore the original render context. + $this->context = $current_context; return $result; } /** - * Updates the stack. - * - * @param array &$element - * The element of the render array that has just been rendered. The stack - * frame for this element will be updated with the bubbleable rendering - * metadata of this element. - */ - protected function updateStack(&$element) { - // The latest frame represents the bubbleable metadata for the subtree. - $frame = static::$stack->pop(); - // Update the frame, but also update the current element, to ensure it - // contains up-to-date information in case it gets render cached. - $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame); - $updated_frame->applyTo($element); - static::$stack->push($updated_frame); - } - - /** - * Bubbles the stack. - * - * Whenever another level in the render array has been rendered, the stack - * must be bubbled, to merge its rendering metadata with that of the parent - * element. - */ - protected function bubbleStack() { - // If there's only one frame on the stack, then this is the root call, and - // we can't bubble up further. ::renderRoot() will reset the stack, but we - // must not reset it here to allow users of ::executeInRenderContext() to - // access the stack directly. - if (static::$stack->count() === 1) { - return; - } - - // Merge the current and the parent stack frame. - $current = static::$stack->pop(); - $parent = static::$stack->pop(); - static::$stack->push($current->merge($parent)); - } - - /** * Replaces placeholders. * * Placeholders may have: diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 6f0234e..1e420b1 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -47,7 +47,7 @@ public function renderRoot(&$elements); * ::renderRoot() call, but that is generally highly problematic (and hence an * exception is thrown when a ::renderRoot() call happens within another * ::renderRoot() call). However, in this case, we only care about the output, - * not about the bubbling. Hence this uses a separate render stack, to not + * not about the bubbling. Hence this uses a separate render context, to not * affect the parent ::renderRoot() call. * * (Can be executed within another render context: it runs in isolation.) @@ -92,8 +92,8 @@ public function renderPlain(&$elements); * or configuration that can affect that rendering changes. * - Placeholders, with associated self-contained placeholder render arrays, * for executing code to handle dynamic requirements that cannot be cached. - * A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to - * perform this bubbling. + * A render context (\Drupal\Core\Render\RenderContext) can be used to perform + * bubbling; it is a stack of \Drupal\Core\Render\BubbleableMetadata objects. * * Additionally, whether retrieving from cache or not, it is important to * know all of the assets (CSS and JavaScript) required by the rendered HTML, @@ -107,9 +107,9 @@ public function renderPlain(&$elements); * - 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, + * - If no render context is set yet, an exception is thrown. Otherwise, * an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the - * stack. + * render context. * - 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 @@ -336,16 +336,17 @@ public function render(&$elements, $is_root_call = FALSE); * * @see \Drupal\Core\Render\BubbleableMetadata * - * @param \SplStack $stack - * The stack to use for this render context. + * @param \Drupal\Core\Render\RenderContext $context + * The render context to execute the callable within. * @param callable $callable * The callable to execute. * @return mixed * The callable's return value. * + * @see \Drupal\Core\Render\RenderContext * @throws \LogicException */ - public function executeInRenderContext(\SplStack $stack, callable $callable); + public function executeInRenderContext(RenderContext $context, callable $callable); /** * Merges the bubbleable rendering metadata o/t 2nd render array with the 1st.