Commit bf61ca13 authored by alexpott's avatar alexpott

Issue #2217749 by Wim Leers, Jalandhar, visabhishek, damiankloip: Entity base...

Issue #2217749 by Wim Leers, Jalandhar, visabhishek, damiankloip: Entity base class should provide standardized cache tags and built-in invalidation.
parent 88f026eb
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Utility\String;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Config\ConfigDuplicateUUIDException;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -163,6 +164,8 @@ public function enable() {
* {@inheritdoc}
*/
public function disable() {
// An entity was disabled, invalidate its own cache tag.
Cache::invalidateTags(array($this->entityTypeId => array($this->id())));
return $this->setStatus(FALSE);
}
......
......@@ -7,7 +7,9 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\DependencySerialization;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
......@@ -357,9 +359,7 @@ public function preSave(EntityStorageInterface $storage) {
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
$this->onSaveOrDelete();
if ($update) {
$this->onUpdateBundleEntity();
}
$this->invalidateTagsOnSave($update);
}
/**
......@@ -384,9 +384,7 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
foreach ($entities as $entity) {
$entity->onSaveOrDelete();
}
self::invalidateTagsOnDelete($entities);
}
/**
......@@ -402,6 +400,21 @@ public function referencedEntities() {
return array();
}
/**
* {@inheritdoc}
*/
public function getCacheTag() {
return array($this->entityTypeId => array($this->id()));
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
// @todo Add bundle-specific listing cache tag? https://drupal.org/node/2145751
return array($this->entityTypeId . 's' => TRUE);
}
/**
* Acts on an entity after it was saved or deleted.
*/
......@@ -421,6 +434,46 @@ protected function onSaveOrDelete() {
}
}
/**
* Invalidates an entity's cache tags upon save.
*
* @param bool $update
* TRUE if the entity has been updated, or FALSE if it has been inserted.
*/
protected function invalidateTagsOnSave($update) {
// An entity was created or updated: invalidate its list cache tags. (An
// updated entity may start to appear in a listing because it now meets that
// listing's filtering requirements. A newly created entity may start to
// appear in listings because it did not exist before.)
$tags = $this->getListCacheTags();
if ($update) {
// An existing entity was updated, also invalidate its unique cache tag.
$tags = NestedArray::mergeDeep($tags, $this->getCacheTag());
$this->onUpdateBundleEntity();
}
Cache::invalidateTags($tags);
}
/**
* Invalidates an entity's cache tags upon delete.
*
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entities.
*/
protected static function invalidateTagsOnDelete(array $entities) {
$tags = array();
foreach ($entities as $entity) {
// An entity was deleted: invalidate its own cache tag, but also its list
// cache tags. (A deleted entity may cause changes in a paged list on
// other pages than the one it's on. The one it's on is handled by its own
// cache tag, but subsequent list pages would not be invalidated, hence we
// must invalidate its list cache tags as well.)
$tags = NestedArray::mergeDeepArray(array($tags, $entity->getCacheTag(), $entity->getListCacheTags()));
$entity->onSaveOrDelete();
}
Cache::invalidateTags($tags);
}
/**
* Acts on entities of which this entity is a bundle entity type.
*/
......
......@@ -324,4 +324,23 @@ public function setOriginalId($id);
*/
public function toArray();
/**
* The unique cache tag associated with this entity.
*
* @return array
* An array of cache tags.
*/
public function getCacheTag();
/**
* The list cache tags associated with this entity.
*
* Enables code listing entities of this type to ensure that newly created
* entities show up immediately.
*
* @return array
* An array of cache tags.
*/
public function getListCacheTags();
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldItemInterface;
......@@ -144,11 +145,8 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
'#view_mode' => $view_mode,
'#langcode' => $langcode,
'#cache' => array(
'tags' => array(
$this->entityTypeId . '_view' => TRUE,
$this->entityTypeId => array($entity->id()),
),
)
'tags' => NestedArray::mergeDeep($this->getCacheTag(), $entity->getCacheTag()),
),
);
// Cache the rendered output if permitted by the view mode and global entity
......@@ -274,14 +272,12 @@ public function resetCache(array $entities = NULL) {
// Always invalidate the ENTITY_TYPE_list tag.
$tags = array($this->entityTypeId . '_list' => TRUE);
foreach ($entities as $entity) {
$id = $entity->id();
$tags[$this->entityTypeId][$id] = $id;
$tags[$this->entityTypeId . '_view_' . $entity->bundle()] = TRUE;
$tags = NestedArray::mergeDeep($tags, $entity->getCacheTag());
}
Cache::deleteTags($tags);
Cache::invalidateTags($tags);
}
else {
Cache::deleteTags(array($this->entityTypeId . '_view' => TRUE));
Cache::invalidateTags($this->getCacheTag());
}
}
......@@ -364,4 +360,11 @@ protected function isViewModeCacheable($view_mode) {
return !empty($view_modes_info[$view_mode]['cache']);
}
/**
* {@inheritdoc}
*/
public function getCacheTag() {
return array($this->entityTypeId . '_view' => TRUE);
}
}
......@@ -149,4 +149,15 @@ public function viewField(FieldItemListInterface $items, $display_options = arra
*/
public function viewFieldItem(FieldItemInterface $item, $display_options = array());
/**
* The cache tag associated with this entity view builder.
*
* An entity view builder is instantiated on a per-entity type basis, so the
* cache tags are also per-entity type.
*
* @return array
* An array of cache tags.
*/
public function getCacheTag();
}
......@@ -70,14 +70,13 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
// Set cache tags; these always need to be set, whether the block is
// cacheable or not, so that the page cache is correctly informed.
$default_cache_tags = array(
'content' => TRUE,
'block_view' => TRUE,
'block' => array($entity->id()),
'theme' => $entity->get('theme'),
);
$build[$entity_id]['#cache']['tags'] = NestedArray::mergeDeep($default_cache_tags, $plugin->getCacheTags());
$build[$entity_id]['#cache']['tags'] = NestedArray::mergeDeepArray(array(
array('content' => TRUE),
$this->getCacheTag(), // Block view builder cache tag.
$entity->getCacheTag(), // Block entity cache tag.
$entity->getListCacheTags(), // Block entity list cache tags.
$plugin->getCacheTags(), // Block plugin cache tags.
));
if ($plugin->isCacheable()) {
$build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
......@@ -156,16 +155,4 @@ public function buildBlock($build) {
return $build;
}
/**
* {@inheritdoc}
*/
public function resetCache(array $entities = NULL) {
if (isset($entities)) {
Cache::invalidateTags(array('block' => array_keys($entities)));
}
else {
Cache::invalidateTags(array('block_view' => TRUE));
}
}
}
......@@ -148,31 +148,6 @@ public function toArray() {
return $properties;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($update) {
Cache::invalidateTags(array('block' => $this->id()));
}
// When placing a new block, invalidate all cache entries for this theme,
// since any page that uses this theme might be affected.
else {
Cache::invalidateTags(array('theme' => $this->theme));
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
Cache::invalidateTags(array('block' => array_keys($entities)));
}
/**
* Sorts active blocks by weight; sorts inactive blocks by name.
*/
......@@ -202,4 +177,16 @@ public function calculateDependencies() {
return $this->dependencies;
}
/**
* {@inheritdoc}
*
* Block configuration entities are a special case: one block entity stores
* the placement of one block in one theme. Instead of using an entity type-
* specific list cache tag like most entities, use the cache tag of the theme
* this block is placed in instead.
*/
public function getListCacheTags() {
return array('theme' => $this->theme);
}
}
......@@ -36,6 +36,8 @@ public function testLocks() {
$container = new ContainerBuilder();
$container->set('module_handler', $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'));
$container->set('current_user', $this->getMock('Drupal\Core\Session\AccountInterface'));
$container->set('cache.test', $this->getMock('Drupal\Core\Cache\CacheBackendInterface'));
$container->setParameter('cache_bins', array('cache.test' => 'test'));
$container->register('request', 'Symfony\Component\HttpFoundation\Request');
$lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$cid = 2;
......@@ -84,7 +86,12 @@ public function testLocks() {
->method('get')
->with('status')
->will($this->returnValue((object) array('value' => NULL)));
$comment->expects($this->once())
->method('getCacheTag')
->will($this->returnValue(array('comment' => array($cid))));
$comment->expects($this->once())
->method('getListCacheTags')
->will($this->returnValue(array('comments' => TRUE)));
$storage = $this->getMock('Drupal\comment\CommentStorageInterface');
$comment->preSave($storage);
$comment->postSave($storage);
......
......@@ -177,7 +177,6 @@ function filter_formats(AccountInterface $account = NULL) {
* @see filter_formats()
*/
function filter_formats_reset() {
Cache::deleteTags(array('filter_formats' => TRUE));
drupal_static_reset('filter_formats');
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\filter\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\EntityWithPluginBagInterface;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -197,7 +196,6 @@ public function disable() {
// Clear the filter cache whenever a text format is disabled.
filter_formats_reset();
Cache::deleteTags(array('filter_format' => $this->format));
return $this;
}
......@@ -235,11 +233,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
// Clear the static caches of filter_formats() and others.
filter_formats_reset();
if ($update) {
// Clear the filter cache whenever a text format is updated.
Cache::deleteTags(array('filter_format' => $this->id()));
}
elseif (!$this->isSyncing()) {
if (!$update && !$this->isSyncing()) {
// Default configuration of modules and installation profiles is allowed
// to specify a list of user roles to grant access to for the new format;
// apply the defined user role permissions when a new format is inserted
......
......@@ -8,7 +8,6 @@
namespace Drupal\node\Entity;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\node\NodeTypeInterface;
......@@ -157,9 +156,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if (!$update) {
// Clear the node type cache, so the new type appears.
Cache::deleteTags(array('node_types' => TRUE));
entity_invoke_bundle_hook('create', 'node', $this->id());
// Create a body if the create_body property is true and we're not in
......@@ -170,9 +166,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
}
}
elseif ($this->getOriginalId() != $this->id()) {
// Clear the node type cache to reflect the rename.
Cache::deleteTags(array('node_types' => TRUE));
$update_count = node_type_update_nodes($this->getOriginalId(), $this->id());
if ($update_count) {
drupal_set_message(format_plural($update_count,
......@@ -185,10 +178,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
}
entity_invoke_bundle_hook('rename', 'node', $this->getOriginalId(), $this->id());
}
else {
// Invalidate the cache tag of the updated node type only.
Cache::invalidateTags(array('node_type' => $this->id()));
}
}
/**
......
......@@ -279,13 +279,29 @@
* @section tags Cache Tags
*
* The fourth argument of the set() method can be used to specify cache tags,
* which are used to identify what type of data is included in each cache. Each
* cache can have multiple tags, and each tag has a string key and a value. The
* value can be:
* - TRUE, to indicate that this type of data is present in the cache.
* which are used to identify what type of data is included in each cache item.
* Each cache item can have multiple cache tags, and each cache tag has a string
* key and a value. The value can be:
* - TRUE, to indicate that this type of data is present in the cache item.
* - An array of values. For example, the "node" tag indicates that particular
* nodes' data is present in the cache, so its value is an array of node IDs.
* Data that has been tagged can be deleted or invalidated as a group.
* node's data is present in the cache item, so its value is an array of node
* IDs.
* Data that has been tagged can be deleted or invalidated as a group: no matter
* the Cache ID (cid) of the cache item, no matter in which cache bin a cache
* item lives; as long as it is tagged with a certain cache tag, it will be
* deleted or invalidated.
*
* Because of that, cache tags are a solution to the cache invalidation problem:
* - For caching to be effective, each cache item must only be invalidated when
* absolutely necessary. (i.e. maximizing the cache hit ratio.)
* - For caching to be correct, each cache item that depends on a certain thing
* must be invalidated whenever that certain thing is modified.
*
* A typical scenario: a user has modified a node that appears in two views,
* three blocks and on twelve pages. Without cache tags, we couldn't possibly
* know which cache items to invalidate, so we'd have to invalidate everything:
* we had to sacrifice effectiveness to achieve correctness. With cache tags, we
* can have both.
*
* Example:
* @code
......@@ -297,14 +313,25 @@
* );
* \Drupal::cache()->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags);
*
* // Delete or invalidate all caches with certain tags.
* // Delete or invalidate all cache items with certain tags.
* \Drupal\Core\Cache\Cache::deleteTags(array('node' => array(1));
* \Drupal\Core\Cache\Cache::invalidateTags(array('user' => array(1)));
* @endcode
*
* @todo Update cache tag deletion in https://drupal.org/node/918538
* Drupal is a content management system, so naturally you want changes to your
* content to be reflected everywhere, immediately. That's why we made sure that
* every entity type in Drupal 8 automatically has support for cache tags: when
* you save an entity, you can be sure that the cache items that have the
* corresponding cache tags will be invalidated.
* This also is the case when you define your own entity types: you'll get the
* exact same cache tag invalidation as any of the built-in entity types, with
* the ability to override any of the default behavior if needed.
* See \Drupal\Core\Entity\EntityInterface::getCacheTag(),
* \Drupal\Core\Entity\EntityInterface::getListCacheTags(),
* \Drupal\Core\Entity\Entity::invalidateTagsOnSave() and
* \Drupal\Core\Entity\Entity::invalidateTagsOnDelete().
*
* @todo Extend entity cache tags based on https://drupal.org/node/2217749
* @todo Update cache tag deletion in https://drupal.org/node/918538
*
* @section configuration Configuration
*
......
......@@ -7,7 +7,6 @@
namespace Drupal\system\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\system\MenuInterface;
......@@ -81,22 +80,4 @@ public function isLocked() {
return (bool) $this->locked;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
Cache::invalidateTags(array('menu' => $this->id()));
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
Cache::invalidateTags(array('menu' => array_keys($entities)));
}
}
......@@ -222,7 +222,7 @@ public function testReferencedEntity() {
'entity_test:' . $this->referencing_entity->id(),
// Includes the main entity's cache tags, since this entity references it.
$cache_tag,
$view_cache_tag
$view_cache_tag,
);
$non_referencing_entity_cache_tags = array(
'entity_test_view:1',
......
......@@ -7,7 +7,6 @@
namespace Drupal\user\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\user\RoleInterface;
......@@ -134,20 +133,8 @@ public function preSave(EntityStorageInterface $storage) {
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
Cache::invalidateTags(array('role' => $this->id()));
// Clear render cache.
entity_render_cache_clear();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$ids = array_keys($entities);
$storage->deleteRoleReferences($ids);
Cache::invalidateTags(array('role' => $ids));
}
}
......@@ -58,7 +58,7 @@ public function generate(AccountInterface $account) {
}
else {
$permissions_hash = $this->doGenerate($sorted_roles);
$this->cache->set("user_permissions_hash:$role_list", $permissions_hash, Cache::PERMANENT, array('role' => $sorted_roles));
$this->cache->set("user_permissions_hash:$role_list", $permissions_hash, Cache::PERMANENT, array('user_role' => $sorted_roles));
}
return $permissions_hash;
......
......@@ -7,7 +7,6 @@
namespace Drupal\views\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\views\Views;
......@@ -305,10 +304,7 @@ public function calculateDependencies() {
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $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();
}
......@@ -359,17 +355,9 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
parent::postDelete($storage, $entities);
$tempstore = \Drupal::service('user.tempstore')->get('views');
$tags = array();
foreach ($entities as $entity) {
$id = $entity->id();
$tempstore->delete($id);
$tags['view'][$id] = $id;
$tempstore->delete($entity->id());
}
// Clear cache tags for these views.
// @todo Remove if views implements a view_builder controller.
Cache::deleteTags($tags);
}
/**
......
......@@ -1213,4 +1213,18 @@ public function calculateDependencies() {
public function getConfigDependencyName() {
}
/**
* {@inheritdoc}
*/
public function getCacheTag() {
$this->storage->getCacheTag();
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
$this->storage->getListCacheTags();
}
}
......@@ -78,6 +78,13 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
*/
protected $id;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cacheBackend;
/**
* {@inheritdoc}
*/
......@@ -120,10 +127,14 @@ public function setUp() {
->with('en')
->will($this->returnValue(new Language(array('id' => 'en'))));
$this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$container = new ContainerBuilder();
$container->set('entity.manager', $this->entityManager);
$container->set('uuid', $this->uuid);
$container->set('language_manager', $this->languageManager);
$container->set('cache.test', $this->cacheBackend);
$container->setParameter('cache_bins', array('cache.test' => 'test'));
\Drupal::setContainer($container);
$this->entity = $this->getMockForAbstractClass('\Drupal\Core\Config\Entity\ConfigEntityBase', array($values, $this->entityTypeId));
......@@ -333,6 +344,10 @@ public function testEnable() {
* @depends testSetStatus
*/
public function testDisable() {
$this->cacheBackend->expects($this->once())
->method('invalidateTags')
->with(array($this->entityTypeId => array($this->id)));
$this->entity->setStatus(TRUE);
$this->assertSame($this->entity, $this->entity->disable());
$this->assertFalse($this->entity->status());
......@@ -425,4 +440,5 @@ public function testToArray() {
$this->assertSame($this->entity->get($name), $properties[$name]);
}
}
}
......@@ -27,6 +27,13 @@ class ConfigEntityStorageTest extends UnitTestCase {
*/
protected $entityType;
/**
* The type ID of the entity under test.
*
* @var string
*/
protected $entityTypeId;
/**
* The module handler.
*
......@@ -83,6 +90,13 @@ class ConfigEntityStorageTest extends UnitTestCase {
*/
protected $entityManager;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cacheBackend;
/**
* {@inheritdoc}
*/
......@@ -101,6 +115,7 @@ protected function setUp() {
parent::setUp();
$this->entityType = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$this->entityTypeId = 'test_entity_type';
$this->entityType->expects($this->any())
->method('getKey')
->will($this->returnValueMap(array(
......@@ -109,7 +124,7 @@ protected function setUp() {
)));
$this->entityType->expects($this->any())
->method('id')
->will($this->returnValue('test_entity_type'));
->will($this->returnValue($this->entityTypeId));
$this->entityType->expects($this->any())
->method('getConfigPrefix')
->will($this->returnValue('the_config_prefix'));
......@@ -144,8 +159,12 @@ protected function setUp() {
->with('test_entity_type')
->will($this->returnValue($this->entityType));
$this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$container = new ContainerBuilder();
$container->set('entity.manager', $this->entityManager);
$container->set('cache.test', $this->cacheBackend);
$container->setParameter('cache_bins', array('cache.test' => 'test'));
\Drupal::setContainer($container);
}
......@@ -158,6 +177,9 @@ public function testCreateWithPredefinedUuid() {
->method('getClass')
->will($this->returnValue(get_class($this->getMockEntity())));
$this->cacheBackend->expects($this->never())
->method('invalidateTags');
$this->moduleHandler->expects($this->at(0))
->method('invokeAll')
->with('test_entity_type_create');
......@@ -183,6 +205,9 @@ public function testCreate() {
->method('getClass')
->will($this->returnValue(get_class($this->getMockEntity())));
$this->cacheBackend->expects($this->never())
->method('invalidateTags');
$this->moduleHandler->expects($this->at(0))
->method('invokeAll')