Issue #2381217 by Wim Leers, dawehner, Fabianx: Views should set cache tags on its render arrays, and bubble the output's cache tags to the cache items written to the Views output cache

8.0.x
catch 2015-03-12 10:16:15 +00:00
parent 94fb2afd31
commit 9309d3d9f5
20 changed files with 849 additions and 95 deletions

View File

@ -544,11 +544,6 @@ class Renderer implements RendererInterface {
$data = $this->getCacheableRenderArray($elements);
// Cache tags are cached, but we also want to associate the "rendered" cache
// tag. This allows us to invalidate the entire render cache, regardless of
// the cache bin.
$data['#cache']['tags'][] = 'rendered';
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
$expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : Cache::PERMANENT;
$cache = $this->cacheFactory->get($bin);
@ -690,7 +685,7 @@ class Renderer implements RendererInterface {
'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
],
];
$cache->set($pre_bubbling_cid, $redirect_data, $expire, $redirect_data['#cache']['tags']);
$cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
}
// Current cache contexts incomplete: this request only uses a subset of
@ -711,7 +706,7 @@ class Renderer implements RendererInterface {
$data['#cache']['contexts'] = $merged_cache_contexts;
}
}
$cache->set($cid, $data, $expire, $data['#cache']['tags']);
$cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
}
/**

View File

@ -7,6 +7,11 @@
namespace Drupal\node\Tests\Views;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\views\Tests\AssertViewsCacheTagsTrait;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
@ -18,6 +23,14 @@ use Drupal\views\Views;
*/
class FrontPageTest extends ViewTestBase {
use AssertPageCacheContextsAndTagsTrait;
use AssertViewsCacheTagsTrait;
/**
* {@inheritdoc}
*/
protected $dumpHeaders = TRUE;
/**
* The entity storage for nodes.
*
@ -35,7 +48,8 @@ class FrontPageTest extends ViewTestBase {
protected function setUp() {
parent::setUp();
$this->nodeStorage = $this->container->get('entity.manager')->getStorage('node');
$this->nodeStorage = $this->container->get('entity.manager')
->getStorage('node');
}
/**
@ -173,4 +187,166 @@ class FrontPageTest extends ViewTestBase {
$this->assertPattern('/class=".+view-frontpage/', 'Frontpage view was rendered');
}
/**
* Tests the cache tags when using the "none" cache plugin.
*/
public function testCacheTagsWithCachePluginNone() {
$this->enablePageCaching();
$this->assertFrontPageViewCacheTags(FALSE);
}
/**
* Tests the cache tags when using the "tag" cache plugin.
*/
public function testCacheTagsWithCachePluginTag() {
$this->enablePageCaching();
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
'type' => 'tag',
]);
$view->save();
$this->assertFrontPageViewCacheTags(TRUE);
}
/**
* Tests the cache tags when using the "time" cache plugin.
*/
public function testCacheTagsWithCachePluginTime() {
$this->enablePageCaching();
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
'type' => 'time',
'options' => [
'results_lifespan' => 3600,
'output_lifespan' => 3600,
],
]);
$view->save();
$this->assertFrontPageViewCacheTags(TRUE);
}
/**
* Tests the cache tags on the front page.
*
* @param bool $do_assert_views_caches
* Whether to check Views' result & output caches.
*/
protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
$view = Views::getView('frontpage');
$view->setDisplay('page_1');
$cache_contexts = [];
// Test before there are any nodes.
$empty_node_listing_cache_tags = [
'config:views.view.frontpage',
'node_list',
];
$this->assertViewsCacheTags(
$view,
$empty_node_listing_cache_tags,
$do_assert_views_caches,
$empty_node_listing_cache_tags
);
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
Cache::mergeTags($empty_node_listing_cache_tags, ['rendered'])
);
// Create some nodes on the frontpage view. Add more than 10 nodes in order
// to enable paging.
$this->drupalCreateContentType(['type' => 'article']);
for ($i = 0; $i < 15; $i++) {
$node = Node::create([
'body' => [
[
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
]
],
'type' => 'article',
'created' => $i,
'title' => $this->randomMachineName(8),
'nid' => $i + 1,
]);
$node->enforceIsNew(TRUE);
$node->save();
}
$cache_contexts = Cache::mergeContexts($cache_contexts, [
'theme',
'timezone',
'user.roles'
]);
// First page.
$first_page_result_cache_tags = [
'config:views.view.frontpage',
'node_list',
'node:6',
'node:7',
'node:8',
'node:9',
'node:10',
'node:11',
'node:12',
'node:13',
'node:14',
'node:15',
];
$first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, [
'config:filter.format.plain_text',
'node_view',
'user_view',
'user:0',
]);
$view->setDisplay('page_1');
$view->setCurrentPage(0);
$this->assertViewsCacheTags(
$view,
$first_page_result_cache_tags,
$do_assert_views_caches,
$first_page_output_cache_tags
);
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
Cache::mergeTags($first_page_output_cache_tags, ['rendered'])
);
// Second page.
$this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), $cache_contexts, [
// The cache tags for the listed nodes.
'node:1',
'node:2',
'node:3',
'node:4',
'node:5',
// The rest.
'config:filter.format.plain_text',
'config:views.view.frontpage',
'node_list',
'node_view',
'user_view',
'user:0',
'rendered',
]);
// Let's update a node title on the first page and ensure that the page
// cache entry invalidates.
$node = Node::load(10);
$title = $node->getTitle() . 'a';
$node->setTitle($title);
$node->save();
$this->drupalGet(Url::fromRoute('view.frontpage.page_1'));
$this->assertText($title);
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Url;
/**
* Provides test assertions for testing page-level cache contexts & tags.
*
* Can be used by test classes that extend \Drupal\simpletest\WebTestBase.
*/
trait AssertPageCacheContextsAndTagsTrait {
/**
* Enables page caching.
*/
protected function enablePageCaching() {
$config = $this->config('system.performance');
$config->set('cache.page.use_internal', 1);
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Asserts page cache miss, then hit for the given URL; checks cache headers.
*
* @param \Drupal\Core\Url $url
* The URL to test.
* @param string[] $expected_contexts
* The expected cache contexts for the given URL.
* @param string[] $expected_tags
* The expected cache tags for the given URL.
*/
protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
$absolute_url = $url->setAbsolute()->toString();
sort($expected_contexts);
sort($expected_tags);
$get_cache_header_values = function ($header_name) {
$header_value = $this->drupalGetHeader($header_name);
if (empty($header_value)) {
return [];
}
else {
return explode(' ', $header_value);
}
};
// Assert cache miss + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
$actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
$actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
$this->assertIdentical($actual_contexts, $expected_contexts);
if ($actual_contexts !== $expected_contexts) {
debug(array_diff($actual_contexts, $expected_contexts));
}
$this->assertIdentical($actual_tags, $expected_tags);
if ($actual_tags !== $expected_tags) {
debug(array_diff($actual_tags, $expected_tags));
}
// Assert cache hit + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
$actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
$this->assertIdentical($actual_contexts, $expected_contexts);
if ($actual_contexts !== $expected_contexts) {
debug(array_diff($actual_contexts, $expected_contexts));
}
$this->assertIdentical($actual_tags, $expected_tags);
if ($actual_tags !== $expected_tags) {
debug(array_diff($actual_tags, $expected_tags));
}
// Assert page cache item + expected cache tags.
$cid_parts = array($url->setAbsolute()->toString(), 'html');
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$this->assertEqual($cache_entry->tags, $expected_tags);
if ($cache_entry->tags !== $expected_tags) {
debug(array_diff($cache_entry->tags, $expected_tags));
}
}
}

