diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 1bef225488a9bcf1d563b0e6d4e6012216931352..ec90b7e04a4afe3b80ff7a6c8a12606917550a4e 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -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); } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 2300f60031342d13db700f1f89b68eaba84ce447..ed95b23127a12925c274254a71c99c5dc4b3fd2d 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -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. */ diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 90e4699c1dff2be10ed2e9efaa2e67e2922df137..ab621441a1005fee602284fece8e97030589e83b 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -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(); + } diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index b4c72a4ea837e661dfbb000cc8845f69dcbab2e9..a0b8154c8cd63437d502bf7c9a4fc0a15f4120a6 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -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); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilderInterface.php b/core/lib/Drupal/Core/Entity/EntityViewBuilderInterface.php index b41848b971722fcf3ed588e36d8ab1e2990cce97..aee9588d45ba838d457588f6a542e0b1163e54f7 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilderInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilderInterface.php @@ -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(); + } diff --git a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php index cb448515352d9a4905a6c60613e10ef0cdff363b..7db7b70c5911cb3e011d9912922e2570bf6c4af8 100644 --- a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php +++ b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php @@ -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)); - } - } - } diff --git a/core/modules/block/lib/Drupal/block/Entity/Block.php b/core/modules/block/lib/Drupal/block/Entity/Block.php index aa9141c11c9a14f8f5e3a5ac5b062516c7a4342d..f1111fed1188210d8d377045b1f1776fcb71700f 100644 --- a/core/modules/block/lib/Drupal/block/Entity/Block.php +++ b/core/modules/block/lib/Drupal/block/Entity/Block.php @@ -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); + } + } diff --git a/core/modules/comment/tests/Drupal/comment/Tests/Entity/CommentLockTest.php b/core/modules/comment/tests/Drupal/comment/Tests/Entity/CommentLockTest.php index 2101d935624441593dd6b0a39a3a5a9b46f0c032..887d3ca5e1e869292431c754ae6a320609085945 100644 --- a/core/modules/comment/tests/Drupal/comment/Tests/Entity/CommentLockTest.php +++ b/core/modules/comment/tests/Drupal/comment/Tests/Entity/CommentLockTest.php @@ -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); diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 2e71d68b775238b4fe89a246956acc64f91a1896..75b781daf041186cefc49c294cdd56b931243fce 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -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'); } diff --git a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php index 2c475a59922989e5a5b4bc68162aca0599b79dea..bd8f0f90af35628a1e0ffc22267a90b0c242b84d 100644 --- a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php +++ b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php @@ -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 diff --git a/core/modules/node/lib/Drupal/node/Entity/NodeType.php b/core/modules/node/lib/Drupal/node/Entity/NodeType.php index 8ca6f72fc119870d799c30574ab04a9d87d722f7..fde17a295fabd2c326404418700cba83bf097b9d 100644 --- a/core/modules/node/lib/Drupal/node/Entity/NodeType.php +++ b/core/modules/node/lib/Drupal/node/Entity/NodeType.php @@ -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())); - } } /** diff --git a/core/modules/system/core.api.php b/core/modules/system/core.api.php index 58445e07ffb938f787c2594d01727c00d2462954..1ecc9c1a744d470d3629234089c8b24ae50b0762 100644 --- a/core/modules/system/core.api.php +++ b/core/modules/system/core.api.php @@ -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 * diff --git a/core/modules/system/lib/Drupal/system/Entity/Menu.php b/core/modules/system/lib/Drupal/system/Entity/Menu.php index 4b6cee5d2343dfa52057eb0a92e6686524fac4c2..2ea8dd18fde3406ddf6eae82c5ae1e36e2c1b9a7 100644 --- a/core/modules/system/lib/Drupal/system/Entity/Menu.php +++ b/core/modules/system/lib/Drupal/system/Entity/Menu.php @@ -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))); - } - } diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php index 5112cc2a1ae349a59893a41a6b0820065f5b0217..e4a6a522b6c3ca24c0c376f87d18a6752acf8cc8 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php @@ -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', diff --git a/core/modules/user/lib/Drupal/user/Entity/Role.php b/core/modules/user/lib/Drupal/user/Entity/Role.php index 81a0ee63e7d7f6c2d8dfeaeb7aeb792e8aa4f113..d9cd07ca38a11ed7263df3296b5c536c374352f8 100644 --- a/core/modules/user/lib/Drupal/user/Entity/Role.php +++ b/core/modules/user/lib/Drupal/user/Entity/Role.php @@ -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)); - } - } diff --git a/core/modules/user/lib/Drupal/user/PermissionsHash.php b/core/modules/user/lib/Drupal/user/PermissionsHash.php index 3c6fe8754440412c778cbcc446fd2caf6c16ab3e..1fed13eeb000c1bbbf1be4aa27def62a137a4058 100644 --- a/core/modules/user/lib/Drupal/user/PermissionsHash.php +++ b/core/modules/user/lib/Drupal/user/PermissionsHash.php @@ -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; diff --git a/core/modules/views/lib/Drupal/views/Entity/View.php b/core/modules/views/lib/Drupal/views/Entity/View.php index 2beb7ac96f0d22c6157e348edb0f0b3aaa43863b..304d1f8ddbc9e0d9d073a1af8332e140cd15e553 100644 --- a/core/modules/views/lib/Drupal/views/Entity/View.php +++ b/core/modules/views/lib/Drupal/views/Entity/View.php @@ -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); } /** diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php index ca3d5ecb19a7f9e02ddac2c94e78a47c4c3735a2..0aabe8545b010e89d3611d63afceb37ae2783e77 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -1213,4 +1213,18 @@ public function calculateDependencies() { public function getConfigDependencyName() { } + /** + * {@inheritdoc} + */ + public function getCacheTag() { + $this->storage->getCacheTag(); + } + + /** + * {@inheritdoc} + */ + public function getListCacheTags() { + $this->storage->getListCacheTags(); + } + } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php index 016cc738f6894c5aba861c603ba34e3f9819476f..3c52f73988fc192548ace7161ec62809c503e3df 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -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]); } } + } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php index 10ba1a6cab7f22d541f1918718090ad0d575acfb..caf022674fc7e2a4cfcc224b7984c3dc4f9cb9d5 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php @@ -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') ->with('test_entity_type_create'); @@ -221,6 +246,12 @@ public function testSaveInsert(EntityInterface $entity) { $config_object->expects($this->once()) ->method('save'); + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + )); + $this->configFactory->expects($this->once()) ->method('get') ->with('the_config_prefix.foo') @@ -273,6 +304,13 @@ public function testSaveUpdate(EntityInterface $entity) { $config_object->expects($this->once()) ->method('save'); + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + $this->entityTypeId => array('foo'), // Own cache tag. + )); + $this->configFactory->expects($this->exactly(2)) ->method('loadMultiple') ->with(array('the_config_prefix.foo')) @@ -325,6 +363,13 @@ public function testSaveRename(ConfigEntityInterface $entity) { $config_object->expects($this->once()) ->method('save'); + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + $this->entityTypeId => array('bar'), // Own cache tag. + )); + $this->configFactory->expects($this->once()) ->method('rename') ->will($this->returnValue($config_object)); @@ -362,6 +407,9 @@ public function testSaveRename(ConfigEntityInterface $entity) { * @expectedExceptionMessage The entity does not have an ID. */ public function testSaveInvalid() { + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $entity = $this->getMockEntity(); $this->entityStorage->save($entity); } @@ -383,6 +431,9 @@ public function testSaveDuplicate() { $config_object->expects($this->never()) ->method('save'); + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $this->configFactory->expects($this->once()) ->method('get') ->with('the_config_prefix.foo') @@ -410,6 +461,9 @@ public function testSaveMismatch() { $config_object->expects($this->never()) ->method('save'); + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $this->configFactory->expects($this->once()) ->method('get') ->with('the_config_prefix.foo') @@ -439,6 +493,12 @@ public function testSaveNoMismatch() { $config_object->expects($this->once()) ->method('save'); + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + )); + $this->configFactory->expects($this->once()) ->method('get') ->with('the_config_prefix.baz') @@ -482,6 +542,9 @@ public function testSaveChangedUuid() { array('id', 'foo'), ))); + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $this->configFactory->expects($this->at(1)) ->method('loadMultiple') ->with(array('the_config_prefix.foo')) @@ -643,6 +706,9 @@ public function testLoadRevision() { * @covers ::deleteRevision() */ public function testDeleteRevision() { + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $this->assertSame(NULL, $this->entityStorage->deleteRevision(1)); } @@ -669,6 +735,13 @@ public function testDelete() { $config_map[] = array("the_config_prefix.$id", $config_object); } + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + $this->entityTypeId => array('foo', 'bar'), // Own cache tag. + )); + $this->configFactory->expects($this->exactly(2)) ->method('get') ->will($this->returnValueMap($config_map)); @@ -710,6 +783,9 @@ public function testDeleteNothing() { $this->configFactory->expects($this->never()) ->method('get'); + $this->cacheBackend->expects($this->never()) + ->method('invalidateTags'); + $this->entityStorage->delete(array()); } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php index 47c9ad4c35cf021f02157bab56e8293cd6581a24..6ccad149eeb0cf63deea38668590e49a185554fe 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php @@ -67,6 +67,13 @@ class EntityUnitTest extends UnitTestCase { */ protected $languageManager; + /** + * The mocked cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cacheBackend; + /** * The entity values. * @@ -112,11 +119,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\Entity\Entity', array($this->values, $this->entityTypeId)); @@ -280,9 +290,26 @@ public function testPreSave() { * @covers ::postSave */ public function testPostSave() { + $this->cacheBackend->expects($this->at(0)) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + )); + $this->cacheBackend->expects($this->at(1)) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId . 's' => TRUE, // List cache tag. + $this->entityTypeId => array($this->values['id']), // Own cache tag. + )); + // This method is internal, so check for errors on calling it only. $storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface'); - $this->entity->postSave($storage); + + // A creation should trigger the invalidation of the "list" cache tag. + $this->entity->postSave($storage, FALSE); + // An update should trigger the invalidation of both the "list" and the + // "own" cache tags. + $this->entity->postSave($storage, TRUE); } /** @@ -317,16 +344,23 @@ public function testPreDelete() { * @covers ::postDelete */ public function testPostDelete() { + $this->cacheBackend->expects($this->once()) + ->method('invalidateTags') + ->with(array( + $this->entityTypeId => array($this->values['id']), + $this->entityTypeId . 's' => TRUE, + )); $storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface'); $entity = $this->getMockBuilder('\Drupal\Core\Entity\Entity') + ->setConstructorArgs(array($this->values, $this->entityTypeId)) ->setMethods(array('onSaveOrDelete')) - ->disableOriginalConstructor() ->getMock(); $entity->expects($this->once()) ->method('onSaveOrDelete'); - $this->entity->postDelete($storage, array($entity)); + $entities = array($this->values['id'] => $entity); + $this->entity->postDelete($storage, $entities); } /**