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 e68ca9358f)
merge-requests/64/head
catch 2020-01-17 11:48:31 +00:00
parent 43b42be33c
commit a7eacc877e
6 changed files with 196 additions and 5 deletions

View File

@ -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);
}
/**

View File

@ -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);
}

View File

@ -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'

View File

@ -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;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Drupal\FunctionalTests\Entity;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestWithBundle;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests that bundle tags are invalidated when entities change.
*
* @group Entity
*/
class EntityBundleListCacheTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['cache_test', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
EntityTestBundle::create([
'id' => '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');
}
}

View File

@ -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
*/