View File

@ -7,9 +7,7 @@
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Cache\Cache;
/**
* Enables the page cache and tests its cache tags in various scenarios.
@ -21,6 +19,8 @@ use Drupal\Core\Cache\Cache;
*/
class PageCacheTagsIntegrationTest extends WebTestBase {
use AssertPageCacheContextsAndTagsTrait;
protected $profile = 'standard';
protected $dumpHeaders = TRUE;
@ -31,10 +31,7 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
protected function setUp() {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.use_internal', 1);
$config->set('cache.page.max_age', 300);
$config->save();
$this->enablePageCaching();
}
/**
@ -128,46 +125,10 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
'config:system.menu.footer',
'config:system.menu.main',
'config:system.site',
'comment_list',
'node_list',
'config:views.view.comments_recent',
));
}
/**
* Asserts page cache miss, then hit for the given URL; checks cache headers.
*
* @param \Drupal\Core\Url $url
* The URL to test.
* @param string[] $expected_contexts
* The expected cache contexts for the given URL.
* @param string[] $expected_tags
* The expected cache tags for the given URL.
*/
protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
$absolute_url = $url->setAbsolute()->toString();
sort($expected_contexts);
sort($expected_tags);
// Assert cache miss + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
$actual_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
$actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
$this->assertIdentical($actual_contexts, $expected_contexts);
$this->assertIdentical($actual_tags, $expected_tags);
// Assert cache hit + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$actual_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
$actual_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
$this->assertIdentical($actual_contexts, $expected_contexts);
$this->assertIdentical($actual_tags, $expected_tags);
// Assert page cache item + expected cache tags.
$cid_parts = array($url->setAbsolute()->toString(), 'html');
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$this->assertEqual($cache_entry->tags, $expected_tags);
}
}

