From 2b21781bb5e0c3108406d624b0b32c45021926c5 Mon Sep 17 00:00:00 2001 From: Kristiaan Van den Eynde Date: Wed, 3 Apr 2019 12:16:21 +0200 Subject: [PATCH] Issue #2551419 by kristiaanvandeneynde: Abstract RenderCache into a separate service that is capable of cache redirects in a non-render array-specific way --- core/core.services.yml | 5 +- core/lib/Drupal/Core/Cache/VariationCache.php | 210 ++++++++++++++++++ .../Core/Cache/VariationCacheFactory.php | 64 ++++++ .../Cache/VariationCacheFactoryInterface.php | 21 ++ .../Core/Cache/VariationCacheInterface.php | 86 +++++++ .../Core/Render/PlaceholderingRenderCache.php | 8 +- core/lib/Drupal/Core/Render/RenderCache.php | 152 ++++++------- .../src/Functional/ItemCacheTagsTest.php | 8 +- .../tests/src/Kernel/BlockViewBuilderTest.php | 25 ++- .../Functional/BlockContentCacheTagsTest.php | 6 +- .../src/Functional/ShortcutCacheTagsTest.php | 12 +- .../Entity/EntityCacheTagsTestBase.php | 75 +++---- .../Entity/EntityWithUriCacheTagsTestBase.php | 7 +- .../Functional/WorkspaceCacheContextTest.php | 15 +- .../Core/Render/RendererBubblingTest.php | 165 +++++++------- .../Core/Render/RendererPlaceholdersTest.php | 41 ++-- .../Drupal/Tests/Core/Render/RendererTest.php | 9 +- .../Tests/Core/Render/RendererTestBase.php | 23 +- 18 files changed, 648 insertions(+), 284 deletions(-) create mode 100644 core/lib/Drupal/Core/Cache/VariationCache.php create mode 100644 core/lib/Drupal/Core/Cache/VariationCacheFactory.php create mode 100644 core/lib/Drupal/Core/Cache/VariationCacheFactoryInterface.php create mode 100644 core/lib/Drupal/Core/Cache/VariationCacheInterface.php diff --git a/core/core.services.yml b/core/core.services.yml index a4327bda9b..ee7d6252b0 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -174,6 +174,9 @@ services: cache_contexts_manager: class: Drupal\Core\Cache\Context\CacheContextsManager arguments: ['@service_container', '%cache_contexts%' ] + variation_cache_factory: + class: Drupal\Core\Cache\VariationCacheFactory + arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager'] cache_tags.invalidator: parent: container.trait class: Drupal\Core\Cache\CacheTagsInvalidator @@ -1669,7 +1672,7 @@ services: arguments: ['%renderer.config%'] render_cache: class: Drupal\Core\Render\PlaceholderingRenderCache - arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager', '@render_placeholder_generator'] + arguments: ['@request_stack', '@variation_cache_factory', '@cache_contexts_manager', '@render_placeholder_generator'] renderer: class: Drupal\Core\Render\Renderer arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_placeholder_generator', '@render_cache', '@request_stack', '%renderer.config%'] diff --git a/core/lib/Drupal/Core/Cache/VariationCache.php b/core/lib/Drupal/Core/Cache/VariationCache.php new file mode 100644 index 0000000000..b983f2367f --- /dev/null +++ b/core/lib/Drupal/Core/Cache/VariationCache.php @@ -0,0 +1,210 @@ +requestStack = $request_stack; + $this->cacheBackend = $cache_backend; + $this->cacheContextsManager = $cache_contexts_manager; + } + + /** + * {@inheritdoc} + */ + public function get(array $keys) { + return $this->cacheBackend->get($this->getVariableCacheID($keys)); + } + + /** + * {@inheritdoc} + */ + public function set(array $keys, $data, CacheableDependencyInterface $cacheability) { + $cid = implode(':', $keys); + + // Calculate the potentially redirected cache ID based on cacheability. + $redirected_cacheability = CacheableMetadata::createFromObject($cacheability); + $redirected_cid = $this->createCacheID($keys, $redirected_cacheability); + + // If the cache ID calculation returns the same cache ID as above, we can + // simply store the data at said address as it has no variations. + if ($redirected_cid === $cid) { + $this->cacheBackend->set($cid, $data, $this->maxAgeToExpire($cacheability->getCacheMaxAge()), $cacheability->getCacheTags()); + } + // Otherwise, we need to store the cacheability as a redirect so that future + // attempts to get the data know how to build the proper cache ID. + else { + $this->setRedirectedCache($keys, CacheableMetadata::createFromObject($cacheability)); + $this->cacheBackend->set($redirected_cid, $data, $this->maxAgeToExpire($redirected_cacheability->getCacheMaxAge()), $redirected_cacheability->getCacheTags()); + } + } + + /** + * {@inheritdoc} + */ + public function delete(array $keys) { + return $this->cacheBackend->delete($this->getVariableCacheID($keys)); + } + + /** + * {@inheritdoc} + */ + public function invalidate(array $keys) { + return $this->cacheBackend->invalidate($this->getVariableCacheID($keys)); + } + + /** + * Maps a max-age value to an "expire" value for the Cache API. + * + * @param int $max_age + * A max-age value. + * + * @return int + * A corresponding "expire" value. + * + * @see \Drupal\Core\Cache\CacheBackendInterface::set() + */ + protected function maxAgeToExpire($max_age) { + if ($max_age !== Cache::PERMANENT) { + return (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age; + } + return $max_age; + } + + /** + * Creates a cache ID based on cache keys and cacheable metadata. + * + * @param string[] $keys + * The cache keys of the data to store. + * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata + * The cacheable metadata of the data to store. + * + * @return string + * The cache ID. + */ + protected function createCacheID(array $keys, CacheableMetadata &$cacheable_metadata) { + if ($contexts = $cacheable_metadata->getCacheContexts()) { + $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($contexts); + $keys = array_merge($keys, $context_cache_keys->getKeys()); + $cacheable_metadata = $cacheable_metadata->merge($context_cache_keys); + } + return implode(':', $keys); + } + + /** + * Retrieves the (potentially) variable cache ID based on cache keys. + * + * This will check whether there are any cache redirects and, if so, calculate + * the actual cache ID based on the keys and their associated cache contexts. + * + * @param string[] $keys + * The cache keys of the data to store. + * + * @return string + * The cache ID. + */ + protected function getVariableCacheID(array $keys) { + $cid = implode(':', $keys); + if ($redirect_cache = $this->getRedirectedCache($keys)) { + // Clone the cacheability as it can be affected by cache ID generation. + $cacheable_metadata = CacheableMetadata::createFromObject($redirect_cache->data['cacheable_metadata']); + $cid = $this->createCacheID($redirect_cache->data['keys'], $cacheable_metadata); + } + return $cid; + } + + /** + * Stores a cache redirect entry based on cache keys and cacheable metadata. + * + * A cache redirect is essentially a meta-entry for the actual cache entry + * which stores the cacheable metadata of the cache entry. By doing this, we + * can query the cache redirect entry to know how to build the cache ID for + * retrieving the actual cache entry. + * + * @param string[] $keys + * The cache keys of the data to store the cache redirect for. + * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata + * The cacheable metadata of the data to store the cache redirect for. + */ + protected function setRedirectedCache(array $keys, CacheableMetadata $cacheable_metadata) { + $this->cacheBackend->set($this->redirectCacheId($keys), [ + 'keys' => $keys, + 'cacheable_metadata' => $cacheable_metadata, + ]); + } + + /** + * Gets a cache redirect entry based on cache keys. + * + * @param string[] $keys + * The cache keys of the data to retrieve the cache redirect entry for. + * + * @return object|false + * The cache redirect item or FALSE on failure. + * + * @see ::setRedirectedCache() + */ + protected function getRedirectedCache(array $keys) { + return $this->cacheBackend->get($this->redirectCacheId($keys)); + } + + /** + * Creates the redirect cache ID. + * + * @param string[] $keys + * The cache keys to create the redirect cache ID for. + * + * @return string + * The cache ID. + * + * @see ::setRedirectedCache() + */ + protected function redirectCacheId(array $keys) { + return 'redirect:' . implode(':', $keys); + } + + /** + * {@inheritdoc} + */ + public function sharesCacheKey(array $keys_a, CacheableDependencyInterface $cacheability_a, array $keys_b, CacheableDependencyInterface $cacheability_b) { + $cacheable_metadata_a = CacheableMetadata::createFromObject($cacheability_a); + $cacheable_metadata_b = CacheableMetadata::createFromObject($cacheability_b); + return $this->createCacheID($keys_a, $cacheable_metadata_a) === $this->createCacheID($keys_b, $cacheable_metadata_b); + } + +} diff --git a/core/lib/Drupal/Core/Cache/VariationCacheFactory.php b/core/lib/Drupal/Core/Cache/VariationCacheFactory.php new file mode 100644 index 0000000000..0468ea2702 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/VariationCacheFactory.php @@ -0,0 +1,64 @@ +requestStack = $request_stack; + $this->cacheFactory = $cache_factory; + $this->cacheContextsManager = $cache_contexts_manager; + } + + /** + * {@inheritdoc} + */ + public function get($bin) { + if (!isset($this->bins[$bin])) { + $this->bins[$bin] = new VariationCache($this->requestStack, $this->cacheFactory->get($bin), $this->cacheContextsManager); + } + return $this->bins[$bin]; + } + +} diff --git a/core/lib/Drupal/Core/Cache/VariationCacheFactoryInterface.php b/core/lib/Drupal/Core/Cache/VariationCacheFactoryInterface.php new file mode 100644 index 0000000000..e9c948813d --- /dev/null +++ b/core/lib/Drupal/Core/Cache/VariationCacheFactoryInterface.php @@ -0,0 +1,21 @@ +placeholderGenerator = $placeholder_generator; } diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php index 20e4b0c142..1d0bf78910 100644 --- a/core/lib/Drupal/Core/Render/RenderCache.php +++ b/core/lib/Drupal/Core/Render/RenderCache.php @@ -2,19 +2,15 @@ namespace Drupal\Core\Render; -use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\Context\CacheContextsManager; -use Drupal\Core\Cache\CacheFactoryInterface; +use Drupal\Core\Cache\VariationCacheFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; /** * Wraps the caching logic for the render caching system. * * @internal - * - * @todo Refactor this out into a generic service capable of cache redirects, - * and let RenderCache use that. https://www.drupal.org/node/2551419 */ class RenderCache implements RenderCacheInterface { @@ -26,9 +22,9 @@ class RenderCache implements RenderCacheInterface { protected $requestStack; /** - * The cache factory. + * The variation cache factory. * - * @var \Drupal\Core\Cache\CacheFactoryInterface + * @var \Drupal\Core\Cache\VariationCacheFactoryInterface */ protected $cacheFactory; @@ -44,12 +40,12 @@ class RenderCache implements RenderCacheInterface { * * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. - * @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory - * The cache factory. + * @param \Drupal\Core\Cache\VariationCacheFactoryInterface $cache_factory + * The variation cache factory. * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager * The cache contexts manager. */ - public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) { + public function __construct(RequestStack $request_stack, VariationCacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) { $this->requestStack = $request_stack; $this->cacheFactory = $cache_factory; $this->cacheContextsManager = $cache_contexts_manager; @@ -63,22 +59,24 @@ public function get(array $elements) { // and render caching of forms prevents this from happening. // @todo remove the isMethodCacheable() check when // https://www.drupal.org/node/2367555 lands. - if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { + if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$this->isElementCacheable($elements)) { return FALSE; } - $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; - if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; + if (($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($elements['#cache']['keys'])) { $cached_element = $cache->data; + // Two-tier caching: redirect to actual (post-bubbling) cache item. // @see \Drupal\Core\Render\RendererInterface::render() // @see ::set() if (isset($cached_element['#cache_redirect'])) { - return $this->get($cached_element); + return $this->get($cached_element['#cache']['keys']); } // Return the cached element. return $cached_element; } + return FALSE; } @@ -90,23 +88,28 @@ public function set(array &$elements, array $pre_bubbling_elements) { // and render caching of forms prevents this from happening. // @todo remove the isMethodCacheable() check when // https://www.drupal.org/node/2367555 lands. - if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { + if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$this->isElementCacheable($elements)) { return FALSE; } $data = $this->getCacheableRenderArray($elements); $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; - $cache = $this->cacheFactory->get($bin); + $cache_bin = $this->cacheFactory->get($bin); - // Calculate the pre-bubbling CID. - $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); + // We extract the cache keys before the two-tier caching check so that we + // can manipulate them inside of the two-tier caching logic. This allows us + // to define a new set of cache keys to store the actual post-bubbling + // element at in case we are dealing with a two-tier cache situation. + $element_cache_keys = $elements['#cache']['keys']; - // Two-tier caching: detect different CID post-bubbling, create redirect, - // update redirect if different set of cache contexts. + // Two-tier caching: detect different cache metadata post-bubbling, create + // redirect, update redirect if different set of cache contexts. // @see \Drupal\Core\Render\RendererInterface::render() // @see ::get() - if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { + $cacheability_a = CacheableMetadata::createFromRenderArray($elements); + $cacheability_b = CacheableMetadata::createFromRenderArray($pre_bubbling_elements); + if (!$cache_bin->sharesCacheKey($element_cache_keys, $cacheability_a, $pre_bubbling_elements['#cache']['keys'], $cacheability_b)) { // The cache redirection strategy we're implementing here is pretty // simple in concept. Suppose we have the following render structure: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) @@ -210,7 +213,7 @@ public function set(array &$elements, array $pre_bubbling_elements) { // Get the cacheability of this element according to the current (stored) // redirecting cache item, if any. $redirect_cacheability = new CacheableMetadata(); - if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { + if ($stored_cache_redirect = $cache_bin->get($pre_bubbling_elements['#cache']['keys'])) { $redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data); } @@ -229,12 +232,17 @@ public function set(array &$elements, array $pre_bubbling_elements) { // Stored cache contexts incomplete: this request causes cache contexts to // be added to the redirecting cache item. if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) { + // Manipulate the post-bubbling cache keys so that we store the cache + // redirect at the original cache keys' entry and the actual + // post-bubbling element at the manipulated cache keys' entry. + array_unshift($element_cache_keys, 'render_cache_redirect'); + $redirect_data = [ '#cache_redirect' => TRUE, '#cache' => [ - // The cache keys of the current element; this remains the same - // across requests. - 'keys' => $elements['#cache']['keys'], + // Point to the manipulated cache keys where the post-bubbling + // element should be cached at. + 'keys' => $element_cache_keys, // The union of the current element's and stored cache contexts. 'contexts' => $redirect_cacheability_updated->getCacheContexts(), // The union of the current element's and stored cache tags. @@ -245,7 +253,19 @@ public function set(array &$elements, array $pre_bubbling_elements) { 'bin' => $bin, ], ]; - $cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); + + // We can store this redirect indefinitely as we only keep expanding it + // and never reduce it and thus must ensure it varies by the pre- + // bubbling element's cache contexts so that we can always find it based + // on the original cache contexts. + $cacheable_metadata = new CacheableMetadata(); + $cache_bin->set( + $pre_bubbling_elements['#cache']['keys'], + $redirect_data, + $cacheable_metadata + ->addCacheContexts($pre_bubbling_elements['#cache']['contexts']) + ->addCacheTags(['rendered']) + ); } // Current cache contexts incomplete: this request only uses a subset of @@ -254,65 +274,16 @@ public function set(array &$elements, array $pre_bubbling_elements) { // redirecting cache item would be pointing to a cache item that can never // exist. if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) { - // Recalculate the cache ID. - $recalculated_cid_pseudo_element = [ - '#cache' => [ - 'keys' => $elements['#cache']['keys'], - 'contexts' => $redirect_cacheability_updated->getCacheContexts(), - ], - ]; - $cid = $this->createCacheID($recalculated_cid_pseudo_element); // Ensure the about-to-be-cached data uses the merged cache contexts. $data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts(); } } - $cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered'])); - } - /** - * Maps a #cache[max-age] value to an "expire" value for the Cache API. - * - * @param int $max_age - * A #cache[max-age] value. - * - * @return int - * A corresponding "expire" value. - * - * @see \Drupal\Core\Cache\CacheBackendInterface::set() - */ - protected function maxAgeToExpire($max_age) { - return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age; - } - - /** - * Creates the cache ID for a renderable element. - * - * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. - * - * @param array &$elements - * A renderable array. - * - * @return string - * The cache ID string, or FALSE if the element may not be cached. - */ - protected function createCacheID(array &$elements) { - // If the maximum age is zero, then caching is effectively prohibited. - if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) { - return FALSE; - } - - if (isset($elements['#cache']['keys'])) { - $cid_parts = $elements['#cache']['keys']; - if (!empty($elements['#cache']['contexts'])) { - $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); - $cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys()); - CacheableMetadata::createFromRenderArray($elements) - ->merge($context_cache_keys) - ->applyTo($elements); - } - return implode(':', $cid_parts); - } - return FALSE; + $cache_bin->set( + $element_cache_keys, + $data, + CacheableMetadata::createFromRenderArray($data)->addCacheTags(['rendered']) + ); } /** @@ -355,4 +326,25 @@ public function getCacheableRenderArray(array $elements) { return $data; } + /** + * Checks whether a renderable array can be cached. + * + * This allows us to not even have to instantiate the cache backend if a + * renderable array does not have any cache keys or specifies a zero cache + * max age. + * + * @param array $element + * A renderable array. + * + * @return bool + * Whether the renderable array is cacheable. + */ + protected function isElementCacheable(array $element) { + // If the maximum age is zero, then caching is effectively prohibited. + if (isset($element['#cache']['max-age']) && $element['#cache']['max-age'] === 0) { + return FALSE; + } + return isset($element['#cache']['keys']); + } + } diff --git a/core/modules/aggregator/tests/src/Functional/ItemCacheTagsTest.php b/core/modules/aggregator/tests/src/Functional/ItemCacheTagsTest.php index edd2ac32a9..8345dff2b8 100644 --- a/core/modules/aggregator/tests/src/Functional/ItemCacheTagsTest.php +++ b/core/modules/aggregator/tests/src/Functional/ItemCacheTagsTest.php @@ -63,11 +63,13 @@ protected function createEntity() { * Tests that when creating a feed item, the feed tag is invalidated. */ public function testEntityCreation() { + $cache_bin = $this->getRenderCacheBackend(); + // Create a cache entry that is tagged with a feed cache tag. - \Drupal::cache('render')->set('foo', 'bar', CacheBackendInterface::CACHE_PERMANENT, $this->entity->getCacheTags()); + $cache_bin->set(['foo'], 'bar', $this->entity); // Verify a cache hit. - $this->verifyRenderCache('foo', ['aggregator_feed:1']); + $this->verifyRenderCache(['foo'], ['aggregator_feed:1']); // Now create a feed item in that feed. Item::create([ @@ -77,7 +79,7 @@ public function testEntityCreation() { ])->save(); // Verify a cache miss. - $this->assertFalse(\Drupal::cache('render')->get('foo'), 'Creating a new feed item invalidates the cache tag of the feed.'); + $this->assertFalse($cache_bin->get(['foo']), 'Creating a new feed item invalidates the cache tag of the feed.'); } } diff --git a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php index 71c4f55b8d..ec1555d0d2 100644 --- a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php +++ b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php @@ -146,6 +146,10 @@ public function testBlockViewBuilderCache() { * @see ::testBlockViewBuilderCache() */ protected function verifyRenderCacheHandling() { + /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */ + $variation_cache_factory = $this->container->get('variation_cache_factory'); + $cache_bin = $variation_cache_factory->get('render'); + // Force a request via GET so we can test the render cache. $request = \Drupal::request(); $request_method = $request->server->get('REQUEST_METHOD'); @@ -153,13 +157,13 @@ protected function verifyRenderCacheHandling() { // Test that a cache entry is created. $build = $this->getBlockRenderArray(); - $cid = 'entity_view:block:test_block:' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys()); + $cache_keys = ['entity_view', 'block', 'test_block']; $this->renderer->renderRoot($build); - $this->assertTrue($this->container->get('cache.render')->get($cid), 'The block render element has been cached.'); + $this->assertTrue($cache_bin->get($cache_keys), 'The block render element has been cached.'); // Re-save the block and check that the cache entry has been deleted. $this->block->save(); - $this->assertFalse($this->container->get('cache.render')->get($cid), 'The block render cache entry has been cleared when the block was saved.'); + $this->assertFalse($cache_bin->get($cache_keys), 'The block render cache entry has been cleared when the block was saved.'); // Rebuild the render array (creating a new cache entry in the process) and // delete the block to check the cache entry is deleted. @@ -169,9 +173,9 @@ protected function verifyRenderCacheHandling() { $build['#block'] = $this->block; $this->renderer->renderRoot($build); - $this->assertTrue($this->container->get('cache.render')->get($cid), 'The block render element has been cached.'); + $this->assertTrue($cache_bin->get($cache_keys), 'The block render element has been cached.'); $this->block->delete(); - $this->assertFalse($this->container->get('cache.render')->get($cid), 'The block render cache entry has been cleared when the block was deleted.'); + $this->assertFalse($cache_bin->get($cache_keys), 'The block render cache entry has been cleared when the block was deleted.'); // Restore the previous request method. $request->setMethod($request_method); @@ -287,6 +291,10 @@ public function testBlockViewBuilderBuildAlter() { * The expected max-age. */ protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) { + /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */ + $variation_cache_factory = $this->container->get('variation_cache_factory'); + $cache_bin = $variation_cache_factory->get('render'); + $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; // Check that the expected cacheability metadata is present in: @@ -304,15 +312,14 @@ protected function assertBlockRenderedWithExpectedCacheability(array $expected_k // - the render cache item. $this->pass('Render cache item'); $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts); - $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys()); - $cache_item = $this->container->get('cache.render')->get($cid); - $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.'); + $cache_item = $cache_bin->get($expected_keys); + $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache keys.'); $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']); $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']); $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']); - $this->container->get('cache.render')->delete($cid); + $cache_bin->delete($expected_keys); } /** diff --git a/core/modules/block_content/tests/src/Functional/BlockContentCacheTagsTest.php b/core/modules/block_content/tests/src/Functional/BlockContentCacheTagsTest.php index ea5dd2873d..6d0071e22b 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentCacheTagsTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentCacheTagsTest.php @@ -86,20 +86,16 @@ public function testBlock() { // Expected keys, contexts, and tags for the block. // @see \Drupal\block\BlockViewBuilder::viewMultiple() $expected_block_cache_keys = ['entity_view', 'block', $block->id()]; - $expected_block_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; $expected_block_cache_tags = Cache::mergeTags(['block_view', 'rendered'], $block->getCacheTags()); $expected_block_cache_tags = Cache::mergeTags($expected_block_cache_tags, $block->getPlugin()->getCacheTags()); // Expected contexts and tags for the BlockContent entity. // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults(). - $expected_entity_cache_contexts = ['theme']; $expected_entity_cache_tags = Cache::mergeTags(['block_content_view'], $this->entity->getCacheTags()); $expected_entity_cache_tags = Cache::mergeTags($expected_entity_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity)); // Verify that what was render cached matches the above expectations. - $cid = $this->createCacheId($expected_block_cache_keys, $expected_block_cache_contexts); - $redirected_cid = $this->createCacheId($expected_block_cache_keys, Cache::mergeContexts($expected_block_cache_contexts, $expected_entity_cache_contexts)); - $this->verifyRenderCache($cid, Cache::mergeTags($expected_block_cache_tags, $expected_entity_cache_tags), ($cid !== $redirected_cid) ? $redirected_cid : NULL); + $this->verifyRenderCache($expected_block_cache_keys, Cache::mergeTags($expected_block_cache_tags, $expected_entity_cache_tags)); } } diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php index a2282b1ddf..bc89dfd89b 100644 --- a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php +++ b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\shortcut\Functional; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\shortcut\Entity\Shortcut; use Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase; @@ -54,18 +55,23 @@ protected function createEntity() { * Tests that when creating a shortcut, the shortcut set tag is invalidated. */ public function testEntityCreation() { + $cache_bin = $this->getRenderCacheBackend(); + // Create a cache entry that is tagged with a shortcut set cache tag. $cache_tags = ['config:shortcut.set.default']; - \Drupal::cache('render')->set('foo', 'bar', CacheBackendInterface::CACHE_PERMANENT, $cache_tags); + + $cacheability = new CacheableMetadata(); + $cacheability->addCacheTags($cache_tags); + $cache_bin->set(['foo'], 'bar', $cacheability); // Verify a cache hit. - $this->verifyRenderCache('foo', $cache_tags); + $this->verifyRenderCache(['foo'], $cache_tags); // Now create a shortcut entity in that shortcut set. $this->createEntity(); // Verify a cache miss. - $this->assertFalse(\Drupal::cache('render')->get('foo'), 'Creating a new shortcut invalidates the cache tag of the shortcut set.'); + $this->assertFalse($cache_bin->get(['foo']), 'Creating a new shortcut invalidates the cache tag of the shortcut set.'); } } diff --git a/core/modules/system/tests/src/Functional/Entity/EntityCacheTagsTestBase.php b/core/modules/system/tests/src/Functional/Entity/EntityCacheTagsTestBase.php index 36d8d0670f..7fc4bb5b46 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityCacheTagsTestBase.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityCacheTagsTestBase.php @@ -390,18 +390,17 @@ public function testReferencedEntity() { // Also verify the existence of an entity render cache entry. $cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full']; - $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); $access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity); $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencingEntity); - $redirected_cid = NULL; + $redirected_keys = NULL; if (count($access_cache_contexts) || count($additional_cache_contexts)) { $cache_contexts = Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts); $cache_contexts = Cache::mergeContexts($cache_contexts, $access_cache_contexts); - $redirected_cid = $this->createCacheId($cache_keys, $cache_contexts); + $redirected_keys = array_merge(['render_cache_redirect'], $cache_keys); $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts); $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $context_metadata->getCacheTags()); } - $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid); + $this->verifyRenderCache($cache_keys, $referencing_entity_cache_tags, $redirected_keys); $this->pass("Test non-referencing entity.", 'Debug'); $this->verifyPageCache($non_referencing_entity_url, 'MISS'); @@ -409,8 +408,7 @@ public function testReferencedEntity() { $this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags)); // Also verify the existence of an entity render cache entry. $cache_keys = ['entity_view', 'entity_test', $this->nonReferencingEntity->id(), 'full']; - $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); - $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags); + $this->verifyRenderCache($cache_keys, $non_referencing_entity_cache_tags); $this->pass("Test listing of referencing entities.", 'Debug'); // Prime the page cache for the listing of referencing entities. @@ -641,45 +639,27 @@ public function testReferencedEntity() { $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags); } - /** - * Creates a cache ID from a list of cache keys and a set of cache contexts. - * - * @param string[] $keys - * A list of cache keys. - * @param string[] $contexts - * A set of cache contexts. - * - * @return string - * The cache ID string. - */ - protected function createCacheId(array $keys, array $contexts) { - $cid_parts = $keys; - - $contexts = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($contexts); - $cid_parts = array_merge($cid_parts, $contexts->getKeys()); - - return implode(':', $cid_parts); - } - /** * Verify that a given render cache entry exists, with the correct cache tags. * - * @param string $cid - * The render cache item ID. + * @param string[] $keys + * The render cache item keys. * @param array $tags * An array of expected cache tags. - * @param string|null $redirected_cid - * (optional) The redirected render cache item ID. + * @param string[]|null $redirected_keys + * (optional) The redirected render cache item keys. */ - protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) { + protected function verifyRenderCache(array $keys, array $tags, array $redirected_keys = NULL) { + $cache_bin = $this->getRenderCacheBackend(); + // Also verify the existence of an entity render cache entry. - $cache_entry = \Drupal::cache('render')->get($cid); + $cache_entry = $cache_bin->get($keys); $this->assertTrue($cache_entry, 'A render cache entry exists.'); sort($cache_entry->tags); sort($tags); $this->assertIdentical($cache_entry->tags, $tags); $is_redirecting_cache_item = isset($cache_entry->data['#cache_redirect']); - if ($redirected_cid === NULL) { + if ($redirected_keys === NULL) { $this->assertFalse($is_redirecting_cache_item, 'Render cache entry is not a redirect.'); // If this is a redirecting cache item unlike we expected, log it. if ($is_redirecting_cache_item) { @@ -693,17 +673,26 @@ protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) if (!$is_redirecting_cache_item) { debug($cache_entry->data); } - // Verify that the cache redirect points to the expected CID. - $redirect_cache_metadata = $cache_entry->data['#cache']; - $actual_redirection_cid = $this->createCacheId( - $redirect_cache_metadata['keys'], - $redirect_cache_metadata['contexts'] - ); - $this->assertIdentical($redirected_cid, $actual_redirection_cid); - // Finally, verify that the redirected CID exists and has the same cache - // tags. - $this->verifyRenderCache($redirected_cid, $tags); + // Verify that the cache redirect points to the expected cache keys. + $this->assertIdentical($redirected_keys, $cache_entry->data['#cache']['keys']); + // Finally, verify that the redirected cache object exists and has the + // same cache tags. + $this->verifyRenderCache($redirected_keys, $tags); } } + /** + * Retrieves the render cache backend as a variation cache. + * + * This is how Drupal\Core\Render\RenderCache uses the render cache backend. + * + * @return \Drupal\Core\Cache\VariationCacheInterface + * The render cache backend as a variation cache. + */ + protected function getRenderCacheBackend() { + /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */ + $variation_cache_factory = \Drupal::service('variation_cache_factory'); + return $variation_cache_factory->get('render'); + } + } diff --git a/core/modules/system/tests/src/Functional/Entity/EntityWithUriCacheTagsTestBase.php b/core/modules/system/tests/src/Functional/Entity/EntityWithUriCacheTagsTestBase.php index 27f896b9a6..3851b8089c 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityWithUriCacheTagsTestBase.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityWithUriCacheTagsTestBase.php @@ -44,16 +44,15 @@ public function testEntityUri() { // type supports render caching. if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) { $cache_keys = ['entity_view', $entity_type, $this->entity->id(), $view_mode]; - $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); - $redirected_cid = NULL; + $redirected_keys = NULL; $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->entity); if (count($additional_cache_contexts)) { - $redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts)); + $redirected_keys = array_merge(['render_cache_redirect'], $cache_keys); } $expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag); $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity)); $expected_cache_tags = Cache::mergeTags($expected_cache_tags, [$render_cache_tag]); - $this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid); + $this->verifyRenderCache($cache_keys, $expected_cache_tags, $redirected_keys); } // Verify that after modifying the entity, there is a cache miss. diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceCacheContextTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceCacheContextTest.php index f47ae7f544..0995ad6525 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceCacheContextTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceCacheContextTest.php @@ -30,6 +30,8 @@ public function testWorkspaceCacheContext() { $renderer = \Drupal::service('renderer'); $cache_contexts_manager = \Drupal::service("cache_contexts_manager"); + /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */ + $variation_cache_factory = $this->container->get('variation_cache_factory'); // Check that the 'workspace' cache context is present when the module is // installed. @@ -51,13 +53,12 @@ public function testWorkspaceCacheContext() { $renderer->renderRoot($build); $this->assertTrue(in_array('workspace', $build['#cache']['contexts'], TRUE)); - $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys()); - $this->assertTrue(in_array('[workspace]=live', $cid_parts, TRUE)); + $context_tokens = $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys(); + $this->assertTrue(in_array('[workspace]=live', $context_tokens, TRUE)); // Test that a cache entry is created. - $cid = implode(':', $cid_parts); - $bin = $build['#cache']['bin']; - $this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.'); + $cache_bin = $variation_cache_factory->get($build['#cache']['bin']); + $this->assertTrue($cache_bin->get($build['#cache']['keys']), 'The entity render element has been cached.'); // Switch to the 'stage' workspace and check that the correct workspace // cache context is used. @@ -77,8 +78,8 @@ public function testWorkspaceCacheContext() { $renderer->renderRoot($build); $this->assertTrue(in_array('workspace', $build['#cache']['contexts'], TRUE)); - $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys()); - $this->assertTrue(in_array('[workspace]=stage', $cid_parts, TRUE)); + $context_tokens = $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys(); + $this->assertTrue(in_array('[workspace]=stage', $context_tokens, TRUE)); } } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php index 202c9efa71..f05b626ca7 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php @@ -7,7 +7,9 @@ namespace Drupal\Tests\Core\Render; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\MemoryBackend; +use Drupal\Core\Cache\VariationCache; use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; use Drupal\Core\State\State; use Drupal\Core\Cache\Cache; @@ -79,8 +81,8 @@ public function testContextBubblingCustomCacheBin() { $bin = $this->randomMachineName(); $this->setUpRequest(); - $this->memoryCache = new MemoryBackend(); - $custom_cache = new MemoryBackend(); + $this->memoryCache = new VariationCache($this->requestStack, new MemoryBackend(), $this->cacheContextsManager); + $custom_cache = new VariationCache($this->requestStack, new MemoryBackend(), $this->cacheContextsManager); $this->cacheFactory->expects($this->atLeastOnce()) ->method('get') @@ -113,10 +115,10 @@ public function testContextBubblingCustomCacheBin() { ]; $this->renderer->renderRoot($build); - $this->assertRenderCacheItem('parent:foo', [ + $this->assertRenderCacheItem(['parent'], [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['parent'], + 'keys' => ['render_cache_redirect', 'parent'], 'contexts' => ['bar', 'foo'], 'tags' => [], 'bin' => $bin, @@ -132,7 +134,7 @@ public function testContextBubblingCustomCacheBin() { * * @dataProvider providerTestContextBubblingEdgeCases */ - public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, array $expected_cache_items) { + public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, $expected_cache_item, $expected_cache_redirect) { $this->setUpRequest(); $this->setupMemoryCache(); $this->cacheContextsManager->expects($this->any()) @@ -142,8 +144,9 @@ public function testContextBubblingEdgeCases(array $element, array $expected_top $this->renderer->renderRoot($element); $this->assertEquals($expected_top_level_contexts, $element['#cache']['contexts'], 'Expected cache contexts found.'); - foreach ($expected_cache_items as $cid => $expected_cache_item) { - $this->assertRenderCacheItem($cid, $expected_cache_item); + $this->assertRenderCacheItem($element['#cache']['keys'], $expected_cache_item); + if ($expected_cache_redirect) { + $this->assertRenderCacheItem($expected_cache_item['#cache']['keys'], $expected_cache_redirect); } } @@ -165,18 +168,16 @@ public function providerTestContextBubblingEdgeCases() { ], ], ]; - $expected_cache_items = [ - 'parent' => [ - '#attached' => [], - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => Cache::PERMANENT, - ], - '#markup' => 'parent', + $expected_cache_item = [ + '#attached' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => Cache::PERMANENT, ], + '#markup' => 'parent', ]; - $data[] = [$test_element, [], $expected_cache_items]; + $data[] = [$test_element, [], $expected_cache_item, NULL]; // Assert cache contexts are sorted when they are used to generate a CID. // (Necessary to ensure that different render arrays where the same keys + @@ -188,16 +189,14 @@ public function providerTestContextBubblingEdgeCases() { 'contexts' => [], ], ]; - $expected_cache_items = [ - 'set_test:bar:baz:foo' => [ - '#attached' => [], - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => Cache::PERMANENT, - ], - '#markup' => '', + $expected_cache_item = [ + '#attached' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => Cache::PERMANENT, ], + '#markup' => '', ]; $context_orders = [ ['foo', 'bar', 'baz'], @@ -210,8 +209,8 @@ public function providerTestContextBubblingEdgeCases() { foreach ($context_orders as $context_order) { $test_element['#cache']['contexts'] = $context_order; sort($context_order); - $expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order; - $data[] = [$test_element, $context_order, $expected_cache_items]; + $expected_cache_item['#cache']['contexts'] = $context_order; + $data[] = [$test_element, $context_order, $expected_cache_item, NULL]; } // A parent with a certain set of cache contexts is unaffected by a child @@ -229,18 +228,16 @@ public function providerTestContextBubblingEdgeCases() { ], ], ]; - $expected_cache_items = [ - 'parent:bar:baz:foo' => [ - '#attached' => [], - '#cache' => [ - 'contexts' => ['bar', 'baz', 'foo'], - 'tags' => [], - 'max-age' => 3600, - ], - '#markup' => 'parent', + $expected_cache_item = [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['bar', 'baz', 'foo'], + 'tags' => [], + 'max-age' => 3600, ], + '#markup' => 'parent', ]; - $data[] = [$test_element, ['bar', 'baz', 'foo'], $expected_cache_items]; + $data[] = [$test_element, ['bar', 'baz', 'foo'], $expected_cache_item, NULL]; // A parent with a certain set of cache contexts that is a subset of the // cache contexts of a child gets a redirecting cache item for the cache ID @@ -266,29 +263,27 @@ public function providerTestContextBubblingEdgeCases() { '#markup' => '', ], ]; - $expected_cache_items = [ - 'parent:foo' => [ - '#cache_redirect' => TRUE, - '#cache' => [ - // The keys + contexts this redirects to. - 'keys' => ['parent'], - 'contexts' => ['bar', 'foo'], - 'tags' => ['dee', 'fiddle', 'har', 'yar'], - 'bin' => 'render', - 'max-age' => Cache::PERMANENT, - ], - ], - 'parent:bar:foo' => [ - '#attached' => [], - '#cache' => [ - 'contexts' => ['bar', 'foo'], - 'tags' => ['dee', 'fiddle', 'har', 'yar'], - 'max-age' => Cache::PERMANENT, - ], - '#markup' => 'parent', + $expected_cache_item = [ + '#cache_redirect' => TRUE, + '#cache' => [ + // The keys + contexts this redirects to. + 'keys' => ['render_cache_redirect', 'parent'], + 'contexts' => ['bar', 'foo'], + 'tags' => ['dee', 'fiddle', 'har', 'yar'], + 'bin' => 'render', + 'max-age' => Cache::PERMANENT, + ] + ]; + $expected_cache_redirect = [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['bar', 'foo'], + 'tags' => ['dee', 'fiddle', 'har', 'yar'], + 'max-age' => Cache::PERMANENT, ], + '#markup' => 'parent', ]; - $data[] = [$test_element, ['bar', 'foo'], $expected_cache_items]; + $data[] = [$test_element, ['bar', 'foo'], $expected_cache_item, $expected_cache_redirect]; // Ensure that bubbleable metadata has been collected from children and set // correctly to the main level of the render array. That ensures that correct @@ -313,18 +308,16 @@ public function providerTestContextBubblingEdgeCases() { ], ], ]; - $expected_cache_items = [ - 'parent:foo' => [ - '#attached' => ['library' => ['foo/bar']], - '#cache' => [ - 'contexts' => ['foo'], - 'tags' => ['dee', 'fiddle', 'har', 'yar'], - 'max-age' => Cache::PERMANENT, - ], - '#markup' => 'parent', + $expected_cache_item = [ + '#attached' => ['library' => ['foo/bar']], + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['dee', 'fiddle', 'har', 'yar'], + 'max-age' => Cache::PERMANENT, ], + '#markup' => 'parent', ]; - $data[] = [$test_element, ['foo'], $expected_cache_items]; + $data[] = [$test_element, ['foo'], $expected_cache_item, NULL]; return $data; } @@ -381,17 +374,17 @@ public function testConditionalCacheContextBubblingSelfHealing() { $element = $test_element; $current_user_role = 'A'; $this->renderer->renderRoot($element); - $this->assertRenderCacheItem('parent', [ + $this->assertRenderCacheItem(['parent'], [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['parent'], + 'keys' => ['render_cache_redirect', 'parent'], 'contexts' => ['user.roles'], 'tags' => ['a', 'b'], 'bin' => 'render', 'max-age' => Cache::PERMANENT, ], ]); - $this->assertRenderCacheItem('parent:r.A', [ + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['user.roles'], @@ -406,17 +399,17 @@ public function testConditionalCacheContextBubblingSelfHealing() { $element = $test_element; $current_user_role = 'B'; $this->renderer->renderRoot($element); - $this->assertRenderCacheItem('parent', [ + $this->assertRenderCacheItem(['parent'], [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['parent'], + 'keys' => ['render_cache_redirect', 'parent'], 'contexts' => ['foo', 'user.roles'], 'tags' => ['a', 'b', 'c'], 'bin' => 'render', 'max-age' => 1800, ], ]); - $this->assertRenderCacheItem('parent:foo:r.B', [ + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['foo', 'user.roles'], @@ -439,17 +432,17 @@ public function testConditionalCacheContextBubblingSelfHealing() { $element = $test_element; $current_user_role = 'A'; $this->renderer->renderRoot($element); - $this->assertRenderCacheItem('parent', [ + $this->assertRenderCacheItem(['parent'], [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['parent'], + 'keys' => ['render_cache_redirect', 'parent'], 'contexts' => ['foo', 'user.roles'], 'tags' => ['a', 'b', 'c'], 'bin' => 'render', 'max-age' => 1800, ], ]); - $this->assertRenderCacheItem('parent:foo:r.A', [ + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['foo', 'user.roles'], @@ -471,15 +464,15 @@ public function testConditionalCacheContextBubblingSelfHealing() { $final_parent_cache_item = [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['parent'], + 'keys' => ['render_cache_redirect', 'parent'], 'contexts' => ['bar', 'foo', 'user.roles'], 'tags' => ['a', 'b', 'c', 'd'], 'bin' => 'render', 'max-age' => 300, ], ]; - $this->assertRenderCacheItem('parent', $final_parent_cache_item); - $this->assertRenderCacheItem('parent:bar:foo:r.C', [ + $this->assertRenderCacheItem(['parent'], $final_parent_cache_item); + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['bar', 'foo', 'user.roles'], @@ -493,8 +486,8 @@ public function testConditionalCacheContextBubblingSelfHealing() { $element = $test_element; $current_user_role = 'A'; $this->renderer->renderRoot($element); - $this->assertRenderCacheItem('parent', $final_parent_cache_item); - $this->assertRenderCacheItem('parent:bar:foo:r.A', [ + $this->assertRenderCacheItem(['parent'], $final_parent_cache_item); + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['bar', 'foo', 'user.roles'], @@ -512,8 +505,8 @@ public function testConditionalCacheContextBubblingSelfHealing() { $element = $test_element; $current_user_role = 'B'; $this->renderer->renderRoot($element); - $this->assertRenderCacheItem('parent', $final_parent_cache_item); - $this->assertRenderCacheItem('parent:bar:foo:r.B', [ + $this->assertRenderCacheItem(['parent'], $final_parent_cache_item); + $this->assertRenderCacheItem(['render_cache_redirect', 'parent'], [ '#attached' => [], '#cache' => [ 'contexts' => ['bar', 'foo', 'user.roles'], @@ -558,7 +551,7 @@ public function testBubblingWithPrerender($test_element) { // - … is not cached DOES get called. \Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE); \Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE); - $this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []]]); + $this->memoryCache->set(['cached_nested'], ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []]], new CacheableMetadata()); // Simulate the rendering of an entire response (i.e. a root call). $output = $this->renderer->renderRoot($test_element); diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php index 36137b4a00..84f97013d4 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php @@ -298,15 +298,11 @@ public function providerPlaceholders() { $element_with_cache_keys = $base_element_a3; $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys; $expected_placeholder_render_array['#cache']['keys'] = $keys; - // The CID parts here consist of the cache keys plus the 'user' cache - // context, which in this unit test is simply the given cache context token, - // see \Drupal\Tests\Core\Render\RendererTestBase::setUp(). - $cid_parts = array_merge($keys, ['user']); $cases[] = [ $element_with_cache_keys, $args, $expected_placeholder_render_array, - $cid_parts, + $keys, [], [], [ @@ -546,30 +542,32 @@ protected function generatePlaceholderElement() { } /** - * @param false|array $cid_parts + * @param false|array $cache_keys * @param string[] $bubbled_cache_contexts * Additional cache contexts that were bubbled when the placeholder was * rendered. * @param array $expected_data * A render array with the expected values. */ - protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache_contexts, array $expected_data) { - if ($cid_parts !== FALSE) { + protected function assertPlaceholderRenderCache($cache_keys, array $bubbled_cache_contexts, array $expected_data) { + if ($cache_keys !== FALSE) { if ($bubbled_cache_contexts) { // Verify render cached placeholder. - $cached_element = $this->memoryCache->get(implode(':', $cid_parts))->data; + $cached_element = $this->memoryCache->get($cache_keys)->data; + + // Add the redirected cache's key. + array_unshift($cache_keys, 'render_cache_redirect'); $expected_redirect_element = [ '#cache_redirect' => TRUE, '#cache' => $expected_data['#cache'] + [ - 'keys' => $cid_parts, + 'keys' => $cache_keys, 'bin' => 'render', ], ]; $this->assertEquals($expected_redirect_element, $cached_element, 'The correct cache redirect exists.'); } - // Verify render cached placeholder. - $cached = $this->memoryCache->get(implode(':', array_merge($cid_parts, $bubbled_cache_contexts))); + $cached = $this->memoryCache->get($cache_keys); $cached_element = $cached->data; $this->assertEquals($expected_data, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by the placeholder being replaced.'); } @@ -581,8 +579,8 @@ protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache * * @dataProvider providerPlaceholders */ - public function testUncacheableParent($element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) { - if ($placeholder_cid_parts) { + public function testUncacheableParent($element, $args, array $expected_placeholder_render_array, $placeholder_cache_keys, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) { + if ($placeholder_cache_keys) { $this->setupMemoryCache(); } else { @@ -601,7 +599,7 @@ public function testUncacheableParent($element, $args, array $expected_placehold 'dynamic_animal' => $args[0], ]; $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.'); - $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array); + $this->assertPlaceholderRenderCache($placeholder_cache_keys, $bubbled_cache_contexts, $placeholder_expected_render_cache_array); } /** @@ -609,11 +607,10 @@ public function testUncacheableParent($element, $args, array $expected_placehold * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set - * @covers \Drupal\Core\Render\RenderCache::createCacheID * * @dataProvider providerPlaceholders */ - public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) { + public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cache_keys, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) { $element = $test_element; $this->setupMemoryCache(); @@ -636,10 +633,10 @@ public function testCacheableParent($test_element, $args, array $expected_placeh 'dynamic_animal' => $args[0], ]; $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.'); - $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array); + $this->assertPlaceholderRenderCache($placeholder_cache_keys, $bubbled_cache_contexts, $placeholder_expected_render_cache_array); // GET request: validate cached data. - $cached = $this->memoryCache->get('placeholder_test_GET'); + $cached = $this->memoryCache->get(['placeholder_test_GET']); // There are three edge cases, where the shape of the render cache item for // the parent (with CID 'placeholder_test_GET') is vastly different. These // are the cases where: @@ -663,7 +660,7 @@ public function testCacheableParent($test_element, $args, array $expected_placeh $expected_redirect = [ '#cache_redirect' => TRUE, '#cache' => [ - 'keys' => ['placeholder_test_GET'], + 'keys' => ['render_cache_redirect', 'placeholder_test_GET'], 'contexts' => ['user'], 'tags' => [], 'max-age' => Cache::PERMANENT, @@ -672,7 +669,7 @@ public function testCacheableParent($test_element, $args, array $expected_placeh ]; $this->assertEquals($expected_redirect, $cached_element); // Follow the redirect. - $cached_element = $this->memoryCache->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data; + $cached_element = $this->memoryCache->get(['render_cache_redirect', 'placeholder_test_GET'])->data; $expected_element = [ '#markup' => '

