interdiff impossible; taking evasive action reverted: --- b/core/lib/Drupal/Core/Render/Renderer.php +++ a/core/lib/Drupal/Core/Render/Renderer.php @@ -209,36 +209,6 @@ return ''; } - // Render only the children if the internal #render_children property is - // set. - // @see \Drupal\Core\Theme\ThemeManager::render(). - if (isset($elements['#render_children'])) { - // A non-empty #children property takes precedence. This happens only if - // it has been manually set into the render array. - if (!empty($elements['#children'])) { - $children_keys = ['#children']; - } - else { - $children_keys = Element::children($elements); - - if (empty($children_keys)) { - return ''; - } - } - - // Remove all elements except the children because the main level has been - // already rendered when the #render_children is set and therefore they - // should not have any effect on the render children. - $new_elements = array_intersect_key($elements, array_flip($children_keys)); - // Create a new variable that references the render array that was passed - // in. This allows the markup and cache information to be attached after - // rendering the new elements array. - $original_elements = &$elements; - // Change $elements to reference $new_elements. This prevents - // unintentional changes to the render array that was passed in. - $elements = &$new_elements; - } - 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']); @@ -458,8 +428,10 @@ } // 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'])) { - // element have to be rendered there. - if ($theme_is_implemented) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the @@ -468,10 +440,10 @@ $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'])) { - // If #theme is not implemented 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 && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } @@ -495,7 +467,9 @@ // 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'])) { - if (isset($elements['#theme_wrappers'])) { 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 @@ -539,12 +513,6 @@ $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); - // #markup should be always saved to the referenced elements variable to - // prevent re-rendering. - if (isset($original_elements)) { - $original_elements['#markup'] = $elements['#markup']; - } - // We've rendered this element (and its subtree!), now update the context. $context->update($elements); unchanged: --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -236,6 +236,36 @@ protected function doRender(&$elements, $is_root_call = FALSE) { return ''; } + // Render only the children if the internal #render_children property is + // set. + // @see \Drupal\Core\Theme\ThemeManager::render(). + if (isset($elements['#render_children'])) { + // A non-empty #children property takes precedence. This happens only if + // it has been manually set into the render array. + if (!empty($elements['#children'])) { + $children_keys = ['#children']; + } + else { + $children_keys = Element::children($elements); + + if (empty($children_keys)) { + return ''; + } + } + + // Remove all elements except the children because the main level has been + // already rendered when the #render_children is set and therefore they + // should not have any effect on the render children. + $new_elements = array_intersect_key($elements, array_flip($children_keys)); + // Create a new variable that references the render array that was passed + // in. This allows the markup and cache information to be attached after + // rendering the new elements array. + $original_elements = &$elements; + // Change $elements to reference $new_elements. This prevents + // unintentional changes to the render array that was passed in. + $elements = &$new_elements; + } + $context = $this->getCurrentRenderContext(); if (!isset($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."); @@ -428,10 +458,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // 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'])) { + // element have to be rendered there. + if ($theme_is_implemented) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the @@ -440,13 +468,20 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $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'])) { + // If #theme is not implemented 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 && empty($elements['#children'])) { + // Collect all bubbleable metadata from the children so that it can be + // applied to the mainlevel of the $elements. This way correct metadata is + // being applied in case $elements is being re-rendered. + $bubbleable_metadata = BubbleableMetadata::createFromRenderArray($elements); foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); + $children_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($elements[$key]); + $bubbleable_metadata = $bubbleable_metadata->merge($children_bubbleable_metadata); } + $bubbleable_metadata->applyTo($elements); $elements['#children'] = Markup::create($elements['#children']); } @@ -467,9 +502,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // 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'])) { + if (isset($elements['#theme_wrappers'])) { 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 @@ -552,6 +585,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $context->bubble(); $elements['#printed'] = TRUE; + + // #markup should be always saved to the referenced elements variable to + // prevent re-rendering. #cache and #attached ensures that correct cacheable + // metadata is applied for the re-rendered instances. + if (isset($original_elements)) { + $original_elements['#markup'] = $elements['#markup']; + $original_elements['#cache'] = $elements['#cache']; + $original_elements['#attached'] = $elements['#attached']; + $original_elements['#printed'] = $elements['#printed']; + } + return $elements['#markup']; } diff -u b/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php --- b/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -541,33 +541,78 @@ } /** - * Tests that a first render returns the rendered output and a second doesn't. + * Tests rendering same render array twice. * - * (Because of the #printed property.) + * Tests that a first render returns the rendered output and a second doesn't + * because of the #printed property. Also tests that correct metadata has been + * set for re-rendering. * * @covers ::render * @covers ::doRender + * + * @dataProvider providerRenderTwice */ - public function testRenderTwice() { - $build = [ - '#markup' => 'test', - ]; - - $this->assertEquals('test', $this->renderer->renderRoot($build)); + public function testRenderTwice($build) { + $this->assertEquals('kittens', $this->renderer->renderRoot($build)); + $this->assertEquals('kittens', $build['#markup']); + $this->assertEquals(['kittens-147'], $build['#cache']['tags']); $this->assertTrue($build['#printed']); // We don't want to reprint already printed render arrays. $this->assertEquals('', $this->renderer->renderRoot($build)); + } + + /** + * Provides a list of render array iterations. + * + * @return array + */ + public function providerRenderTwice() { + return [ + [ + [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'] + ], + ], + ], + [ + [ + 'child' => [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'], + ], + ], + ], + ], + [ + [ + '#render_children' => TRUE, + 'child' => [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'], + ], + ], + ], + ], + ]; + } + /** + * Ensures that #access is taken in account when rendering #render_children. + */ + public function testRenderChildrenAccess() { $build = [ + '#access' => FALSE, + '#render_children' => TRUE, 'child' => [ '#markup' => 'kittens', ], ]; - $this->assertEquals('kittens', $this->renderer->renderRoot($build)); - $this->assertTrue($build['#printed']); - // We don't want to reprint already printed render arrays. $this->assertEquals('', $this->renderer->renderRoot($build)); }