View File

@ -191,7 +191,7 @@ abstract class CachePluginBase extends PluginBase {
// that is used to render the view for this request and rendering does
// not happen twice.
$this->storage = $this->view->display_handler->output = $this->renderer->getCacheableRenderArray($output);
\Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->getCacheTags());
\Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), Cache::mergeTags($this->storage['#cache']['tags'], ['rendered']));
break;
}
}
@ -239,9 +239,6 @@ abstract class CachePluginBase extends PluginBase {
/**
* Clear out cached data for a view.
*
* We're just going to nuke anything related to the view, regardless of display,
* to be sure that we catch everything. Maybe that's a bad idea.
*/
public function cacheFlush() {
Cache::invalidateTags($this->view->storage->getCacheTags());
@ -301,12 +298,18 @@ abstract class CachePluginBase extends PluginBase {
'langcode' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
'base_url' => $GLOBALS['base_url'],
);
foreach (array('exposed_info', 'page', 'sort', 'order', 'items_per_page', 'offset') as $key) {
foreach (array('exposed_info', 'sort', 'order') as $key) {
if ($this->view->getRequest()->query->has($key)) {
$key_data[$key] = $this->view->getRequest()->query->get($key);
}
}
$key_data['pager'] = [
'page' => $this->view->getCurrentPage(),
'items_per_page' => $this->view->getItemsPerPage(),
'offset' => $this->view->getOffset(),
];
$this->resultsKey = $this->view->storage->id() . ':' . $this->displayHandler->display['id'] . ':results:' . hash('sha256', serialize($key_data));
}
@ -343,18 +346,21 @@ abstract class CachePluginBase extends PluginBase {
* @return string[]
* An array of cache tags based on the current view.
*/
protected function getCacheTags() {
public function getCacheTags() {
$tags = $this->view->storage->getCacheTags();
// The list cache tags for the entity types listed in this view.
$entity_information = $this->view->query->getEntityTableInfo();
if (!empty($entity_information)) {
// Add the list cache tags for each entity type used by this view.
foreach (array_keys($entity_information) as $entity_type) {
$tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($entity_type)->getListCacheTags());
foreach ($entity_information as $table => $metadata) {
$tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($metadata['entity_type'])->getListCacheTags());
}
}
$tags = Cache::mergeTags($tags, $this->view->getQuery()->getCacheTags());
return $tags;
}

View File

@ -2126,6 +2126,15 @@ abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInte
'#post_render_cache' => &$this->view->element['#post_render_cache'],
);
if (!isset($element['#cache'])) {
$element['#cache'] = [];
}
$element['#cache'] += ['tags' => []];
// If the output is a render array, add cache tags, regardless of whether
// caching is enabled or not; cache tags must always be set.
$element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $this->view->getCacheTags());
return $element;
}

View File