#cache enabled, GET

This is a rendered placeholder!

', '#attached' => [ @@ -1002,7 +999,7 @@ public function testRenderChildrenPlaceholdersDifferentArguments() { $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.'); // GET request: validate cached data. - $cached_element = $this->memoryCache->get('simpletest:renderer:children_placeholders')->data; + $cached_element = $this->memoryCache->get(['simpletest', 'renderer', 'children_placeholders'])->data; $expected_element = [ '#attached' => [ 'drupalSettings' => [ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 54037c57e9..e3fd071357 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -792,7 +792,6 @@ public function testRenderWithThemeArguments() { * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set - * @covers \Drupal\Core\Render\RenderCache::createCacheID */ public function testRenderCache() { $this->setUpRequest(); @@ -836,7 +835,7 @@ public function testRenderCache() { $this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.'); // The cache item also has a 'rendered' cache tag. - $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark'); + $cache_item = $this->cacheFactory->get('render')->get(['render_cache_test']); $this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); } @@ -845,7 +844,6 @@ public function testRenderCache() { * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set - * @covers \Drupal\Core\Render\RenderCache::createCacheID * * @dataProvider providerTestRenderCacheMaxAge */ @@ -862,7 +860,7 @@ public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache ]; $this->renderer->renderRoot($element); - $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark'); + $cache_item = $this->cacheFactory->get('render')->get(['render_cache_test']); if (!$is_render_cached) { $this->assertFalse($cache_item); } @@ -890,7 +888,6 @@ public function providerTestRenderCacheMaxAge() { * @covers ::doRender * @covers \Drupal\Core\Render\RenderCache::get * @covers \Drupal\Core\Render\RenderCache::set - * @covers \Drupal\Core\Render\RenderCache::createCacheID * @covers \Drupal\Core\Render\RenderCache::getCacheableRenderArray * * @dataProvider providerTestRenderCacheProperties @@ -915,7 +912,7 @@ public function testRenderCacheProperties(array $expected_results) { $this->renderer->renderRoot($element); $cache = $this->cacheFactory->get('render'); - $data = $cache->get('render_cache_test:en:stark')->data; + $data = $cache->get(['render_cache_test'])->data; // Check that parent markup is ignored when caching children's markup. $this->assertEquals($data['#markup'] === '', (bool) Element::children($data)); diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index 258fa22fad..d86910a5a0 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\Context\ContextCacheKeys; use Drupal\Core\Cache\MemoryBackend; +use Drupal\Core\Cache\VariationCache; use Drupal\Core\Render\PlaceholderGenerator; use Drupal\Core\Render\PlaceholderingRenderCache; use Drupal\Core\Render\Renderer; @@ -51,14 +52,14 @@ protected $requestStack; /** - * @var \Drupal\Core\Cache\CacheFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Cache\VariationCacheFactoryInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $cacheFactory; /** * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit_Framework_MockObject_MockObject */ - protected $cacheContexts; + protected $cacheContextsManager; /** * The mocked controller resolver. @@ -82,7 +83,7 @@ protected $elementInfo; /** - * @var \Drupal\Core\Cache\CacheBackendInterface + * @var \Drupal\Core\Cache\VariationCacheInterface */ protected $memoryCache; @@ -139,7 +140,7 @@ protected function setUp() { $request = new Request(); $request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']); $this->requestStack->push($request); - $this->cacheFactory = $this->getMock('Drupal\Core\Cache\CacheFactoryInterface'); + $this->cacheFactory = $this->getMock('Drupal\Core\Cache\VariationCacheFactoryInterface'); $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') ->disableOriginalConstructor() ->getMock(); @@ -208,7 +209,7 @@ protected function setUpUnusedCache() { * Sets up a memory-based render cache back-end. */ protected function setupMemoryCache() { - $this->memoryCache = $this->memoryCache ?: new MemoryBackend(); + $this->memoryCache = $this->memoryCache ?: new VariationCache($this->requestStack, new MemoryBackend(), $this->cacheContextsManager); $this->cacheFactory->expects($this->atLeastOnce()) ->method('get') @@ -232,19 +233,19 @@ protected function setUpRequest($method = 'GET') { /** * Asserts a render cache item. * - * @param string $cid - * The expected cache ID. + * @param string[] $keys + * The expected cache keys. * @param mixed $data * The expected data for that cache ID. * @param string $bin * The expected cache bin. */ - protected function assertRenderCacheItem($cid, $data, $bin = 'render') { + protected function assertRenderCacheItem($keys, $data, $bin = 'render') { $cache_backend = $this->cacheFactory->get($bin); - $cached = $cache_backend->get($cid); - $this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', $cid)); + $cached = $cache_backend->get($keys); + $this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', implode(':', $keys))); if ($cached !== FALSE) { - $this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', $cid)); + $this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', implode(':', $keys))); $this->assertSame(Cache::mergeTags($data['#cache']['tags'], ['rendered']), $cached->tags, "The cache item's cache tags also has the 'rendered' cache tag."); } } -- 2.17.1