Commit 76fb5cba authored by Dries's avatar Dries
Browse files

Issue #1712456 by damiankloip, aspilicious, amateescu: How to leverage cache tags in Views.

parent 6ca86d9f
......@@ -370,17 +370,17 @@ public function referencedEntities() {
* {@inheritdoc}
*/
public function changed() {
$referenced_entity_ids = array(
$this->entityType() => array($this->id() => TRUE),
$referenced_entities = array(
$this->entityType() => array($this->id() => $this),
);
foreach ($this->referencedEntities() as $referenced_entity) {
$referenced_entity_ids[$referenced_entity->entityType()][$referenced_entity->id()] = TRUE;
$referenced_entities[$referenced_entity->entityType()][$referenced_entity->id()] = $referenced_entity;
}
foreach ($referenced_entity_ids as $entity_type => $entity_ids) {
foreach ($referenced_entities as $entity_type => $entities) {
if (\Drupal::entityManager()->hasController($entity_type, 'view_builder')) {
\Drupal::entityManager()->getViewBuilder($entity_type)->resetCache(array_keys($entity_ids));
\Drupal::entityManager()->getViewBuilder($entity_type)->resetCache($entities);
}
}
}
......
......@@ -264,11 +264,13 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
if (isset($ids)) {
public function resetCache(array $entities = NULL) {
if (isset($entities)) {
$tags = array();
foreach ($ids as $entity_id) {
$tags[$this->entityType][$entity_id] = $entity_id;
foreach ($entities as $entity) {
$id = $entity->id();
$tags[$this->entityType][$id] = $id;
$tags[$this->entityType . '_view_' . $entity->bundle()] = TRUE;
}
\Drupal::cache($this->cacheBin)->deleteTags($tags);
}
......
......@@ -79,10 +79,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
/**
* Resets the entity render cache.
*
* @param array|null $ids
* (optional) If specified, the cache is reset for the given entity IDs
* only.
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* (optional) If specified, the cache is reset for the given entities only.
*/
public function resetCache(array $ids = NULL);
public function resetCache(array $entities = NULL);
}
......@@ -411,6 +411,6 @@ public function save(array $form, array &$form_state) {
// Clear the block and page caches so that anonymous users see the comment
// they have posted.
Cache::invalidateTags(array('content' => TRUE));
$this->entityManager->getViewBuilder($entity->entityType())->resetCache(array($entity->id()));
$this->entityManager->getViewBuilder($entity->entityType())->resetCache(array($entity));
}
}
......@@ -307,7 +307,7 @@ function testCommentFunctionality() {
));
// We've changed role permissions, so need to reset render cache.
// @todo Revisit after https://drupal.org/node/2099105
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($this->entity->id()));
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($this->entity));
$this->drupalGet('entity_test/' . $this->entity->id());
$this->assertPattern('@<h2[^>]*>Comments</h2>@', 'Comments were displayed.');
$this->assertLink('Log in', 0, 'Link to log in was found.');
......@@ -326,7 +326,7 @@ function testCommentFunctionality() {
));
// We've changed role permissions, so need to reset render cache.
// @todo Revisit after https://drupal.org/node/2099105
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($this->entity->id()));
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($this->entity));
$this->drupalGet('entity_test/' . $this->entity->id());
$this->assertNoPattern('@<h2[^>]*>Comments</h2>@', 'Comments were not displayed.');
$this->assertFieldByName('subject', '', 'Subject field found.');
......
......@@ -101,7 +101,7 @@ public function testFieldItemAttributes() {
))->save();
// Browse to the entity and verify that the attributes from both modules
// are rendered in the field item HTML markup.
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($entity->id()));
\Drupal::entityManager()->getViewBuilder('entity_test')->resetCache(array($entity));
$this->drupalGet('entity_test/' . $entity->id());
$xpath = $this->xpath('//div[@data-field-item-attr="foobar" and @property="schema:text" and text()=:value]', array(':value' => $test_value));
$this->assertTrue($xpath, 'The field item attributes from both modules have been found in the rendered output of the field.');
......
......@@ -61,7 +61,9 @@ public function query() {
* {@inheritdoc}
*/
public function render(ResultRow $values) {
return $this->renderLink($this->getEntity($values), $values);
if ($entity = $this->getEntity($values)) {
return $this->renderLink($entity, $values);
}
}
/**
......
......@@ -7,6 +7,7 @@
namespace Drupal\views\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\views\Views;
......@@ -278,6 +279,10 @@ public function getExportProperties() {
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
parent::postSave($storage_controller, $update);
// Clear cache tags for this view.
// @todo Remove if views implements a view_builder controller.
$id = $this->id();
Cache::deleteTags(array('view' => array($id => $id)));
views_invalidate_cache();
}
......@@ -318,9 +323,17 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
parent::postDelete($storage_controller, $entities);
$tempstore = \Drupal::service('user.tempstore')->get('views');
$tags = array();
foreach ($entities as $entity) {
$tempstore->delete($entity->id());
$id = $entity->id();
$tempstore->delete($id);
$tags['view'][$id] = $id;
}
// Clear cache tags for these views.
// @todo Remove if views implements a view_builder controller.
Cache::deleteTags($tags);
}
/**
......
......@@ -7,8 +7,8 @@
namespace Drupal\views\Plugin\views\cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language;
use Drupal\views\ViewExecutable;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\views\Plugin\views\PluginBase;
use Drupal\Core\Database\Query\Select;
......@@ -94,7 +94,8 @@ public function summaryTitle() {
* @param $type
* The cache type, either 'query', 'result' or 'output'.
*/
protected function cacheExpire($type) { }
protected function cacheExpire($type) {
}
/**
* Determine expiration time in the cache table of the cache type
......@@ -109,7 +110,6 @@ protected function cacheSetExpire($type) {
return CacheBackendInterface::CACHE_PERMANENT;
}
/**
* Save data to the cache.
*
......@@ -126,17 +126,16 @@ public function cacheSet($type) {
'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0,
'current_page' => $this->view->getCurrentPage(),
);
cache($this->table)->set($this->generateResultsKey(), $data, $this->cacheSetExpire($type));
\Drupal::cache($this->table)->set($this->generateResultsKey(), $data, $this->cacheSetExpire($type), $this->getCacheTags());
break;
case 'output':
$this->storage['output'] = $this->view->display_handler->output;
$this->gatherHeaders();
cache($this->table)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type));
\Drupal::cache($this->table)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), $this->getCacheTags());
break;
}
}
/**
* Retrieve data from the cache.
*
......@@ -151,7 +150,7 @@ public function cacheGet($type) {
case 'results':
// Values to set: $view->result, $view->total_rows, $view->execute_time,
// $view->current_page.
if ($cache = cache($this->table)->get($this->generateResultsKey())) {
if ($cache = \Drupal::cache($this->table)->get($this->generateResultsKey())) {
if (!$cutoff || $cache->created > $cutoff) {
$this->view->result = $cache->data['result'];
$this->view->total_rows = $cache->data['total_rows'];
......@@ -162,7 +161,7 @@ public function cacheGet($type) {
}
return FALSE;
case 'output':
if ($cache = cache($this->table)->get($this->generateOutputKey())) {
if ($cache = \Drupal::cache($this->table)->get($this->generateOutputKey())) {
if (!$cutoff || $cache->created > $cutoff) {
$this->storage = $cache->data;
$this->view->display_handler->output = $cache->data['output'];
......@@ -181,7 +180,8 @@ public function cacheGet($type) {
* to be sure that we catch everything. Maybe that's a bad idea.
*/
public function cacheFlush() {
cache($this->table)->deleteTags(array($this->view->storage->id() => TRUE));
$id = $this->view->storage->id();
Cache::invalidateTags(array('view' => array($id => $id)));
}
/**
......@@ -326,6 +326,50 @@ public function generateOutputKey() {
return $this->outputKey;
}
/**
* Gets an array of cache tags for the current view.
*
* @return array
* An array fo cache tags based on the current view.
*/
protected function getCacheTags() {
$tags = array();
$id = $this->view->storage->id();
$tags['view'][$id] = $id;
$entity_information = $this->view->query->getEntityTableInfo();
if (!empty($entity_information)) {
// Add an ENTITY_TYPE_view tag for each entity type used by this view.
foreach (array_keys($entity_information) as $type) {
$tags[$type . '_view'] = TRUE;
}
// Collect entity IDs if there are view results.
if (!empty($this->view->result)) {
foreach ($this->view->result as $result) {
$type = $result->_entity->entityType();
$tags[$type][] = $result->_entity->id();
$tags[$type . '_view_' . $result->_entity->bundle()] = TRUE;
foreach ($result->_relationship_entities as $entity) {
$type = $entity->entityType();
$tags[$type][] = $entity->id();
$tags[$type . '_view_' . $entity->bundle()] = TRUE;
}
}
}
}
// Filter out any duplicate values from generated tags.
return array_map(function($item) {
return is_array($item) ? array_unique($item) : $item;
}, $tags);
}
}
/**
......
......@@ -23,7 +23,8 @@
*/
class None extends CachePluginBase {
public function cacheStart() { /* do nothing */ }
public function cacheStart() {
}
public function summaryTitle() {
return t('None');
......
<?php
/**
* @file
* Contains \Drupal\views\Plugin\views\cache\Tag.
*/
namespace Drupal\views\Plugin\views\cache;
/**
* Simple caching of query results for Views displays.
*
* @ingroup views_cache_plugins
*
* @ViewsCache(
* id = "tag",
* title = @Translation("Tag based"),
* help = @Translation("Tag based caching of data. Caches will persist until any related cache tags are invalidated.")
* )
*/
class Tag extends CachePluginBase {
/**
* {@inheritdoc}
*/
public function summaryTitle() {
return t('Tag');
}
/**
* {@inheritdoc}
*/
protected function cacheExpire($type) {
return FALSE;
}
}
......@@ -1247,8 +1247,8 @@ public function query($get_count = FALSE) {
// Make sure each entity table has the base field added so that the
// entities can be loaded.
$entity_tables = $this->getEntityTables();
if ($entity_tables) {
$entity_information = $this->getEntityTableInfo();
if ($entity_information) {
$params = array();
if ($groupby) {
// Handle grouping, by retrieving the minimum entity_id.
......@@ -1257,10 +1257,10 @@ public function query($get_count = FALSE) {
);
}
foreach ($entity_tables as $table_alias => $table) {
$info = entity_get_info($table['entity_type']);
$base_field = empty($table['revision']) ? $info['entity_keys']['id'] : $info['entity_keys']['revision'];
$this->addField($table_alias, $base_field, '', $params);
foreach ($entity_information as $entity_type => $info) {
$entity_info = \Drupal::entityManager()->getDefinition($entity_type);
$base_field = empty($table['revision']) ? $entity_info['entity_keys']['id'] : $entity_info['entity_keys']['revision'];
$this->addField($info['alias'], $base_field, '', $params);
}
}
......@@ -1451,9 +1451,11 @@ function execute(ViewExecutable $view) {
* Returns an array of all tables from the query that map to an entity type.
*
* Includes the base table and all relationships, if eligible.
*
* Available keys for each table:
* - base: The actual base table (i.e. "user" for an author relationship).
* - relationship_id: The id of the relationship, or "none".
* - alias: The alias used for the relationship.
* - entity_type: The entity type matching the base table.
* - revision: A boolean that specifies whether the table is a base table or
* a revision table of the entity type.
......@@ -1461,14 +1463,17 @@ function execute(ViewExecutable $view) {
* @return array
* An array of table information, keyed by table alias.
*/
public function getEntityTables() {
public function getEntityTableInfo() {
// Start with the base table.
$entity_tables = array();
$views_data = Views::viewsData();
$base_table_data = $views_data->get($this->view->storage->get('base_table'));
$base_table = $this->view->storage->get('base_table');
$base_table_data = $views_data->get($base_table);
if (isset($base_table_data['table']['entity type'])) {
$entity_tables[$this->view->storage->get('base_table')] = array(
'base' => $this->view->storage->get('base_table'),
$entity_tables[$base_table_data['table']['entity type']] = array(
'base' => $base_table,
'alias' => $base_table,
'relationship_id' => 'none',
'entity_type' => $base_table_data['table']['entity type'],
'revision' => FALSE,
......@@ -1478,9 +1483,10 @@ public function getEntityTables() {
foreach ($this->view->relationship as $relationship_id => $relationship) {
$table_data = $views_data->get($relationship->definition['base']);
if (isset($table_data['table']['entity type'])) {
$entity_tables[$relationship->alias] = array(
$entity_tables[$table_data['table']['entity type']] = array(
'base' => $relationship->definition['base'],
'relationship_id' => $relationship_id,
'alias' => $relationship->alias,
'entity_type' => $table_data['table']['entity type'],
'revision' => FALSE,
);
......@@ -1489,7 +1495,7 @@ public function getEntityTables() {
// Determine which of the tables are revision tables.
foreach ($entity_tables as $table_alias => $table) {
$info = entity_get_info($table['entity_type']);
$info = \Drupal::entityManager()->getDefinition($table['entity_type']);
if (isset($info['revision table']) && $info['revision table'] == $table['base']) {
$entity_tables[$table_alias]['revision'] = TRUE;
}
......@@ -1506,36 +1512,34 @@ public function getEntityTables() {
* $result->_relationship_entities[$relationship_id];
*/
function loadEntities(&$results) {
$entity_tables = $this->getEntityTables();
$entity_information = $this->getEntityTableInfo();
// No entity tables found, nothing else to do here.
if (empty($entity_tables)) {
if (empty($entity_information)) {
return;
}
// Assemble a list of entities to load.
$ids_by_table = array();
foreach ($entity_tables as $table_alias => $table) {
$entity_type = $table['entity_type'];
$info = entity_get_info($entity_type);
$id_key = empty($table['revision']) ? $info['entity_keys']['id'] : $info['entity_keys']['revision'];
$id_alias = $this->getFieldAlias($table_alias, $id_key);
$ids_by_type = array();
foreach ($entity_information as $entity_type => $info) {
$entity_info = \Drupal::entityManager()->getDefinition($entity_type);
$id_key = empty($table['revision']) ? $entity_info['entity_keys']['id'] : $entity_info['entity_keys']['revision'];
$id_alias = $this->getFieldAlias($info['alias'], $id_key);
foreach ($results as $index => $result) {
// Store the entity id if it was found.
if (isset($result->{$id_alias}) && $result->{$id_alias} != '') {
$ids_by_table[$table_alias][$index] = $result->$id_alias;
$ids_by_type[$entity_type][$index] = $result->$id_alias;
}
}
}
// Load all entities and assign them to the correct result row.
foreach ($ids_by_table as $table_alias => $ids) {
$table = $entity_tables[$table_alias];
$entity_type = $table['entity_type'];
$relationship_id = $table['relationship_id'];
foreach ($ids_by_type as $entity_type => $ids) {
$info = $entity_information[$entity_type];
$relationship_id = $info['relationship_id'];
// Drupal core currently has no way to load multiple revisions. Sad.
if ($table['revision']) {
if ($info['revision']) {
$entities = array();
foreach ($ids as $revision_id) {
$entity = entity_revision_load($entity_type, $revision_id);
......
<?php
/**
* @file
* Contains \Drupal\views\Tests\Plugin\CacheTagTest.
*/
namespace Drupal\views\Tests\Plugin;
use Drupal\views\Views;
/**
* Tests the Tag class.
*
* @see \Drupal\views\Plugin\views\cache\Tag
*/
class CacheTagTest extends PluginTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_tag_cache');
/**
* Views used by this test.
*
* @var array
*/
public static $modules = array('node');
/**
* The node storage controller.
*
* @var \Drupal\node\NodeStorageController
*/
protected $nodeStorageController;
/**
* The node view builder.
*
* @var \Drupal\node\NodeViewBuilder
*/
protected $nodeViewBuilder;
/**
* The user view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilder
*/
protected $userViewBuilder;
/**
* An array of page nodes.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $pages;
/**
* An article node.
*
* @var \Drupal\node\NodeInterface
*/
protected $article;
/**
* A test user.
*
* @var \Drupal\user\UserInterface
*/
protected $user;
public static function getInfo() {
return array(
'name' => 'Cache tag',
'description' => 'Tests tag cache plugin.',
'group' => 'Views Plugins'
);
}
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
$this->nodeStorageController = $this->container->get('entity.manager')->getStorageController('node');
$this->nodeViewBuilder = $this->container->get('entity.manager')->getViewBuilder('node');
$this->userViewBuilder = $this->container->get('entity.manager')->getViewBuilder('user');
for ($i = 1; $i <= 5; $i++) {
$this->pages[] = $this->drupalCreateNode(array('title' => "Test $i", 'type' => 'page'));
}
$this->article = $this->drupalCreateNode(array('title' => "Test article", 'type' => 'article'));
$this->user = $this->drupalCreateUser();
}
/**
* Tests the tag cache plugin.
*/
public function testTagCaching() {
$view = Views::getView('test_tag_cache');
$view->render();
// Saving the view should invalidate the tags.
$cache_plugin = $view->display_handler->getPlugin('cache');
$this->assertTrue($cache_plugin->cacheGet('results'), 'Results cache found.');
$this->assertTrue($cache_plugin->cacheGet('output'), 'Output cache found.');
$view->storage->save();
$this->assertFalse($cache_plugin->cacheGet('results'), 'Results cache empty after the view is saved.');
$this->assertFalse($cache_plugin->cacheGet('output'), 'Output cache empty after the view is saved.');
$view->destroy();
$view->render();
// Test invalidating the nodes in this view invalidates the cache.
$cache_plugin = $view->display_handler->getPlugin('cache');
$this->assertTrue($cache_plugin->cacheGet('results'), 'Results cache found.');
$this->assertTrue($cache_plugin->cacheGet('output'), 'Output cache found.');
$this->nodeViewBuilder->resetCache($this->pages);
$this->assertFalse($cache_plugin->cacheGet('results'), 'Results cache empty after resetCache is called with pages.');
$this->assertFalse($cache_plugin->cacheGet('output'), 'Output cache empty after resetCache is called with pages.');
$view->destroy();
$view->render();
// Test saving a node in this view invalidates the cache.
$cache_plugin = $view->display_handler->getPlugin('cache');
$this->assertTrue($cache_plugin->cacheGet('results'), 'Results cache found.');
$this->assertTrue($cache_plugin->cacheGet('output'), 'Output cache found.');