diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php new file mode 100644 index 0000000000000000000000000000000000000000..c56b421324991220e1bd7c9bebcf2ac667472599 --- /dev/null +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -0,0 +1,106 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\BubbleableMetadata. + */ + +namespace Drupal\Core\Render; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\Cache; + +/** + * Value object used for bubbleable rendering metadata. + * + * @see \Drupal\Core\Render\RendererInterface::render() + */ +class BubbleableMetadata { + + /** + * Cache tags. + * + * @var string[] + */ + protected $tags; + + /** + * Attached assets. + * + * @var string[][] + */ + protected $attached; + + /** + * #post_render_cache metadata. + * + * @var array[] + */ + protected $postRenderCache; + + /** + * Constructs a BubbleableMetadata value object. + * + * @param string[] $tags + * An array of cache tags. + * @param array $attached + * An array of attached assets. + * @param array $post_render_cache + * An array of #post_render_cache metadata. + */ + public function __construct(array $tags = [], array $attached = [], array $post_render_cache = []) { + $this->tags = $tags; + $this->attached = $attached; + $this->postRenderCache = $post_render_cache; + } + + /** + * Merges the values of another bubbleable metadata object with this one. + * + * @param \Drupal\Core\Render\BubbleableMetadata $other + * The other bubbleable metadata object. + * @return static + * A new bubbleable metadata object, with the merged data. + * + * @todo Add unit test for this in + * \Drupal\Tests\Core\Render\BubbleableMetadataTest when + * drupal_merge_attached() no longer is a procedural function and remove + * the '@codeCoverageIgnore' annotation. + */ + public function merge(BubbleableMetadata $other) { + $result = new BubbleableMetadata(); + $result->tags = Cache::mergeTags($this->tags, $other->tags); + $result->attached = drupal_merge_attached($this->attached, $other->attached); + $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache); + return $result; + } + + /** + * Applies the values of this bubbleable metadata object to a render array. + * + * @param array &$build + * A render array. + */ + public function applyTo(array &$build) { + $build['#cache']['tags'] = $this->tags; + $build['#attached'] = $this->attached; + $build['#post_render_cache'] = $this->postRenderCache; + } + + /** + * Creates a bubbleable metadata object with values taken from a render array. + * + * @param array $build + * A render array. + * + * @return static + */ + public static function createFromRenderArray(array $build) { + $meta = new static(); + $meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : []; + $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; + $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : []; + return $meta; + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index 081902ff93481f39772f1dffaeac196e4b72d66b..84426c4e6024309f655f0138b130264a7f2d434a 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\Renderer; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; use Drupal\Core\Routing\RouteMatchInterface; @@ -262,14 +263,7 @@ public function invokePageAttachmentHooks(array &$page) { } // Merge the attachments onto the $page render array. - $page['#attached'] = isset($page['#attached']) ? $page['#attached'] : []; - $page['#post_render_cache'] = isset($page['#post_render_cache']) ? $page['#post_render_cache'] : []; - if (isset($attachments['#attached'])) { - $page['#attached'] = drupal_merge_attached($page['#attached'], $attachments['#attached']); - } - if (isset($attachments['#post_render_cache'])) { - $page['#post_render_cache'] = NestedArray::mergeDeep($page['#post_render_cache'], $attachments['#post_render_cache']); - } + $page = Renderer::mergeBubbleableMetadata($page, $attachments); } /** diff --git a/core/lib/Drupal/Core/Render/RenderStackFrame.php b/core/lib/Drupal/Core/Render/RenderStackFrame.php deleted file mode 100644 index a1443c4fa7d7f67aae74a575900db96e76f87a63..0000000000000000000000000000000000000000 --- a/core/lib/Drupal/Core/Render/RenderStackFrame.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Core\Render\RenderStackFrame. - */ - -namespace Drupal\Core\Render; - -/** - * Value object used for bubbleable rendering metadata. - * - * @see drupal_render() - */ -class RenderStackFrame { - - /** - * Cache tags. - * - * @var array - */ - public $tags = []; - - /** - * Attached assets. - * - * @var array - */ - public $attached = []; - - /** - * #post_render_cache metadata. - * - * @var array - */ - public $postRenderCache = []; - -} diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 39d2090de591524f832433266c1de49fe984bd64..357ee6a03943b00ad960902074739886466648ca 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -7,13 +7,12 @@ namespace Drupal\Core\Render; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheContexts; use Drupal\Core\Cache\CacheFactoryInterface; 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; use Symfony\Component\HttpFoundation\RequestStack; /** @@ -161,7 +160,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset(static::$stack)) { static::$stack = new \SplStack(); } - static::$stack->push(new RenderStackFrame()); + static::$stack->push(new BubbleableMetadata()); // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. @@ -365,12 +364,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // stack frame to collect those additions, merge them back to the element, // and then update the current frame to match the modified element state. do { - static::$stack->push(new RenderStackFrame()); + static::$stack->push(new BubbleableMetadata()); $this->processPostRenderCache($elements); $post_render_additions = static::$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'] = $post_render_additions->postRenderCache; + $elements['#post_render_cache'] = NULL; + BubbleableMetadata::createFromRenderArray($elements) + ->merge($post_render_additions) + ->applyTo($elements); } while (!empty($elements['#post_render_cache'])); if (static::$stack->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); @@ -405,12 +405,12 @@ protected function resetStack() { */ protected function updateStack(&$element) { // The latest frame represents the bubbleable metadata for the subtree. - $frame = static::$stack->top(); + $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. - $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); + $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame); + $updated_frame->applyTo($element); + static::$stack->push($updated_frame); } /** @@ -431,10 +431,7 @@ protected function bubbleStack() { // Merge the current and the parent stack frame. $current = static::$stack->pop(); $parent = static::$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); - static::$stack->push($current); + static::$stack->push($current->merge($parent)); } /** @@ -575,4 +572,14 @@ public function getCacheableRenderArray(array $elements) { ]; } + /** + * {@inheritdoc} + */ + public static function mergeBubbleableMetadata(array $a, array $b) { + $meta_a = BubbleableMetadata::createFromRenderArray($a); + $meta_b = BubbleableMetadata::createFromRenderArray($b); + $meta_a->merge($meta_b)->applyTo($a); + return $a; + } + } diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 579ce5e973a82c0c8c6f98cb3f932a489fc5e161..3625e663853237aae34a06ded97c472cebc5e959 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -88,7 +88,8 @@ public function renderPlain(&$elements); * 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. + * an empty \Drupal\Core\Render\BubbleableMetadata 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 @@ -287,4 +288,20 @@ public function render(&$elements, $is_root_call = FALSE); */ public function getCacheableRenderArray(array $elements); + /** + * Merges the bubbleable rendering metadata o/t 2nd render array with the 1st. + * + * @param array $a + * A render array. + * @param array $b + * A render array. + * + * @return array + * The first render array, modified to also contain the bubbleable rendering + * metadata of the second render array. + * + * @see \Drupal\Core\Render\BubbleableMetadata + */ + public static function mergeBubbleableMetadata(array $a, array $b); + } diff --git a/core/modules/comment/src/CommentPostRenderCache.php b/core/modules/comment/src/CommentPostRenderCache.php index ce64d6d2d01523ae850012c76a051df69e4960f6..206046b582e5de37f2a5a65c8eebdfe414fd7111 100644 --- a/core/modules/comment/src/CommentPostRenderCache.php +++ b/core/modules/comment/src/CommentPostRenderCache.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityFormBuilderInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Render\Renderer; use Drupal\field\Entity\FieldStorageConfig; /** @@ -75,7 +76,7 @@ public function renderForm(array $element, array $context) { $callback = 'comment.post_render_cache:renderForm'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); - $element['#attached'] = drupal_merge_attached($element['#attached'], $form['#attached']); + $element = Renderer::mergeBubbleableMetadata($element, $form); return $element; } diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 0779f51bbf303ac9b03b2c3a0bb6ada084bdacbb..52f0649ee659e39ab99bba03481c1f1f794830c5 100644 --- a/core/modules/filter/src/Element/ProcessedText.php +++ b/core/modules/filter/src/Element/ProcessedText.php @@ -9,7 +9,9 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element\RenderElement; +use Drupal\Core\Render\Renderer; use Drupal\filter\Entity\FilterFormat; use Drupal\filter\Plugin\FilterInterface; @@ -107,44 +109,20 @@ public static function preRenderText($element) { } // Perform filtering. - $cache_tags = array(); - $all_assets = array(); - $all_post_render_cache_callbacks = array(); + $metadata = BubbleableMetadata::createFromRenderArray($element); foreach ($filters as $filter) { if ($filter_must_be_applied($filter)) { $result = $filter->process($text, $langcode); - $all_assets[] = $result->getAssets(); - $cache_tags = Cache::mergeTags($cache_tags, $result->getCacheTags()); - $all_post_render_cache_callbacks[] = $result->getPostRenderCacheCallbacks(); + $metadata = $metadata->merge($result->getBubbleableMetadata()); $text = $result->getProcessedText(); } } - // Filtering done, store in #markup. + // Filtering done, store in #markup, set the updated bubbleable rendering + // metadata, and set the text format's cache tag. $element['#markup'] = $text; - - // Collect all cache tags. - if (isset($element['#cache']) && isset($element['#cache']['tags'])) { - // Merge the original cache tags array. - $cache_tags = Cache::mergeTags($cache_tags, $element['#cache']['tags']); - } - // Prepend the text format's cache tags array. - $cache_tags = Cache::mergeTags($cache_tags, $format->getCacheTags()); - $element['#cache']['tags'] = $cache_tags; - - // Collect all attached assets. - if (isset($element['#attached'])) { - // Prepend the original attached assets array. - array_unshift($all_assets, $element['#attached']); - } - $element['#attached'] = NestedArray::mergeDeepArray($all_assets); - - // Collect all #post_render_cache callbacks. - if (isset($element['#post_render_cache'])) { - // Prepend the original attached #post_render_cache array. - array_unshift($all_assets, $element['#post_render_cache']); - } - $element['#post_render_cache'] = NestedArray::mergeDeepArray($all_post_render_cache_callbacks); + $metadata->applyTo($element); + $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $format->getCacheTags()); return $element; } diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index 5ad48c1bca301812e8d1424b587673733b169ade..f620868aad17d50d3130a2d899c2b359731f7ea6 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; +use Drupal\Core\Render\BubbleableMetadata; /** * Used to return values from a text filter plugin's processing method. @@ -248,4 +249,17 @@ public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) return $this; } + /** + * Returns the attached asset libraries, etc. as a bubbleable metadata object. + * + * @return \Drupal\Core\Render\BubbleableMetadata + */ + public function getBubbleableMetadata() { + return new BubbleableMetadata( + $this->getCacheTags(), + $this->getAssets(), + $this->getPostRenderCacheCallbacks() + ); + } + } diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 2c49b6609387085e6ec8ec07e022dde17101045f..61c9e8406442edf647dcd03a7b15c74807e4c7cb 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -2177,11 +2177,13 @@ public function render() { $element = array( '#theme' => $this->themeFunctions(), '#view' => $this->view, + '#pre_render' => [[$this, 'elementPreRender']], + '#rows' => $rows, // Assigned by reference so anything added in $element['#attached'] will // be available on the view. '#attached' => &$this->view->element['#attached'], - '#pre_render' => [[$this, 'elementPreRender']], - '#rows' => $rows, + '#cache' => &$this->view->element['#cache'], + '#post_render_cache' => &$this->view->element['#post_render_cache'], ); return $element; diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 31eee1143fc069379782b620f8041404fbbcba50..74c86202edbb6b7c2f266ef4fdc7a23eab627af6 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -8,12 +8,15 @@ namespace Drupal\views\Plugin\views\field; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; +use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Renderer; use Drupal\views\Plugin\views\HandlerBase; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ResultRow; @@ -1593,13 +1596,21 @@ protected function documentSelfTokens(&$tokens) { } * {@inheritdoc} */ function theme(ResultRow $values) { + $renderer = $this->getRenderer(); $build = array( '#theme' => $this->themeFunctions(), '#view' => $this->view, '#field' => $this, '#row' => $values, ); - return $this->getRenderer()->render($build); + $output = $renderer->render($build); + + // Set the bubbleable rendering metadata on $view->element. This ensures the + // bubbleable rendering metadata of individual rendered fields is not lost. + // @see \Drupal\Core\Render\Renderer::updateStack() + $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build); + + return $output; } public function themeFunctions() { diff --git a/core/modules/views/src/Tests/Plugin/CacheTest.php b/core/modules/views/src/Tests/Plugin/CacheTest.php index f5cb6bb1cdfa47372665b5194121e5d2635fad3f..68f50f7af0eb8fd0538923f4c56e852a046e4e00 100644 --- a/core/modules/views/src/Tests/Plugin/CacheTest.php +++ b/core/modules/views/src/Tests/Plugin/CacheTest.php @@ -149,8 +149,8 @@ function testHeaderStorage() { drupal_render($output); $this->assertTrue(in_array('views_test_data/test', $output['#attached']['library']), 'Make sure libraries are added for cached views.'); $this->assertEqual(['foo' => 'bar'], $output['#attached']['drupalSettings'], 'Make sure drupalSettings are added for cached views.'); - $this->assertTrue(['views_test_data:1'], $output['#cache']['tags']); - $this->assertTrue(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']); + $this->assertEqual(['views_test_data:1'], $output['#cache']['tags']); + $this->assertEqual(['views_test_data_post_render_cache' => [['foo' => 'bar']]], $output['#post_render_cache']); $this->assertFalse(!empty($view->build_info['pre_render_called']), 'Make sure hook_views_pre_render is not called for the cached view.'); } diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php new file mode 100644 index 0000000000000000000000000000000000000000..351b282637ea56baf7d3c474a202a9371fae613e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -0,0 +1,117 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Render\BubbleableMetadataTest. + */ + +namespace Drupal\Tests\Core\Render; + +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Tests\UnitTestCase; +use Drupal\Core\Render\Element; + +/** + * @coversDefaultClass \Drupal\Core\Render\BubbleableMetadata + * @group Render + */ +class BubbleableMetadataTest extends UnitTestCase { + + /** + * @covers ::apply + * @dataProvider providerTestApply + */ + public function testApply(BubbleableMetadata $metadata, array $render_array, array $expected) { + $this->assertNull($metadata->applyTo($render_array)); + $this->assertEquals($expected, $render_array); + } + + /** + * Provides test data for apply(). + * + * @return array + */ + public function providerTestApply() { + $data = []; + + $empty_metadata = new BubbleableMetadata(); + $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]); + + $empty_render_array = []; + $nonempty_render_array = [ + '#cache' => [ + 'tags' => ['llamas:are:awesome:but:kittens:too'], + ], + '#attached' => [ + 'library' => [ + 'core/jquery', + ], + ], + '#post_render_cache' => [], + ]; + + + $expected_when_empty_metadata = [ + '#cache' => [ + 'tags' => [] + ], + '#attached' => [], + '#post_render_cache' => [], + ]; + $data[] = [$empty_metadata, $empty_render_array, $expected_when_empty_metadata]; + $data[] = [$empty_metadata, $nonempty_render_array, $expected_when_empty_metadata]; + $expected_when_nonempty_metadata = [ + '#cache' => ['tags' => ['foo:bar']], + '#attached' => [ + 'settings' => [ + 'foo' => 'bar', + ], + ], + '#post_render_cache' => [], + ]; + $data[] = [$nonempty_metadata, $empty_render_array, $expected_when_nonempty_metadata]; + $data[] = [$nonempty_metadata, $nonempty_render_array, $expected_when_nonempty_metadata]; + + return $data; + } + + /** + * @covers ::createFromRenderArray + * @dataProvider providerTestCreateFromRenderArray + */ + public function testCreateFromRenderArray(array $render_array, BubbleableMetadata $expected) { + $this->assertEquals($expected, BubbleableMetadata::createFromRenderArray($render_array)); + } + + /** + * Provides test data for createFromRenderArray(). + * + * @return array + */ + public function providerTestCreateFromRenderArray() { + $data = []; + + $empty_metadata = new BubbleableMetadata(); + $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]); + + $empty_render_array = []; + $nonempty_render_array = [ + '#cache' => [ + 'tags' => ['foo:bar'], + ], + '#attached' => [ + 'settings' => [ + 'foo' => 'bar', + ], + ], + '#post_render_cache' => [], + ]; + + + $data[] = [$empty_render_array, $empty_metadata]; + $data[] = [$nonempty_render_array, $nonempty_metadata]; + + return $data; + } + +}