Commit d2f9c183 authored by catch's avatar catch

Issue #2318377 by dawehner, Wim Leers, damiankloip: Determine whether a view...

Issue #2318377 by dawehner, Wim Leers, damiankloip: Determine whether a view is cacheable and its required contexts, store this i/t config entity.
parent 3f5bb7d9
......@@ -11,7 +11,7 @@ display:
position: null
display_options:
access:
type: perm
type: none
cache:
type: none
query:
......
......@@ -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();
}
/**
......
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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'];
}
}
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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'];
}
}
......@@ -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;
}
}
......@@ -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'));
}
......
......@@ -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'];
}
}
......@@ -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'];
}
}
......@@ -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;
}
}
......@@ -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
......
......@@ -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}
*/
......
<?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();
}
......@@ -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;
}
}
/**
......
......@@ -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 [];
}
}
......@@ -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'];
}
}
......@@ -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'];
}
}
......@@ -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.
*/