@ -312,6 +312,13 @@ abstract class QueryPluginBase extends PluginBase {
return $entity_tables;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}
/**

View File

@ -8,6 +8,7 @@
namespace Drupal\views\Plugin\views\query;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
@ -1537,6 +1538,23 @@ class Sql extends QueryPluginBase {
}
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = [];
// Add cache tags for each row, if there is an entity associated with it.
if (!$this->hasAggregate) {
foreach ($this->view->result as $row) {
if ($row->_entity) {
$tags = Cache::mergeTags($row->_entity->getCacheTags(), $tags);
}
}
}
return $tags;
}
public function addSignature(ViewExecutable $view) {
$view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
}

View File

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\views\Tests\AssertViewsCacheTagsTrait.
*/
namespace Drupal\views\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\views\ViewExecutable;
use Symfony\Component\HttpFoundation\Request;
trait AssertViewsCacheTagsTrait {
/**
* Asserts a view's result & output cache items' cache tags.
*
* @param \Drupal\views\ViewExecutable $view
* The view to test, must have caching enabled.
* @param null|string[] $expected_results_cache
* NULL when expecting no results cache item, a set of cache tags expected
* to be set on the results cache item otherwise.
* @param bool $views_caching_is_enabled
* Whether to expect an output cache item. If TRUE, the cache tags must
* match those in $expected_render_array_cache_tags.
* @param string[] $expected_render_array_cache_tags
* A set of cache tags expected to be set on the built view's render array.
*
* @return array
* The render array
*/
protected function assertViewsCacheTags(ViewExecutable $view, $expected_results_cache, $views_caching_is_enabled, array $expected_render_array_cache_tags) {
$build = $view->preview();
// Ensure the current request is a GET request so that render caching is
// active for direct rendering of views, just like for actual requests.
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request_stack = \Drupal::service('request_stack');
$request_stack->push(new Request());
\Drupal::service('renderer')->renderRoot($build);
$request_stack->pop();
// Render array cache tags.
$this->pass('Checking render array cache tags.');
sort($expected_render_array_cache_tags);
$this->assertEqual($build['#cache']['tags'], $expected_render_array_cache_tags);
if ($views_caching_is_enabled) {
$this->pass('Checking Views results cache item cache tags.');
/** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
$cache_plugin = $view->display_handler->getPlugin('cache');
// Results cache.
$results_cache_item = \Drupal::cache('data')->get($cache_plugin->generateResultsKey());
if (is_array($expected_results_cache)) {
$this->assertTrue($results_cache_item, 'Results cache item found.');
if ($results_cache_item) {
sort($expected_results_cache);
$this->assertEqual($results_cache_item->tags, $expected_results_cache);
}
}
else {
$this->assertFalse($results_cache_item, 'Results cache item not found.');
}
// Output cache.
$this->pass('Checking Views output cache item cache tags.');
$output_cache_item = \Drupal::cache('render')->get($cache_plugin->generateOutputKey());
if ($views_caching_is_enabled === TRUE) {
$this->assertTrue($output_cache_item, 'Output cache item found.');
if ($output_cache_item) {
$this->assertEqual($output_cache_item->tags, Cache::mergeTags($expected_render_array_cache_tags, ['rendered']));
}
}
else {
$this->assertFalse($output_cache_item, 'Output cache item not found.');
}
}
$view->destroy();
return $build;
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\views\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Url;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\views\Views;
/**
@ -18,6 +19,9 @@ use Drupal\views\Views;
*/
class GlossaryTest extends ViewTestBase {
use AssertPageCacheContextsAndTagsTrait;
use AssertViewsCacheTagsTrait;
/**
* Modules to enable.
*
@ -39,6 +43,7 @@ class GlossaryTest extends ViewTestBase {
'a' => 3,
'l' => 6,
);
$nodes_by_char = [];
foreach ($nodes_per_char as $char => $count) {
$setting = array(
'type' => $type->id()
@ -46,7 +51,8 @@ class GlossaryTest extends ViewTestBase {
for ($i = 0; $i < $count; $i++) {
$node = $setting;
$node['title'] = $char . $this->randomString(3);
$this->drupalCreateNode($node);
$node = $this->drupalCreateNode($node);
$nodes_by_char[$char][] = $node;
}
}
@ -77,6 +83,16 @@ class GlossaryTest extends ViewTestBase {
$result_count = trim(str_replace(array('|', '(', ')'), '', (string) $result[0]));
$this->assertEqual($result_count, $count, 'The expected number got rendered.');
}
// Verify cache tags.
$this->enablePageCaching();
$this->assertPageCacheContextsAndTags(Url::fromRoute('view.glossary.page_1'), [], [
'config:views.view.glossary',
'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
'node_list',
'user_list',
'rendered',
]);
}
}

View File

@ -44,7 +44,6 @@ class CacheTest extends PluginTestBase {
* @see views_plugin_cache_time
*/
public function testTimeResultCaching() {
// Create a basic result which just 2 results.
$view = Views::getView('test_cache');
$view->setDisplay();
$view->display_handler->overrideOption('cache', array(
@ -55,6 +54,7 @@ class CacheTest extends PluginTestBase {
)
));
// Test the default (non-paged) display.
$this->executeView($view);
// Verify the result.
$this->assertEqual(5, count($view->result), 'The number of returned rows match.');
@ -67,7 +67,19 @@ class CacheTest extends PluginTestBase {
);
db_insert('views_test_data')->fields($record)->execute();
// The Result should be the same as before, because of the caching.
// The result should be the same as before, because of the caching. (Note
// that views_test_data records don't have associated cache tags, and hence
// the results cache items aren't invalidated.)
$view->destroy();
$this->executeView($view);
// Verify the result.
$this->assertEqual(5, count($view->result), 'The number of returned rows match.');
}
/**
* Tests result caching with a pager.
*/
public function testTimeResultCachingWithPager() {
$view = Views::getView('test_cache');
$view->setDisplay();
$view->display_handler->overrideOption('cache', array(
@ -78,9 +90,31 @@ class CacheTest extends PluginTestBase {
)
));
$mapping = ['views_test_data_name' => 'name'];
$view->setDisplay('page_1');
$view->setCurrentPage(0);
$this->executeView($view);
// Verify the result.
$this->assertEqual(5, count($view->result), 'The number of returned rows match.');
$this->assertIdenticalResultset($view, [['name' => 'John'], ['name' => 'George']], $mapping);
$view->destroy();
$view->setDisplay('page_1');
$view->setCurrentPage(1);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['name' => 'Ringo'], ['name' => 'Paul']], $mapping);
$view->destroy();
$view->setDisplay('page_1');
$view->setCurrentPage(0);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['name' => 'John'], ['name' => 'George']], $mapping);
$view->destroy();
$view->setDisplay('page_1');
$view->setCurrentPage(2);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['name' => 'Meredith']], $mapping);
$view->destroy();
}
/**
@ -149,7 +183,8 @@ class CacheTest extends PluginTestBase {
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->assertEqual(['views_test_data:1'], $output['#cache']['tags']);
// Note: views_test_data_views_pre_render() adds some cache tags.
$this->assertEqual(['config:views.view.test_cache_header_storage', '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.');
}

View File

@ -0,0 +1,203 @@
<?php
/**
* @file
* Contains \Drupal\views\Tests\RenderCacheIntegrationTest.
*/
namespace Drupal\views\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\views\Views;
/**
* Tests the general integration between Views and the render cache.
*
* @group views
*/
class RenderCacheIntegrationTest extends ViewUnitTestBase {
use AssertViewsCacheTagsTrait;
/**
* {@inheritdoc}
*/
public static $testViews = ['entity_test_fields', 'entity_test_row'];
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
}
/**
* Tests a field-based view's cache tags when using the "none" cache plugin.
*/
public function testFieldBasedViewCacheTagsWithCachePluginNone() {
$this->assertCacheTagsForFieldBasedView(FALSE);
}
/**
* Tests a field-based view's cache tags when using the "tag" cache plugin.
*/
public function testFieldBasedViewCacheTagsWithCachePluginTag() {
$view = Views::getview('entity_test_fields');
$view->getDisplay()->overrideOption('cache', [
'type' => 'tag',
]);
$view->save();
$this->assertCacheTagsForFieldBasedView(TRUE);
}
/**
* Tests a field-based view's cache tags when using the "time" cache plugin.
*/
public function testFieldBasedViewCacheTagsWithCachePluginTime() {
$view = Views::getview('entity_test_fields');
$view->getDisplay()->overrideOption('cache', [
'type' => 'time',
'options' => [
'results_lifespan' => 3600,
'output_lifespan' => 3600,
],
]);
$view->save();
$this->assertCacheTagsForFieldBasedView(TRUE);
}
/**
* Tests cache tags on output & result cache items for a field-based view.
*
* @param bool $do_assert_views_caches
* Whether to check Views' result & output caches.
*/
protected function assertCacheTagsForFieldBasedView($do_assert_views_caches) {
$this->pass('Checking cache tags for field-based view.');
$view = Views::getview('entity_test_fields');
// Empty result (no entities yet).
$base_tags = ['config:views.view.entity_test_fields', 'entity_test_list'];
$this->assertViewsCacheTags($view, $base_tags, $do_assert_views_caches, $base_tags);
// Non-empty result (1 entity).
$entities[] = $entity = EntityTest::create();
$entity->save();
$tags_with_entity = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
$this->assertViewsCacheTags($view, $tags_with_entity, $do_assert_views_caches, $tags_with_entity);
// Paged result (more entities than the items-per-page limit).
for ($i = 0; $i < 5; $i++) {
$entities[] = $entity = EntityTest::create();
$entity->save();
}
// Page 1.
$tags_page_1 = Cache::mergeTags($base_tags, $entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
$this->assertViewsCacheTags($view, $tags_page_1, $do_assert_views_caches, $tags_page_1);
$view->destroy();
// Page 2.
$view->setCurrentPage(1);
$tags_page_2 = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
$this->assertViewsCacheTags($view, $tags_page_2, $do_assert_views_caches, $tags_page_2);
$view->destroy();
// Ensure that invalidation works on both pages.
$view->setCurrentPage(1);
$entities[0]->name->value = $random_name = $this->randomMachineName();
$entities[0]->save();
$build = $this->assertViewsCacheTags($view, $tags_page_2, $do_assert_views_caches, $tags_page_2);
$this->assertTrue(strpos($build['#markup'], $random_name) !== FALSE);
$view->destroy();
$view->setCurrentPage(0);
$entities[1]->name->value = $random_name = $this->randomMachineName();
$entities[1]->save();
$build = $this->assertViewsCacheTags($view, $tags_page_1, $do_assert_views_caches, $tags_page_1);
$this->assertTrue(strpos($build['#markup'], $random_name) !== FALSE);
}
/**
* Tests a entity-based view's cache tags when using the "none" cache plugin.
*/
public function testEntityBasedViewCacheTagsWithCachePluginNone() {
$this->assertCacheTagsForEntityBasedView(FALSE);
}
/**
* Tests a entity-based view's cache tags when using the "tag" cache plugin.
*/
public function testEntityBasedViewCacheTagsWithCachePluginTag() {
$view = Views::getview('entity_test_row');
$view->getDisplay()->overrideOption('cache', [
'type' => 'tag',
]);
$view->save();
$this->assertCacheTagsForEntityBasedView(TRUE);
}
/**
* Tests a entity-based view's cache tags when using the "time" cache plugin.
*/
public function testEntityBasedViewCacheTagsWithCachePluginTime() {
$view = Views::getview('entity_test_row');
$view->getDisplay()->overrideOption('cache', [
'type' => 'time',
'options' => [
'results_lifespan' => 3600,
'output_lifespan' => 3600,
],
]);
$view->save();
$this->assertCacheTagsForEntityBasedView(TRUE);
}
/**
* Tests cache tags on output & result cache items for an entity-based view.
*/
protected function assertCacheTagsForEntityBasedView($do_assert_views_caches) {
$this->pass('Checking cache tags for entity-based view.');
$view = Views::getview('entity_test_row');
// Empty result (no entities yet).
$base_tags = $base_render_tags = ['config:views.view.entity_test_row', 'entity_test_list'];
$this->assertViewsCacheTags($view, $base_tags, $do_assert_views_caches, $base_tags);
// Non-empty result (1 entity).
$entities[] = $entity = EntityTest::create();
$entity->save();
$result_tags_with_entity = Cache::mergeTags($base_tags, $entities[0]->getCacheTags());
$render_tags_with_entity = Cache::mergeTags($base_render_tags, $entities[0]->getCacheTags(), ['entity_test_view']);
$this->assertViewsCacheTags($view, $result_tags_with_entity, $do_assert_views_caches, $render_tags_with_entity);
// Paged result (more entities than the items-per-page limit).
for ($i = 0; $i < 5; $i++) {
$entities[] = $entity = EntityTest::create();
$entity->save();
}
$new_entities_cache_tags = Cache::mergeTags($entities[1]->getCacheTags(), $entities[2]->getCacheTags(), $entities[3]->getCacheTags(), $entities[4]->getCacheTags(), $entities[5]->getCacheTags());
$result_tags_page_1 = Cache::mergeTags($base_tags, $new_entities_cache_tags);
$render_tags_page_1 = Cache::mergeTags($base_render_tags, $new_entities_cache_tags, ['entity_test_view']);
$this->assertViewsCacheTags($view, $result_tags_page_1, $do_assert_views_caches, $render_tags_page_1);
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\views;
use Drupal\Component\Utility\String;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Form\FormState;
use Drupal\Core\Routing\RouteProviderInterface;
@ -1398,6 +1399,7 @@ class ViewExecutable implements \Serializable {
}
$this->display_handler->output = $this->display_handler->render();
if ($cache) {
$cache->cacheSet('output');
}
@ -1423,6 +1425,22 @@ class ViewExecutable implements \Serializable {
return $this->display_handler->output;
}
/**
* Gets the cache tags associated with the executed view.
*
* Note: The cache plugin controls the used tags, so you can override it, if
* needed.
*
* @return string[]
* An array of cache tags.
*/
public function getCacheTags() {
$this->initDisplay();
/** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
$cache = $this->display_handler->getPlugin('cache');
return $cache->getCacheTags();
}
/**
* Builds the render array outline for the given display.
*

View File

@ -0,0 +1,74 @@
langcode: und
status: true
dependencies: { }
id: entity_test_fields
label: ''
module: views
description: ''
tag: ''
base_table: entity_test
base_field: nid
core: '8'
display:
default:
display_options:
access:
type: none
cache:
type: none
exposed_form:
type: basic
fields:
id:
alter:
alter_text: false
element_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: numeric
entity_type: entity_test
entity_field: id
id: id
table: entity_test
field: id
name:
alter:
alter_text: false
ellipsis: true
html: false
make_link: false
strip_tags: false
trim: false
word_boundary: true
empty_zero: false
field: name
hide_empty: false
id: name
table: entity_test
plugin_id: standard
entity_type: entity_test
entity_field: name
sorts:
id:
table: entity_test
id: id
field: id
plugin_id: standard
entity_type: entity_test
entity_field: id
order: desc
pager:
type: full
options:
items_per_page: 5
style:
type: default
row:
type: fields
display_plugin: default
display_title: Master
id: default
position: 0

View File

@ -0,0 +1,41 @@
langcode: und
status: true
dependencies: { }
id: entity_test_row
label: ''
module: views
description: ''
tag: ''
base_table: entity_test
base_field: nid
core: '8'
display:
default:
display_options:
access:
type: none
cache:
type: none
exposed_form:
type: basic
sorts:
id:
table: entity_test
id: id
field: id
plugin_id: standard
entity_type: entity_test
entity_field: id
order: desc
pager:
type: full
options:
items_per_page: 5
style:
type: default
row:
type: 'entity:entity_test'
display_plugin: default
display_title: Master
id: default
position: 0

View File

@ -42,3 +42,15 @@ display:
table: views_test_data
field: id
relationship: none
page_1:
display_plugin: page
id: page_1
display_options:
defaults:
pager: false
pager:
type: full
options:
items_per_page: 2

View File

@ -113,7 +113,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => ['foo'],
'tags' => ['rendered'],
'tags' => [],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -141,7 +141,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -164,7 +164,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
'#post_render_cache' => [],
'#markup' => '',
@ -204,7 +204,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => ['bar', 'baz', 'foo'],
'tags' => ['rendered'],
'tags' => [],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -243,17 +243,14 @@ class RendererBubblingTest extends RendererTestBase {
// The keys + contexts this redirects to.
'keys' => ['parent'],
'contexts' => ['bar', 'foo'],
// The 'rendered' cache tag is also present for the redirecting cache
// item, to ensure it is considered to be part of the render cache
// and thus invalidated along with everything else.
'tags' => ['dee', 'fiddle', 'har', 'rendered', 'yar'],
'tags' => ['dee', 'fiddle', 'har', 'yar'],
],
],
'parent:bar:foo' => [
'#attached' => [],
'#cache' => [
'contexts' => ['bar', 'foo'],
'tags' => ['dee', 'fiddle', 'har', 'yar', 'rendered'],
'tags' => ['dee', 'fiddle', 'har', 'yar'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -334,14 +331,14 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'keys' => ['parent'],
'contexts' => ['user.roles'],
'tags' => ['a', 'b', 'rendered'],
'tags' => ['a', 'b'],
],
]);
$this->assertRenderCacheItem('parent:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => ['user.roles'],
'tags' => ['a', 'b', 'rendered'],
'tags' => ['a', 'b'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -357,14 +354,14 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'keys' => ['parent'],
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'rendered'],
'tags' => ['a', 'b', 'c'],
],
]);
$this->assertRenderCacheItem('parent:foo:r.B', [
'#attached' => [],
'#cache' => [
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'rendered'],
'tags' => ['a', 'b', 'c'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -388,14 +385,14 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'keys' => ['parent'],
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'rendered'],
'tags' => ['a', 'b', 'c'],
],
]);
$this->assertRenderCacheItem('parent:foo:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => ['foo', 'user.roles'],
'tags' => ['a', 'b', 'rendered'],
'tags' => ['a', 'b'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -411,7 +408,7 @@ class RendererBubblingTest extends RendererTestBase {
'#cache' => [
'keys' => ['parent'],
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'd', 'rendered'],
'tags' => ['a', 'b', 'c', 'd'],
],
];
$this->assertRenderCacheItem('parent', $final_parent_cache_item);
@ -419,7 +416,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'd', 'rendered'],
'tags' => ['a', 'b', 'c', 'd'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -434,7 +431,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'rendered'],
'tags' => ['a', 'b'],
],
'#post_render_cache' => [],
'#markup' => 'parent',
@ -449,7 +446,7 @@ class RendererBubblingTest extends RendererTestBase {
'#attached' => [],
'#cache' => [
'contexts' => ['bar', 'foo', 'user.roles'],
'tags' => ['a', 'b', 'c', 'rendered'],
'tags' => ['a', 'b', 'c'],
],
'#post_render_cache' => [],
'#markup' => 'parent',

View File

@ -91,7 +91,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
'#post_render_cache' => $test_element['#post_render_cache'],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
$this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@ -227,7 +227,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
@ -322,7 +322,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
@ -348,7 +348,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
@ -462,7 +462,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
$this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@ -560,7 +560,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
$this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@ -588,7 +588,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
$this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
@ -619,7 +619,7 @@ class RendererPostRenderCacheTest extends RendererTestBase {
],
'#cache' => [
'contexts' => [],
'tags' => ['rendered'],
'tags' => [],
],
];
$this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');

View File

@ -7,6 +7,7 @@
namespace Drupal\Tests\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
@ -567,9 +568,12 @@ class RendererTest extends RendererTestBase {
'render_cache_tag',
'render_cache_tag_child:1',
'render_cache_tag_child:2',
'rendered',
];
$this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.');
// The cache item also has a 'rendered' cache tag.
$cache_item = $this->cacheFactory->get('render')->get('render_cache_test');
$this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\Tests\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Renderer;
@ -152,6 +153,7 @@ class RendererTestBase extends UnitTestCase {
$this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', $cid));
if ($cached !== FALSE) {
$this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', $cid));
$this->assertSame(Cache::mergeTags($data['#cache']['tags'], ['rendered']), $cached->tags, "The cache item's cache tags also has the 'rendered' cache tag.");
}
}