Commit 91b4829e authored by alexpott's avatar alexpott

Issue #2382503 by Wim Leers: Not possible to render self-contained render...

Issue #2382503 by Wim Leers: Not possible to render self-contained render array while a render stack is active
parent d4e8abd8
......@@ -39,6 +39,13 @@ class Renderer implements RendererInterface {
*/
protected $elementInfo;
/**
* The stack containing bubbleable rendering metadata.
*
* @var \SplStack|null
*/
protected static $stack;
/**
* Constructs a new Renderer.
*
......@@ -65,36 +72,18 @@ public function renderRoot(&$elements) {
/**
* {@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);
};
public function renderPlain(&$elements) {
$current_stack = self::$stack;
$this->resetStack();
$output = $this->renderRoot($elements);
self::$stack = $current_stack;
return $output;
}
/**
* {@inheritdoc}
*/
public function render(&$elements, $is_root_call = FALSE) {
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']);
......@@ -112,10 +101,10 @@ public function render(&$elements, $is_root_call = FALSE) {
return '';
}
if (!isset($stack)) {
$stack = new \SplStack();
if (!isset(self::$stack)) {
self::$stack = new \SplStack();
}
$stack->push(new RenderStackFrame());
self::$stack->push(new RenderStackFrame());
// Try to fetch the prerendered element from cache, run any
// #post_render_cache callbacks and return the final markup.
......@@ -132,10 +121,10 @@ public function render(&$elements, $is_root_call = FALSE) {
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
$update_stack($elements);
$this->updateStack($elements);
// Render cache hit, so rendering is finished, all necessary info
// collected!
$bubble_stack();
$this->bubbleStack();
return $elements['#markup'];
}
}
......@@ -168,7 +157,7 @@ public function render(&$elements, $is_root_call = FALSE) {
}
catch (\Exception $e) {
// Reset stack and re-throw exception.
$stack = NULL;
$this->resetStack();
throw $e;
}
}
......@@ -183,9 +172,9 @@ public function render(&$elements, $is_root_call = FALSE) {
if (!empty($elements['#printed'])) {
// The #printed element contains all the bubbleable rendering metadata for
// the subtree.
$update_stack($elements);
$this->updateStack($elements);
// #printed, so rendering is finished, all necessary info collected!
$bubble_stack();
$this->bubbleStack();
return '';
}
......@@ -311,7 +300,7 @@ public function render(&$elements, $is_root_call = FALSE) {
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// We've rendered this element (and its subtree!), now update the stack.
$update_stack($elements);
$this->updateStack($elements);
// Cache the processed element if #cache is set.
if (isset($elements['#cache'])) {
......@@ -328,31 +317,83 @@ public function render(&$elements, $is_root_call = FALSE) {
// 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
// We've already called ::updateStack() 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());
self::$stack->push(new RenderStackFrame());
$this->processPostRenderCache($elements);
$post_render_additions = $stack->pop();
$post_render_additions = self::$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) {
if (self::$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();
$this->bubbleStack();
$elements['#printed'] = TRUE;
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
return $elements['#markup'];
}
/**
* Resets the renderer service's internal stack (used for bubbling metadata).
*
* Only necessary in very rare/advanced situations, such as when rendering an
* error page if an exception occurred *during* rendering.
*/
protected function resetStack() {
self::$stack = NULL;
}
/**
* 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 = self::$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);
}
/**
* 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. Reset the stack for the next root call.
if (self::$stack->count() === 1) {
$this->resetStack();
return;
}
// Merge the current and the parent stack frame.
$current = self::$stack->pop();
$parent = self::$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);
self::$stack->push($current);
}
/**
* Processes #post_render_cache callbacks.
*
......
......@@ -33,6 +33,34 @@ interface RendererInterface {
*/
public function renderRoot(&$elements);
/**
* Renders final HTML in situations where no assets are needed.
*
* Calls ::render() in such a way that #post_render_cache callbacks are
* applied.
*
* Useful for e.g. rendering the values of tokens or e-mails, which need a
* render array being turned into a string, but don't need any of the
* bubbleable metadata (the attached assets the cache tags).
*
* Some of these are a relatively common use case and happen *within* a
* ::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
* affect the parent ::renderRoot() call.
*
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return string
* The rendered HTML.
*
* @see ::renderRoot()
* @see ::render()
*/
public function renderPlain(&$elements);
/**
* Renders HTML given a structured array tree.
*
......
......@@ -336,7 +336,7 @@ function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to
'#filter_types_to_skip' => $filter_types_to_skip,
'#langcode' => $langcode,
);
return drupal_render_root($build);
return \Drupal::service('renderer')->renderPlain($build);
}
/**
......
......@@ -1197,4 +1197,106 @@ public function testDrupalProcessAttached() {
}
}
/**
* Tests \Drupal\Core\Render\Renderer::renderPlain().
*/
public function testRenderPlain() {
$renderer = \Drupal::service('renderer');
$complex_child_markup = '<p>Imagine this is a render array for an entity.</p>';
$parent_markup = '<p>Rendered!</p>';
$complex_child_template = [
'#markup' => $complex_child_markup,
'#attached' => [
'library' => [
'core/drupal',
],
],
'#cache' => [
'tags' => [
'test:complex_child',
],
],
'#post_render_cache' => [
'common_test_post_render_cache' => [
['foo' => $this->randomString()],
],
],
];
// Case 1: ::renderRoot() with nested ::renderRoot().
$this->pass('Renderer::renderRoot() may not be called inside of another Renderer::renderRoot() call, this must trigger an exception.');
try {
$complex_child = $complex_child_template;
$page = [
'content' => [
'#pre_render' => [
function () use ($renderer, $complex_child) {
$renderer->renderRoot($complex_child);
}
],
'#suffix' => $parent_markup,
]
];
$renderer->renderRoot($page);
$this->fail('No exception triggered.');
}
catch (\LogicException $e) {
$this->pass('Exception triggered.');
}
// Case 2: ::renderRoot() with nested ::render().
$this->pass('Renderer::render() may be called from anywhere, including from inside of another Renderer::renderRoot() call. Bubbling must be performed.');
try {
$complex_child = $complex_child_template;
$page = [
'content' => [
'#pre_render' => [
function ($elements) use ($renderer, $complex_child, $complex_child_markup, $parent_markup) {
$elements['#markup'] = $renderer->render($complex_child);
$this->assertEqual($complex_child_markup, $elements['#markup'], 'Rendered complex child output as expected, without the #post_render_cache callback executed.');
return $elements;
}
],
'#suffix' => $parent_markup,
]
];
$output = $renderer->renderRoot($page);
$this->pass('No exception triggered.');
$this->assertEqual('<p>overridden</p>', $output, 'Rendered output as expected, with the #post_render_cache callback executed.');
$this->assertTrue(in_array('test:complex_child', $page['#cache']['tags']), 'Cache tag bubbling performed.');
$this->assertTrue(in_array('core/drupal', $page['#attached']['library']), 'Asset bubbling performed.');
}
catch (\LogicException $e) {
$this->fail('Exception triggered.');
}
// Case 3: ::renderRoot() with nested ::renderPlain().
$this->pass('Renderer::renderPlain() may be called from anywhere, including from inside of another Renderer::renderRoot() call.');
try {
$complex_child = $complex_child_template;
$page = [
'content' => [
'#pre_render' => [
function ($elements) use ($renderer, $complex_child, $parent_markup) {
$elements['#markup'] = $renderer->renderPlain($complex_child);
$this->assertEqual('<p>overridden</p>', $elements['#markup'], 'Rendered complex child output as expected, with the #post_render_cache callback executed.');
return $elements;
}
],
'#suffix' => $parent_markup,
]
];
$output = $renderer->renderRoot($page);
$this->pass('No exception triggered.');
$this->assertEqual('<p>overridden</p>' . $parent_markup, $output, 'Rendered output as expected, with the #post_render_cache callback executed.');
$this->assertFalse(in_array('test:complex_child', $page['#cache']['tags']), 'Cache tag bubbling not performed.');
$this->assertTrue(empty($page['#attached']), 'Asset bubbling not performed.');
}
catch (\LogicException $e) {
$this->fail('Exception triggered.');
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment