diff --git a/core/lib/Drupal/Core/Entity/EntityListBuilder.php b/core/lib/Drupal/Core/Entity/EntityListBuilder.php index 95b6ba1..676f9f8 100644 --- a/core/lib/Drupal/Core/Entity/EntityListBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityListBuilder.php @@ -218,6 +218,9 @@ public function render() { '#title' => $this->getTitle(), '#rows' => array(), '#empty' => $this->t('There is no @label yet.', array('@label' => $this->entityType->getLabel())), + '#cache' => [ + 'contexts' => $this->entityType->getListCacheContexts(), + ], ); foreach ($this->load() as $entity) { if ($row = $this->buildRow($entity)) { diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 54a10d8..3d257fd 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -203,11 +203,18 @@ class EntityType implements EntityTypeInterface { protected $field_ui_base_route; /** + * The list cache contexts for this entity type. + * + * @var string[] + */ + protected $list_cache_contexts = []; + + /** * The list cache tags for this entity type. * - * @var array + * @var string[] */ - protected $list_cache_tags = array(); + protected $list_cache_tags = []; /** * Constructs a new EntityType. @@ -696,6 +703,13 @@ public function getGroupLabel() { /** * {@inheritdoc} */ + public function getListCacheContexts() { + return $this->list_cache_contexts; + } + + /** + * {@inheritdoc} + */ public function getListCacheTags() { return $this->list_cache_tags; } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index 49302d3..c33c6ec 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -656,6 +656,17 @@ public function getUriCallback(); public function setUriCallback($callback); /** + * The list cache contexts associated with this entity type. + * + * Enables code listing entities of this type to ensure that rendered listings + * are varied as necessary, typically to ensure users of role A see other + * entities listed as users of role B. + * + * @return string[] + */ + public function getListCacheContexts(); + + /** * The list cache tags associated with this entity type. * * Enables code listing entities of this type to ensure that newly created diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 6e33d27..fa0950e 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -45,6 +45,7 @@ * revision_table = "node_revision", * revision_data_table = "node_field_revision", * translatable = TRUE, + * list_cache_contexts = { "node_view_grants" }, * entity_keys = { * "id" = "nid", * "revision" = "vid", diff --git a/core/modules/node/src/Tests/NodeCacheTagsTest.php b/core/modules/node/src/Tests/NodeCacheTagsTest.php index 6da02d0..c18f15b 100644 --- a/core/modules/node/src/Tests/NodeCacheTagsTest.php +++ b/core/modules/node/src/Tests/NodeCacheTagsTest.php @@ -58,4 +58,11 @@ protected function getAdditionalCacheTagsForEntity(EntityInterface $node) { return array('user:' . $node->getOwnerId(), 'user_view'); } + /** + * {@inheritdoc} + */ + protected function getAdditionalCacheContextsForEntityListing() { + return ['node_view_grants']; + } + } diff --git a/core/modules/node/src/Tests/NodeListBuilderTest.php b/core/modules/node/src/Tests/NodeListBuilderTest.php new file mode 100644 index 0000000..beadf0e --- /dev/null +++ b/core/modules/node/src/Tests/NodeListBuilderTest.php @@ -0,0 +1,40 @@ +installEntitySchema('node'); + } + + + public function testCacheContexts() { + /** @var \Drupal\Core\Entity\EntityListBuilderInterface $list_builder */ + $list_builder = $this->container->get('entity.manager')->getListBuilder('node'); + + $build = $list_builder->render(); + $this->container->get('renderer')->render($build); + + $this->assertEqual(['node_view_grants'], $build['#cache']['contexts']); + } + +} diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php index c5dee98..61fe5eb 100644 --- a/core/modules/node/src/Tests/Views/FrontPageTest.php +++ b/core/modules/node/src/Tests/Views/FrontPageTest.php @@ -241,7 +241,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) { $view = Views::getView('frontpage'); $view->setDisplay('page_1'); - $cache_contexts = []; + $cache_contexts = ['node_view_grants', 'language']; // Test before there are any nodes. $empty_node_listing_cache_tags = [ diff --git a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php index a27b39c..e33ee24 100644 --- a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php @@ -159,6 +159,16 @@ protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) { /** * Returns the additional cache tags for the tested entity's listing by type. * + * @return string[] + * An array of the additional cache contexts. + */ + protected function getAdditionalCacheContextsForEntityListing() { + return []; + } + + /** + * Returns the additional cache tags for the tested entity's listing by type. + * * Necessary when there are unavoidable default entities of this type, e.g. * the anonymous and administrator User entities always exist. * @@ -382,13 +392,19 @@ public function testReferencedEntity() { $this->verifyPageCache($empty_entity_listing_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags); + // Verify the entity type's list cache contexts are present. + $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts'); + $this->assertEqual($this->getAdditionalCacheContextsForEntityListing(), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header)); $this->pass("Test listing containing referenced entity.", 'Debug'); // Prime the page cache for the listing containing the referenced entity. - $this->verifyPageCache($nonempty_entity_listing_url, 'MISS'); + $this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags); // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags); + // Verify the entity type's list cache contexts are present. + $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts'); + $this->assertEqual($this->getAdditionalCacheContextsForEntityListing(), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header)); // Verify that after modifying the referenced entity, there is a cache miss diff --git a/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php index 5609959..851fed8 100644 --- a/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php +++ b/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php @@ -56,4 +56,14 @@ public function testPager() { $this->assertRaw('Test entity 51', 'Test entity 51 is shown.'); } + public function testCacheContexts() { + /** @var \Drupal\Core\Entity\EntityListBuilderInterface $list_builder */ + $list_builder = $this->container->get('entity.manager')->getListBuilder('entity_test'); + + $build = $list_builder->render(); + $this->container->get('renderer')->render($build); + + $this->assertEqual(['entity_test_view_grants'], $build['#cache']['contexts']); + } + } diff --git a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php index bf5fa29..1d6e4e8 100644 --- a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php +++ b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php @@ -162,6 +162,7 @@ public function listEntitiesAlphabetically($entity_type_id) { '#items' => $labels, '#title' => $entity_type_id . ' entities', '#cache' => [ + 'contexts' => $entity_type_definition->getListCacheContexts(), 'tags' => $cache_tags, ], ]; @@ -182,11 +183,13 @@ public function listEntitiesAlphabetically($entity_type_id) { * A renderable array. */ public function listEntitiesEmpty($entity_type_id) { + $entity_type_definition = $this->entityManager()->getDefinition($entity_type_id); return [ '#theme' => 'item_list', '#items' => [], '#cache' => [ - 'tags' => $this->entityManager()->getDefinition($entity_type_id)->getListCacheTags(), + 'contexts' => $entity_type_definition->getListCacheContexts(), + 'tags' => $entity_type_definition->getListCacheTags(), ], ]; } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index bec87c4..89b4c20 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -33,6 +33,7 @@ * }, * base_table = "entity_test", * persistent_cache = FALSE, + * list_cache_contexts = { "entity_test_view_grants" }, * entity_keys = { * "id" = "id", * "uuid" = "uuid", diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 51ce002..0683380 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -250,16 +250,6 @@ views_display: rendering_language: type: string label: 'Entity language' - cache_metadata: - type: mapping - label: 'Cache metadata' - mapping: - cacheable: - type: boolean - label: 'Cacheable' - contexts: - type: sequence - label: 'Cache contexts' exposed_block: type: boolean label: 'Put the exposed form in a block' diff --git a/core/modules/views/config/schema/views.schema.yml b/core/modules/views/config/schema/views.schema.yml index f7e089f..e3293a1 100644 --- a/core/modules/views/config/schema/views.schema.yml +++ b/core/modules/views/config/schema/views.schema.yml @@ -114,6 +114,18 @@ views.view.*: label: 'Position' display_options: type: views.display.[%parent.display_plugin] + cache_metadata: + type: mapping + label: 'Cache metadata' + mapping: + cacheable: + type: boolean + label: 'Cacheable' + contexts: + type: sequence + label: 'Cache contexts' + sequence: + type: string views_block: type: block_settings diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index 63d3ba1..daca68f 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -310,13 +310,14 @@ protected function addCacheMetadata() { $current_display = $executable->current_display; $displays = $this->get('display'); - foreach ($displays as $display_id => $display) { + foreach (array_keys($displays) as $display_id) { + $display =& $this->getDisplay($display_id); $executable->setDisplay($display_id); list($display['cache_metadata']['cacheable'], $display['cache_metadata']['contexts']) = $executable->getDisplay()->calculateCacheMetadata(); // Always include at least the language context as there will be most // probable translatable strings in the view output. - $display['cache_metadata']['contexts'][] = 'cache.context.language'; + $display['cache_metadata']['contexts'][] = 'language'; $display['cache_metadata']['contexts'] = array_unique($display['cache_metadata']['contexts']); } // Restore the previous active display. diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 1945cc3..88b0fce 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -137,6 +137,7 @@ public function getViewsData() { $data[$views_base_table]['table']['base'] = [ 'field' => $base_field, 'title' => $this->entityType->getLabel(), + 'cache_contexts' => $this->entityType->getListCacheContexts(), ]; if ($label_key = $this->entityType->getKey('label')) { diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 7cd1041..2a2cd3b 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -118,6 +118,17 @@ protected static $unpackOptions = array(); /** + * The display information coming directly from the view entity. + * + * @see \Drupal\views\Entity\View::getDisplay() + * + * @todo \Drupal\views\Entity\View::duplicateDisplayAsType directly access it. + * + * @var array + */ + public $display; + + /** * Constructs a new DisplayPluginBase object. * * Because DisplayPluginBase::initDisplay() takes the display configuration by @@ -2302,6 +2313,9 @@ public function buildRenderable(array $args = []) { '#arguments' => $args, '#embed' => FALSE, '#view' => $this->view, + '#cache' => [ + 'contexts' => isset($this->display['cache_metadata']['contexts']) ? $this->display['cache_metadata']['contexts'] : [], + ], ]; } diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php index a206d03..59b98fa 100644 --- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\query; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ViewExecutable; @@ -35,7 +36,7 @@ /** * Base plugin class for Views queries. */ -abstract class QueryPluginBase extends PluginBase { +abstract class QueryPluginBase extends PluginBase implements CacheablePluginInterface { /** * A pager plugin that should be provided by the display. @@ -315,6 +316,27 @@ public function getEntityTableInfo() { /** * {@inheritdoc} */ + public function isCacheable() { + // This plugin can't really determine that. + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = []; + if (($views_data = Views::viewsData()->get($this->view->storage->get('base_table'))) && !empty($views_data['table']['entity type'])) { + $entity_type_id = $views_data['table']['entity type']; + $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + $contexts = $entity_type->getListCacheContexts(); + } + return $contexts; + } + + /** + * {@inheritdoc} + */ public function getCacheTags() { return []; } diff --git a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php index 747e170..e140668 100644 --- a/core/modules/views/src/Tests/RenderCacheIntegrationTest.php +++ b/core/modules/views/src/Tests/RenderCacheIntegrationTest.php @@ -10,9 +10,10 @@ use Drupal\Core\Cache\Cache; use Drupal\entity_test\Entity\EntityTest; use Drupal\views\Views; +use Drupal\views\Entity\View; /** - * Tests the general integration between Views and the render cache. + * Tests the general integration between views and the render cache. * * @group views */ @@ -23,12 +24,12 @@ class RenderCacheIntegrationTest extends ViewUnitTestBase { /** * {@inheritdoc} */ - public static $testViews = ['entity_test_fields', 'entity_test_row']; + public static $testViews = ['test_view', 'test_display', 'entity_test_fields', 'entity_test_row']; /** * {@inheritdoc} */ - public static $modules = ['entity_test', 'user']; + public static $modules = ['entity_test', 'user', 'node']; /** * {@inheritdoc} @@ -200,4 +201,27 @@ protected function assertCacheTagsForEntityBasedView($do_assert_views_caches) { $this->assertViewsCacheTags($view, $result_tags_page_1, $do_assert_views_caches, $render_tags_page_1); } + /** + * Ensure that the view renderable contains the cache contexts. + */ + public function testBuildRenderableWithCacheContexts() { + $view = View::load('test_view'); + $display =& $view->getDisplay('default'); + $display['cache_metadata']['contexts'] = ['beatles']; + $executable = $view->getExecutable(); + + $build = $executable->buildRenderable(); + $this->assertEqual(['beatles'], $build['#cache']['contexts']); + } + + /** + * Ensures that saving a view calculates the cache contexts. + */ + public function testViewAddCacheMetadata() { + $view = View::load('test_display'); + $view->save(); + + $this->assertEqual(['node_view_grants', 'language'], $view->getDisplay('default')['cache_metadata']['contexts']); + } + } diff --git a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php index c81a47c..5822fe1 100644 --- a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php @@ -89,6 +89,7 @@ protected function setUp() { 'label' => 'Entity test', 'entity_keys' => ['id' => 'id', 'langcode' => 'langcode'], 'provider' => 'entity_test', + 'list_cache_contexts' => ['entity_test_list_cache_context'], ]); $this->translationManager = $this->getStringTranslationStub(); @@ -164,6 +165,7 @@ public function testBaseTables() { $this->assertEquals('entity_test', $data['entity_test']['table']['provider']); $this->assertEquals('id', $data['entity_test']['table']['base']['field']); + $this->assertEquals(['entity_test_list_cache_context'], $data['entity_test']['table']['base']['cache_contexts']); $this->assertEquals('Entity test', $data['entity_test']['table']['base']['title']); $this->assertFalse(isset($data['entity_test']['table']['defaults'])); @@ -173,7 +175,6 @@ public function testBaseTables() { $this->assertFalse(isset($data['revision_data_table'])); } - /** * Tests data_table support. */