Commit 3d556b75 authored by alexpott's avatar alexpott

Issue #2350551 by Wim Leers, alexpott, damiankloip, arlinsandbulte: Views...

Issue #2350551 by Wim Leers, alexpott, damiankloip, arlinsandbulte: Views fields that have attached assets are lost when Views output caching is enabled
parent 3d14a7d2
<?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;
}
}
......@@ -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);
}
/**
......
<?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 = [];
}
......@@ -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;
}
}
......@@ -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);
}
......@@ -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;
}
......
......@@ -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;
}
......
......@@ -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()
);
}
}
......@@ -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;
......
......@@ -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() {
......
......@@ -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.');
}
......
<?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;
}
}
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