diff --git a/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml index 8ecb8a1a477eb116eccbfadb0a41eb3d95c9410a..f1b113c0873a30a0c472c53e1c52578341fe5ad4 100644 --- a/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml +++ b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block.yml @@ -11,7 +11,7 @@ display: position: null display_options: access: - type: perm + type: none cache: type: none query: diff --git a/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php b/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php index ab2f643c647fd69e3e94c9a370bf327ce722cc5a..da875440d75a351db78596e89ed1cba0dc11ffe0 100644 --- a/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php +++ b/core/modules/entity_reference/src/Tests/Views/EntityReferenceRelationshipTest.php @@ -93,6 +93,8 @@ protected function setUp() { $entity->save(); $this->assertEqual($entity->field_test[0]->entity->id(), $referenced_entity->id()); $this->entities[$entity->id()] = $entity; + + Views::viewsData()->clear(); } /** diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php index 6a4f3d7f5a27d89e043d616f2ab8185a1b220e81..7780eff913473528a1b30db34707e3dd880fd5cf 100644 --- a/core/modules/field/src/Plugin/views/field/Field.php +++ b/core/modules/field/src/Plugin/views/field/Field.php @@ -20,6 +20,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\Session\AccountInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\ResultRow; @@ -34,7 +35,7 @@ * * @ViewsField("field") */ -class Field extends FieldPluginBase { +class Field extends FieldPluginBase implements CacheablePluginInterface { /** * An array to store field renderable arrays for use by renderItems(). @@ -946,5 +947,23 @@ public function getDependencies() { return array('entity' => array($this->getFieldStorageConfig()->getConfigDependencyName())); } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + // @todo what to do about field access? + $contexts = []; + + $contexts[] = 'cache.context.user'; + + return $contexts; + } } diff --git a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php index f72de1d78a96223f41ff0739a590be6e5810f183..4117ec0eb02c0bb0fe2fe903a914be480a5d07fe 100644 --- a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php +++ b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php @@ -99,4 +99,12 @@ public function adminSummary() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + // This filter depends on the current time and therefore is never cacheable. + return FALSE; + } + } diff --git a/core/modules/node/src/Plugin/views/argument_default/Node.php b/core/modules/node/src/Plugin/views/argument_default/Node.php index f232cea3aba85a12d074c05e5cafacdd8e740525..f1be8d69b64b3fe31808d8e2debc0697aee398b8 100644 --- a/core/modules/node/src/Plugin/views/argument_default/Node.php +++ b/core/modules/node/src/Plugin/views/argument_default/Node.php @@ -7,6 +7,7 @@ namespace Drupal\node\Plugin\views\argument_default; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; use Drupal\node\NodeInterface; @@ -20,7 +21,7 @@ * title = @Translation("Content ID from URL") * ) */ -class Node extends ArgumentDefaultPluginBase { +class Node extends ArgumentDefaultPluginBase implements CacheablePluginInterface { /** * {@inheritdoc} @@ -31,4 +32,18 @@ public function getArgument() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.url']; + } + } diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php index 8845ca8d2b3381d908f07b9aa7a343c8e14844f9..5fd692409d1457034cbcf8661cfbbb87732330e3 100644 --- a/core/modules/node/src/Plugin/views/filter/Access.php +++ b/core/modules/node/src/Plugin/views/filter/Access.php @@ -47,4 +47,16 @@ public function query() { } } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + + // Node access is potentially cacheable per user. + $contexts[] = 'cache.context.user'; + + return $contexts; + } + } diff --git a/core/modules/node/src/Plugin/views/filter/Status.php b/core/modules/node/src/Plugin/views/filter/Status.php index 35c818e68e8a8fa3b8b443c96794f80fb4fdd48e..9a55860d9a23abcd68af6909675c70c21b476f7d 100644 --- a/core/modules/node/src/Plugin/views/filter/Status.php +++ b/core/modules/node/src/Plugin/views/filter/Status.php @@ -30,4 +30,15 @@ public function query() { $this->query->addWhereExpression($this->options['group'], "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_NODES*** = 1) OR ***BYPASS_NODE_ACCESS*** = 1"); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + + $contexts[] = 'cache.context.user'; + + return $contexts; + } + } diff --git a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php index efc2c20e1a86ceeb123467b9a2a584ec60212a2f..2cd7a13430f11079cb50053ce5506f39b31d521f 100644 --- a/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php +++ b/core/modules/taxonomy/src/Plugin/views/argument_default/Tid.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\taxonomy\TermInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; @@ -24,7 +25,7 @@ * title = @Translation("Taxonomy term ID from URL") * ) */ -class Tid extends ArgumentDefaultPluginBase { +class Tid extends ArgumentDefaultPluginBase implements CacheablePluginInterface { /** * Overrides \Drupal\views\Plugin\views\Plugin\views\PluginBase::init(). @@ -167,4 +168,18 @@ public function getArgument() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.url']; + } + } diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php index d99adf281551cbad98721f5d50af52e4fcb23db4..071b2f8116379d5b462e56c9c2480182d2fc34ce 100644 --- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php +++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php @@ -361,4 +361,17 @@ public function adminSummary() { return parent::adminSummary(); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + // The result potentially depends on term access and so is just cacheable + // per user. + // @todo https://www.drupal.org/node/2352175 + $contexts[] = 'cache.context.user'; + + return $contexts; + } + } diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php index e357259c1df17501b9ed0709389f0ad346b4f937..a8c76330f071f4d8b6fdf77a9aed138f4667d98d 100644 --- a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php @@ -11,6 +11,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\views\Tests\ViewTestBase; use Drupal\views\Tests\ViewTestData; +use Drupal\views\Views; /** * Tests taxonomy field filters with translations. @@ -83,6 +84,8 @@ function setUp() { } $taxonomy->save(); + Views::viewsData()->clear(); + ViewTestData::createTestViews(get_class($this), array('taxonomy_test_views')); } diff --git a/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php b/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php index f1d987bb3098a9c71696f3dbf2f69fb47c98ca67..1d49666e1dfd84fd8e260586ec1fdd9670ab325e 100644 --- a/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php +++ b/core/modules/user/src/Plugin/views/argument_default/CurrentUser.php @@ -7,6 +7,7 @@ namespace Drupal\user\Plugin\views\argument_default; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; /** @@ -19,10 +20,24 @@ * title = @Translation("User ID from logged in user") * ) */ -class CurrentUser extends ArgumentDefaultPluginBase { +class CurrentUser extends ArgumentDefaultPluginBase implements CacheablePluginInterface { public function getArgument() { return \Drupal::currentUser()->id(); } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.user']; + } + } diff --git a/core/modules/user/src/Plugin/views/argument_default/User.php b/core/modules/user/src/Plugin/views/argument_default/User.php index 85707240d09408c3735be529f79cdf435c5daec5..850ee2ff77f2086738c243269ff3d9ede8cba923 100644 --- a/core/modules/user/src/Plugin/views/argument_default/User.php +++ b/core/modules/user/src/Plugin/views/argument_default/User.php @@ -8,6 +8,7 @@ namespace Drupal\user\Plugin\views\argument_default; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -22,7 +23,7 @@ * title = @Translation("User ID from route context") * ) */ -class User extends ArgumentDefaultPluginBase { +class User extends ArgumentDefaultPluginBase implements CacheablePluginInterface { /** * {@inheritdoc} @@ -74,4 +75,18 @@ public function getArgument() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.url']; + } + } diff --git a/core/modules/user/src/Plugin/views/filter/Current.php b/core/modules/user/src/Plugin/views/filter/Current.php index b431bb796abb6d246bf74ca424bfd53421f2028e..6bf0f2936783e504d41977b0762ccfe43e6639e5 100644 --- a/core/modules/user/src/Plugin/views/filter/Current.php +++ b/core/modules/user/src/Plugin/views/filter/Current.php @@ -47,4 +47,16 @@ public function query() { $this->query->addWhere($this->options['group'], $or); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = parent::getCacheContexts(); + + // This filter depends on the current user. + $contexts[] = 'cache.context.user'; + + return $contexts; + } + } 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 85ab981d452eaf9bd0a9d43f189c2c0ddf51671c..e69060dbb32e05179fafdc14ccf3756fbf776577 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -268,6 +268,16 @@ views_display: field_langcode_add_to_query: type: string label: 'Add the field language to the query' + cache_metadata: + type: mapping + label: 'Cache metadata' + mapping: + cacheable: + type: boolean + label: 'Cacheable' + contexts: + type: sequence + label: 'Cache contexts' views_sort: type: views_handler diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index ea3664e6bf40830fc59263d80584909209d40b97..447c429809c50d39547150af4ca7424f45548cf9 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -295,6 +295,48 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // @todo Check whether isSyncing is needed. + if (!$this->isSyncing()) { + $this->addCacheMetadata(); + } + } + + /** + * Fills in the cache metadata of this view. + * + * Cache metadata is set per view and per display, and ends up being stored in + * the view's configuration. This allows Views to determine very efficiently: + * - whether a view is cacheable at all + * - what the cache key for a given view should be + * + * In other words: this allows us to do the (expensive) work of initializing + * Views plugins and handlers to determine their effect on the cacheability of + * a view at save time rather than at runtime. + */ + protected function addCacheMetadata() { + $executable = $this->getExecutable(); + + $current_display = $executable->current_display; + $displays = $this->get('display'); + foreach ($displays as $display_id => $display) { + $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'] = array_unique($display['cache_metadata']['contexts']); + } + // Restore the previous active display. + $executable->setDisplay($current_display); + } + /** * {@inheritdoc} */ diff --git a/core/modules/views/src/Plugin/CacheablePluginInterface.php b/core/modules/views/src/Plugin/CacheablePluginInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a8a72a96f733c79bd2919b3fd718815e2487ba02 --- /dev/null +++ b/core/modules/views/src/Plugin/CacheablePluginInterface.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Plugin\CacheablePluginInterface. + */ + +namespace Drupal\views\Plugin; + +/** + * Provides information whether and how the specific Views plugin is cacheable. + */ +interface CacheablePluginInterface { + + /** + * Returns TRUE if this plugin is cacheable at all. + * + * @return bool + */ + public function isCacheable(); + + /** + * Returns an array of cache contexts, this plugin varies by. + * + * Note: This method is called on views safe time, so you do have the + * configuration available. For example an exposed filter changes its + * cacheability depending on the URL. + * + * @return string[] + */ + public function getCacheContexts(); + +} diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php index 7d9372e467f8c111567bcc8f27b74c6e5ae327d9..785e024bf91ff32e4fa287edb3c2b682b3303796 100644 --- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\String as UtilityString; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ViewExecutable; @@ -55,7 +56,7 @@ * - numeric: If set to TRUE this field is numeric and will use %d instead of * %s in queries. */ -abstract class ArgumentPluginBase extends HandlerBase { +abstract class ArgumentPluginBase extends HandlerBase implements CacheablePluginInterface { var $validator = NULL; var $argument = NULL; @@ -1013,14 +1014,23 @@ public function getPlugin($type = 'argument_default', $name = NULL) { $options = array(); switch ($type) { case 'argument_default': + if (!isset($this->options['default_argument_type'])) { + return; + } $plugin_name = $this->options['default_argument_type']; $options_name = 'default_argument_options'; break; case 'argument_validator': + if (!isset($this->options['validate']['type'])) { + return; + } $plugin_name = $this->options['validate']['type']; $options_name = 'validate_options'; break; case 'style': + if (!isset($this->options['summary']['format'])) { + return; + } $plugin_name = $this->options['summary']['format']; $options_name = 'summary_options'; } @@ -1032,7 +1042,7 @@ public function getPlugin($type = 'argument_default', $name = NULL) { // we only fetch the options if we're fetching the plugin actually // in use. if ($name == $plugin_name) { - $options = $this->options[$options_name]; + $options = isset($this->options[$options_name]) ? $this->options[$options_name] : []; } $plugin = Views::pluginManager($type)->createInstance($name); @@ -1163,6 +1173,56 @@ protected function unpackArgumentValue($force_int = FALSE) { $this->value = $break->value; $this->operator = $break->operator; } + + /** + * {@inheritdoc} + */ + public function isCacheable() { + $result = TRUE; + + // Asks all subplugins (argument defaults, argument validator and styles). + if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheablePluginInterface) { + $result &= $plugin->isCacheable(); + } + + if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheablePluginInterface) { + $result &= $plugin->isCacheable(); + } + + // Summaries use style plugins. + if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheablePluginInterface) { + $result &= $plugin->isCacheable(); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $contexts = []; + // By definition arguments depends on the URL. + // @todo Once contexts are properly injected into block views we could pull + // the information from there. + $contexts[] = 'cache.context.url'; + + // Asks all subplugins (argument defaults, argument validator and styles). + if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheablePluginInterface) { + $contexts = array_merge($plugin->getCacheContexts(), $contexts); + } + + if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheablePluginInterface) { + $contexts = array_merge($plugin->getCacheContexts(), $contexts); + } + + if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheablePluginInterface) { + $contexts = array_merge($plugin->getCacheContexts(), $contexts); + } + + return $contexts; + } + } /** diff --git a/core/modules/views/src/Plugin/views/argument_default/Fixed.php b/core/modules/views/src/Plugin/views/argument_default/Fixed.php index c2d774b1fa469e30acc82271dc3d2a65df93f589..03b9330deb193c7fe245460faaa0e8c5c64db108 100644 --- a/core/modules/views/src/Plugin/views/argument_default/Fixed.php +++ b/core/modules/views/src/Plugin/views/argument_default/Fixed.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\argument_default; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; /** * The fixed argument default handler. @@ -19,7 +20,7 @@ * title = @Translation("Fixed") * ) */ -class Fixed extends ArgumentDefaultPluginBase { +class Fixed extends ArgumentDefaultPluginBase implements CacheablePluginInterface { protected function defineOptions() { $options = parent::defineOptions(); @@ -44,4 +45,18 @@ public function getArgument() { return $this->options['argument']; } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + } + } diff --git a/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php b/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php index 82bf1fabeb1bfe6bcb8a8a17a8125bc3b618ec19..337b72271dddc3285d95f9917ba178e5317df398 100644 --- a/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php +++ b/core/modules/views/src/Plugin/views/argument_default/QueryParameter.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\argument_default; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; /** * A query parameter argument default handler. @@ -19,7 +20,7 @@ * title = @Translation("Query parameter") * ) */ -class QueryParameter extends ArgumentDefaultPluginBase { +class QueryParameter extends ArgumentDefaultPluginBase implements CacheablePluginInterface { /** * {@inheritdoc} @@ -83,4 +84,18 @@ public function getArgument() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.url']; + } + } diff --git a/core/modules/views/src/Plugin/views/argument_default/Raw.php b/core/modules/views/src/Plugin/views/argument_default/Raw.php index 25dc8bced71962fceea291118cdcb73159f49746..9d194e5d6fac2477468293fb24359745e2860eaf 100644 --- a/core/modules/views/src/Plugin/views/argument_default/Raw.php +++ b/core/modules/views/src/Plugin/views/argument_default/Raw.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Path\AliasManagerInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -22,7 +23,7 @@ * title = @Translation("Raw value from URL") * ) */ -class Raw extends ArgumentDefaultPluginBase { +class Raw extends ArgumentDefaultPluginBase implements CacheablePluginInterface { /** * The alias manager. @@ -102,4 +103,18 @@ public function getArgument() { } } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['cache.context.url']; + } + } diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php index 068197c7681dae30b7eac1c067722b08896b98ca..3a782107a69d974e8d384f3ee4737f8fb7e1d208 100644 --- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php @@ -367,6 +367,17 @@ protected function getCacheTags() { return $tags; } + /** + * Alters the cache metadata of a display upon saving a view. + * + * @param bool $is_cacheable + * Whether the display is cacheable. + * @param string[] $cache_contexts + * The cache contexts the display varies by. + */ + public function alterCacheMetadata(&$is_cacheable, array &$cache_contexts) { + } + } /** diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 12b05fb48adae1f97f535980676495b434606a16..a116a0f5a564b2b5c057ef5f052e300648bf5bb7 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -15,6 +15,7 @@ use Drupal\Core\Theme\Registry; use Drupal\Core\Url; use Drupal\views\Form\ViewsForm; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\area\AreaPluginBase; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\PluginBase; @@ -2281,6 +2282,50 @@ public function preExecute() { $this->view->setShowAdminLinks($this->getOption('show_admin_links')); } + /** + * Calculates the display's cache metadata by inspecting each handler/plugin. + * + * @return array + * Returns an array: + * - first value: (boolean) Whether the display is cacheable. + * - second value: (string[]) The cache contexts the display varies by. + */ + public function calculateCacheMetadata () { + $is_cacheable = TRUE; + $cache_contexts = []; + + // Iterate over ordinary views plugins. + foreach (Views::getPluginTypes('plugin') as $plugin_type) { + $plugin = $this->getPlugin($plugin_type); + if ($plugin instanceof CacheablePluginInterface) { + $cache_contexts = array_merge($cache_contexts, $plugin->getCacheContexts()); + $is_cacheable &= $plugin->isCacheable(); + } + else { + $is_cacheable = FALSE; + } + } + + // Iterate over all handlers. Note that at least the argument handler will + // need to ask all its subplugins. + foreach (array_keys(Views::getHandlerTypes()) as $handler_type) { + $handlers = $this->getHandlers($handler_type); + foreach ($handlers as $handler) { + if ($handler instanceof CacheablePluginInterface) { + $cache_contexts = array_merge($cache_contexts, $handler->getCacheContexts()); + $is_cacheable &= $handler->isCacheable(); + } + } + } + + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + if ($cache_plugin = $this->getPlugin('cache')) { + $cache_plugin->alterCacheMetadata($is_cacheable, $cache_contexts); + } + + return [$is_cacheable, $cache_contexts]; + } + /** * When used externally, this is how a view gets run and returns * data in the format required. diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 366659afc5adb38056407d17410bcb22aee4c24e..11ec8b0f51de579fa56c60a7bc77bc8f9b1aa678 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\HandlerBase; use Drupal\Component\Utility\String as UtilityString; use Drupal\views\Plugin\views\display\DisplayPluginBase; @@ -43,7 +44,7 @@ /** * Base class for Views filters handler plugins. */ -abstract class FilterPluginBase extends HandlerBase { +abstract class FilterPluginBase extends HandlerBase implements CacheablePluginInterface { /** * Contains the actual value of the field,either configured in the views ui @@ -1467,6 +1468,27 @@ protected static function arrayFilterZero($var) { return trim($var) != ''; } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $cache_contexts = []; + // An exposed filter allows the user to change a view's filters. They accept + // input from GET parameters, which are part of the URL. Hence a view with + // an exposed filter is cacheable per URL. + if ($this->isExposed()) { + $cache_contexts[] = 'cache.context.url'; + } + return $cache_contexts; + } + } /** diff --git a/core/modules/views/src/Plugin/views/sort/Random.php b/core/modules/views/src/Plugin/views/sort/Random.php index f0223df42ce4c801ec9e18907202d03d32b25472..1cdfc036966a0e9d2b4d78b25a6377eaa54bd299 100644 --- a/core/modules/views/src/Plugin/views/sort/Random.php +++ b/core/modules/views/src/Plugin/views/sort/Random.php @@ -8,13 +8,14 @@ namespace Drupal\views\Plugin\views\sort; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; /** * Handle a random sort. * * @ViewsSort("random") */ -class Random extends SortPluginBase { +class Random extends SortPluginBase implements CacheablePluginInterface { /** * {@inheritdoc} @@ -32,4 +33,18 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['order']['#access'] = FALSE; } + /** + * {@inheritdoc} + */ + public function isCacheable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + } + } diff --git a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php index 560df7481705dfc7a3a9bb0e4ca06107394028f7..45b86bee6b0a4ac9f4f2bbddb78f501759ae5d34 100644 --- a/core/modules/views/src/Plugin/views/sort/SortPluginBase.php +++ b/core/modules/views/src/Plugin/views/sort/SortPluginBase.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\sort; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\HandlerBase; /** @@ -26,7 +27,7 @@ /** * Base sort handler that has no options and performs a simple sort. */ -abstract class SortPluginBase extends HandlerBase { +abstract class SortPluginBase extends HandlerBase implements CacheablePluginInterface { /** * Determine if a sort can be exposed. @@ -224,6 +225,27 @@ public function defaultExposeOptions() { ); } + /** + * {@inheritdoc} + */ + public function isCacheable() { + // The result of a sort does not depend on outside information, so by + // default it is cacheable. + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $cache_contexts = []; + // Exposed sorts use GET parameters, so it depends on the current URL. + if ($this->isExposed()) { + $cache_contexts[] = 'cache.context.url'; + } + return $cache_contexts; + } + } /** diff --git a/core/modules/views/src/Tests/Plugin/DisplayTest.php b/core/modules/views/src/Tests/Plugin/DisplayTest.php index 6229cf340be62cfd5587e52d2da9a43c607dfb74..bacfdd4584d427ccafdf96597716b2a2bf887eed 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayTest.php @@ -167,6 +167,9 @@ public function testGetAttachedDisplays() { * Tests the readmore functionality. */ public function testReadMore() { + if (!isset($this->options['validate']['type'])) { + return; + } $expected_more_text = 'custom more text'; $view = Views::getView('test_display_more'); diff --git a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php index e01916877034a1a84e6487e37c0dee422babecf9..1b0b6557ddcebbf30a30c551b32e240eae6ecbcb 100644 --- a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php +++ b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php @@ -7,6 +7,8 @@ namespace Drupal\views\Tests\Plugin; +use Drupal\views\Views; + /** * Provides a base class for a testing a relationship. * @@ -39,6 +41,8 @@ protected function setUpFixtures() { $this->installSchema('system', 'sequences'); $this->root_user = entity_create('user', array('name' => $this->randomMachineName())); $this->root_user->save(); + + Views::viewsData()->clear(); } /** diff --git a/core/modules/views/src/Tests/Plugin/RowEntityTest.php b/core/modules/views/src/Tests/Plugin/RowEntityTest.php index 3024b778f6ccd9c7709ba1adbbe90981fb3c8322..35bd1e2b5b585ab47eb9a65fd1df042c8023c612 100644 --- a/core/modules/views/src/Tests/Plugin/RowEntityTest.php +++ b/core/modules/views/src/Tests/Plugin/RowEntityTest.php @@ -24,7 +24,7 @@ class RowEntityTest extends ViewUnitTestBase { * * @var array */ - public static $modules = array('taxonomy', 'text', 'filter', 'field', 'entity', 'system'); + public static $modules = ['taxonomy', 'text', 'filter', 'field', 'entity', 'system', 'node', 'user']; /** * Views used by this test. diff --git a/core/modules/views/src/Tests/ViewTestBase.php b/core/modules/views/src/Tests/ViewTestBase.php index 6aa899d41be7dcfa488c838600d928fd20d55049..5003336d47d61c8b15d3ab0b39a59f5fb7eef51c 100644 --- a/core/modules/views/src/Tests/ViewTestBase.php +++ b/core/modules/views/src/Tests/ViewTestBase.php @@ -30,14 +30,16 @@ abstract class ViewTestBase extends WebTestBase { */ public static $modules = array('views', 'views_test_config'); - protected function setUp() { + protected function setUp($import_test_views = TRUE) { parent::setUp(); // Ensure that the plugin definitions are cleared. foreach (ViewExecutable::getPluginTypes() as $plugin_type) { $this->container->get("plugin.manager.views.$plugin_type")->clearCachedDefinitions(); } - ViewTestData::createTestViews(get_class($this), array('views_test_config')); + if ($import_test_views) { + ViewTestData::createTestViews(get_class($this), array('views_test_config')); + } } /** diff --git a/core/modules/views/src/Tests/ViewsTemplateTest.php b/core/modules/views/src/Tests/ViewsTemplateTest.php index 7eacc0886b7353292121a9bda8cffd47fdc623a8..966029c3d209b02609dddccc67972970fab1766b 100644 --- a/core/modules/views/src/Tests/ViewsTemplateTest.php +++ b/core/modules/views/src/Tests/ViewsTemplateTest.php @@ -25,10 +25,14 @@ class ViewsTemplateTest extends ViewTestBase { */ public static $testViews = array('test_view_display_template'); + /** + * {@inheritdoc} + */ protected function setUp() { - parent::setUp(); + parent::setUp(FALSE); $this->enableViewsTestModule(); + ViewTestData::createTestViews(get_class($this), array('views_test_config')); } /** diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml index df3bbe21b1722c8c4de765700de2f2526ebb2674..164abad9a065ef0e9ed6281672a8913cd94cd800 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml @@ -10,6 +10,8 @@ display: pager: '0' pager_options: '0' sorts: '0' + row: + type: fields fields: age: field: age