From a7eacc877ed96d4f5e62d63cefe66ebe136d69a9 Mon Sep 17 00:00:00 2001 From: catch Date: Fri, 17 Jan 2020 11:48:31 +0000 Subject: [PATCH] Issue #2145751 by jibran, yongt9412, DuaelFr, Berdir, pnagornyak, arpad.rozsa, mbovan, Wim Leers, Nixou, effulgentsia, Fabianx, kristiaanvandeneynde, anavarre, dawehner: Introduce ENTITY_TYPE_list:BUNDLE cache tag and add it to single bundle listing (cherry picked from commit e68ca9358fa3e56ef9ae0746474bc89ffa236d38) --- .../Core/Config/Entity/ConfigEntityBase.php | 8 +- core/lib/Drupal/Core/Entity/EntityBase.php | 19 ++++- .../modules/cache_test/cache_test.routing.yml | 7 ++ .../src/Controller/CacheTestController.php | 26 ++++++ .../Entity/EntityBundleListCacheTest.php | 80 +++++++++++++++++++ .../Tests/Core/Entity/EntityUnitTest.php | 61 ++++++++++++++ 6 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 core/tests/Drupal/FunctionalTests/Entity/EntityBundleListCacheTest.php diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 38809c28c35..04e94f83abb 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -490,7 +490,7 @@ abstract class ConfigEntityBase extends EntityBase implements ConfigEntityInterf * already invalidates it. */ protected function invalidateTagsOnSave($update) { - Cache::invalidateTags($this->getEntityType()->getListCacheTags()); + Cache::invalidateTags($this->getListCacheTagsToInvalidate()); } /** @@ -500,7 +500,11 @@ abstract class ConfigEntityBase extends EntityBase implements ConfigEntityInterf * config system already invalidates them. */ protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) { - Cache::invalidateTags($entity_type->getListCacheTags()); + $tags = $entity_type->getListCacheTags(); + foreach ($entities as $entity) { + $tags = Cache::mergeTags($tags, $entity->getListCacheTagsToInvalidate()); + } + Cache::invalidateTags($tags); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityBase.php b/core/lib/Drupal/Core/Entity/EntityBase.php index 08470cbcdc9..1820192d335 100644 --- a/core/lib/Drupal/Core/Entity/EntityBase.php +++ b/core/lib/Drupal/Core/Entity/EntityBase.php @@ -493,12 +493,24 @@ abstract class EntityBase implements EntityInterface { return $this->cacheContexts; } + /** + * The list cache tags to invalidate for this entity. + * + * @return string[] + * Set of list cache tags. + */ + protected function getListCacheTagsToInvalidate() { + $tags = $this->getEntityType()->getListCacheTags(); + if ($this->getEntityType()->hasKey('bundle')) { + $tags[] = $this->getEntityTypeId() . '_list:' . $this->bundle(); + } + return $tags; + } + /** * {@inheritdoc} */ public function getCacheTagsToInvalidate() { - // @todo Add bundle-specific listing cache tag? - // https://www.drupal.org/node/2145751 if ($this->isNew()) { return []; } @@ -563,7 +575,7 @@ abstract class EntityBase implements EntityInterface { // 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->getEntityType()->getListCacheTags(); + $tags = $this->getListCacheTagsToInvalidate(); if ($this->hasLinkTemplate('canonical')) { // Creating or updating an entity may change a cached 403 or 404 response. $tags = Cache::mergeTags($tags, ['4xx-response']); @@ -592,6 +604,7 @@ abstract class EntityBase implements EntityInterface { // cache tag, but subsequent list pages would not be invalidated, hence we // must invalidate its list cache tags as well.) $tags = Cache::mergeTags($tags, $entity->getCacheTagsToInvalidate()); + $tags = Cache::mergeTags($tags, $entity->getListCacheTagsToInvalidate()); } Cache::invalidateTags($tags); } diff --git a/core/modules/system/tests/modules/cache_test/cache_test.routing.yml b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml index fb87d3ded0e..c785e19c9ed 100644 --- a/core/modules/system/tests/modules/cache_test/cache_test.routing.yml +++ b/core/modules/system/tests/modules/cache_test/cache_test.routing.yml @@ -4,3 +4,10 @@ cache_test.url_bubbling: _controller: '\Drupal\cache_test\Controller\CacheTestController::urlBubbling' requirements: _access: 'TRUE' + +cache_test_list.bundle_tags: + path: '/cache-test-list/{entity_type_id}/{bundle}' + defaults: + _controller: '\Drupal\cache_test\Controller\CacheTestController::bundleTags' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php index de2e8fa82a3..b773d5bb5ae 100644 --- a/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php +++ b/core/modules/system/tests/modules/cache_test/src/Controller/CacheTestController.php @@ -19,4 +19,30 @@ class CacheTestController { ]; } + /** + * Bundle listing tags invalidation. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle + * The bundle. + * + * @return array + * Renderable array. + */ + public function bundleTags($entity_type_id, $bundle) { + $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); + $entity_ids = $storage->getQuery()->condition('type', $bundle)->execute(); + $page = []; + + $entities = $storage->loadMultiple($entity_ids); + foreach ($entities as $entity) { + $page[$entity->id()] = [ + '#markup' => $entity->label(), + ]; + } + $page['#cache']['tags'] = [$entity_type_id . '_list:' . $bundle]; + return $page; + } + } diff --git a/core/tests/Drupal/FunctionalTests/Entity/EntityBundleListCacheTest.php b/core/tests/Drupal/FunctionalTests/Entity/EntityBundleListCacheTest.php new file mode 100644 index 00000000000..207c0b74b7d --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Entity/EntityBundleListCacheTest.php @@ -0,0 +1,80 @@ + 'bundle_a', + 'label' => 'Bundle A', + ])->save(); + EntityTestBundle::create([ + 'id' => 'bundle_b', + 'label' => 'Bundle B', + ])->save(); + } + + /** + * Tests that tags are invalidated when an entity with that bundle changes. + */ + public function testBundleListingCache() { + // Access to lists of test entities with each bundle. + $bundle_a_url = Url::fromRoute('cache_test_list.bundle_tags', ['entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'bundle_a']); + $bundle_b_url = Url::fromRoute('cache_test_list.bundle_tags', ['entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'bundle_b']); + $this->drupalGet($bundle_a_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); + $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_a']); + + $this->drupalGet($bundle_a_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); + $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_a']); + $this->drupalGet($bundle_b_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); + $this->assertCacheTags(['rendered', 'entity_test_with_bundle_list:bundle_b']); + $this->drupalGet($bundle_b_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); + $entity1 = EntityTestWithBundle::create(['type' => 'bundle_a', 'name' => 'entity1']); + $entity1->save(); + // Check that tags are invalidated after creating an entity of the current + // bundle. + $this->drupalGet($bundle_a_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); + $this->drupalGet($bundle_a_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); + // Check that tags are not invalidated after creating an entity of a + // different bundle than the current in the request. + $this->drupalGet($bundle_b_url); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php index 2fc86a66f79..b6963b30c59 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php @@ -443,6 +443,43 @@ class EntityUnitTest extends UnitTestCase { $this->entity->postSave($storage, TRUE); } + /** + * @covers ::postSave + */ + public function testPostSaveBundle() { + $this->cacheTagsInvalidator->expects($this->at(0)) + ->method('invalidateTags') + ->with([ + // List cache tag. + $this->entityTypeId . '_list', + $this->entityTypeId . '_list:' . $this->entity->bundle(), + ]); + $this->cacheTagsInvalidator->expects($this->at(1)) + ->method('invalidateTags') + ->with([ + // Own cache tag. + $this->entityTypeId . ':' . $this->values['id'], + // List cache tag. + $this->entityTypeId . '_list', + $this->entityTypeId . '_list:' . $this->entity->bundle(), + ]); + + $this->entityType->expects($this->atLeastOnce()) + ->method('hasKey') + ->with('bundle') + ->willReturn(TRUE); + + // This method is internal, so check for errors on calling it only. + $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface'); + + // A creation should trigger the invalidation of the global list cache tag + // and the one for the bundle. + $this->entity->postSave($storage, FALSE); + // An update should trigger the invalidation of the "list", bundle list and + // the "own" cache tags. + $this->entity->postSave($storage, TRUE); + } + /** * @covers ::preCreate */ @@ -493,6 +530,30 @@ class EntityUnitTest extends UnitTestCase { $this->entity->postDelete($storage, $entities); } + /** + * @covers ::postDelete + */ + public function testPostDeleteBundle() { + $this->cacheTagsInvalidator->expects($this->once()) + ->method('invalidateTags') + ->with([ + $this->entityTypeId . ':' . $this->values['id'], + $this->entityTypeId . '_list', + $this->entityTypeId . '_list:' . $this->entity->bundle(), + ]); + $this->entityType->expects($this->atLeastOnce()) + ->method('hasKey') + ->with('bundle') + ->willReturn(TRUE); + $storage = $this->createMock('\Drupal\Core\Entity\EntityStorageInterface'); + $storage->expects($this->once()) + ->method('getEntityType') + ->willReturn($this->entityType); + + $entities = [$this->values['id'] => $this->entity]; + $this->entity->postDelete($storage, $entities); + } + /** * @covers ::postLoad */