core/lib/Drupal/Core/Cache/Cache.php | 20 ++ core/lib/Drupal/Core/Entity/EntityViewBuilder.php | 5 +- .../Field/FieldFormatter/LanguageFormatter.php | 3 + .../Field/FieldFormatter/TimestampFormatter.php | 9 +- core/lib/Drupal/Core/Render/BubbleableMetadata.php | 15 +- core/lib/Drupal/Core/Render/Renderer.php | 87 +++++- core/lib/Drupal/Core/Render/RendererInterface.php | 39 ++- .../FieldFormatter/CommentDefaultFormatter.php | 2 + .../FieldFormatter/DateTimeDefaultFormatter.php | 5 + .../FieldFormatter/DateTimePlainFormatter.php | 9 +- core/modules/filter/src/FilterProcessResult.php | 44 +++ core/modules/filter/src/Tests/FilterAPITest.php | 9 + .../src/Plugin/Filter/FilterTestCacheContexts.php | 35 +++ core/modules/node/src/Tests/NodeCacheTagsTest.php | 15 + .../src/Tests/Entity/EntityCacheTagsTestBase.php | 67 ++++- .../Entity/EntityWithUriCacheTagsTestBase.php | 6 +- .../Field/FieldFormatter/TextDefaultFormatter.php | 2 + .../Field/FieldFormatter/TextTrimmedFormatter.php | 2 + .../Tests/Core/Render/BubbleableMetadataTest.php | 14 +- .../Tests/Core/Render/RendererBubblingTest.php | 328 ++++++++++++++++++++- .../Core/Render/RendererPostRenderCacheTest.php | 40 ++- .../Drupal/Tests/Core/Render/RendererTest.php | 5 +- .../Drupal/Tests/Core/Render/RendererTestBase.php | 16 + 23 files changed, 739 insertions(+), 38 deletions(-) diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php index 4e0124f..91cc4a8 100644 --- a/core/lib/Drupal/Core/Cache/Cache.php +++ b/core/lib/Drupal/Core/Cache/Cache.php @@ -22,6 +22,26 @@ class Cache { const PERMANENT = CacheBackendInterface::CACHE_PERMANENT; /** + * Merges arrays of cache contexts and removes duplicates. + * + * @param string[] … + * Arrays of cache contexts to merge. + * + * @return string[] + * The merged array of cache contexts. + */ + public static function mergeContexts() { + $cache_context_arrays = func_get_args(); + $cache_contexts = []; + foreach ($cache_context_arrays as $contexts) { + $cache_contexts = array_merge($cache_contexts, $contexts); + } + $cache_contexts = array_unique($cache_contexts); + sort($cache_contexts); + return $cache_contexts; + } + + /** * Merges arrays of cache tags and removes duplicates. * * The cache tags array is returned in a format that is valid for diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 2f506c2..2c21668 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -185,14 +185,11 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco 'contexts' => array( 'theme', 'user.roles', - // @todo Move this out of here and into field formatters that depend - // on the timezone. Blocked on https://drupal.org/node/2099137. - 'timezone', ), 'bin' => $this->cacheBin, ); - if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { + if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) { $build['#cache']['keys'][] = $langcode; } } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/LanguageFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/LanguageFormatter.php index d936b89..ba730a6 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/LanguageFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/LanguageFormatter.php @@ -30,6 +30,9 @@ class LanguageFormatter extends FormatterBase { public function viewElements(FieldItemListInterface $items) { $elements = array(); + // The 'language' cache context is not necessary, because + // \Drupal\Core\Language\LanguageInterface::getName() always returns the + // human-readable English name. foreach ($items as $delta => $item) { $elements[$delta] = array('#markup' => $item->language ? String::checkPlain($item->language->getName()) : ''); } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php index 6861a00..6de42ee 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php @@ -31,7 +31,14 @@ public function viewElements(FieldItemListInterface $items) { $elements = array(); foreach ($items as $delta => $item) { - $elements[$delta] = array('#markup' => format_date($item->value)); + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#markup' => format_date($item->value) + ]; } return $elements; diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php index 2f62b44..7c94f43 100644 --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -25,6 +25,13 @@ class BubbleableMetadata { protected $tags; /** + * Cache contexts. + * + * @var string[] + */ + protected $contexts; + + /** * Attached assets. * * @var string[][] @@ -43,13 +50,16 @@ class BubbleableMetadata { * * @param string[] $tags * An array of cache tags. + * @param string[] $contexts + * An array of cache contexts. * @param array $attached * An array of attached assets. * @param array $post_render_cache * An array of #post_render_cache metadata. */ - public function __construct(array $tags = [], array $attached = [], array $post_render_cache = []) { + public function __construct(array $tags = [], array $contexts = [], array $attached = [], array $post_render_cache = []) { $this->tags = $tags; + $this->contexts = $contexts; $this->attached = $attached; $this->postRenderCache = $post_render_cache; } @@ -70,6 +80,7 @@ public function __construct(array $tags = [], array $attached = [], array $post_ public function merge(BubbleableMetadata $other) { $result = new BubbleableMetadata(); $result->tags = Cache::mergeTags($this->tags, $other->tags); + $result->contexts = array_unique(array_merge($this->contexts, $other->contexts)); $result->attached = Renderer::mergeAttachments($this->attached, $other->attached); $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache); return $result; @@ -83,6 +94,7 @@ public function merge(BubbleableMetadata $other) { */ public function applyTo(array &$build) { $build['#cache']['tags'] = $this->tags; + $build['#cache']['contexts'] = $this->contexts; $build['#attached'] = $this->attached; $build['#post_render_cache'] = $this->postRenderCache; } @@ -98,6 +110,7 @@ public function applyTo(array &$build) { public static function createFromRenderArray(array $build) { $meta = new static(); $meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : []; + $meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : []; $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : []; return $meta; diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index b2f6072..31d1ebd 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -166,6 +166,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. + $pre_bubbling_cid = NULL; if (isset($elements['#cache'])) { $cached_element = $this->cacheGet($elements); if ($cached_element !== FALSE) { @@ -185,6 +186,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $this->bubbleStack(); return $elements['#markup']; } + else { + // Two-tier caching: set pre-bubbling cache ID, if this element is + // cacheable.. + // @see ::cacheGet() + // @see ::cacheSet() + if ($this->requestStack->getCurrentRequest()->isMethodSafe() && $cid = $this->createCacheID($elements)) { + $pre_bubbling_cid = $cid; + } + } } // If the default values for this element have not been loaded yet, populate @@ -206,6 +216,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // Defaults for bubbleable rendering metadata. + $elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : array(); $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); @@ -347,7 +358,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Cache the processed element if #cache is set, and the metadata necessary // to generate a cache ID is present. if (isset($elements['#cache']) && (isset($elements['#cache']['keys']) || isset($elements['#cache']['cid']))) { - $this->cacheSet($elements); + $this->cacheSet($elements, $pre_bubbling_cid); } // Only when we're in a root (non-recursive) drupal_render() call, @@ -495,6 +506,12 @@ protected function cacheGet(array $elements) { if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { $cached_element = $cache->data; + // Two-tier caching: redirect to actual (post-bubbling) cache item. + // @see ::doRender() + // @see ::cacheSet() + if (isset($cached_element['#cache_redirect'])) { + return $this->cacheGet($cached_element); + } // Return the cached element. return $cached_element; } @@ -508,13 +525,15 @@ protected function cacheGet(array $elements) { * * @param array $elements * A renderable array. + * @param string|null $pre_bubbling_cid + * The pre-bubbling cache ID. * * @return bool|null * Returns FALSE if no cache item could be created, NULL otherwise. * * @see ::getFromCache() */ - protected function cacheSet(array &$elements) { + protected function cacheSet(array &$elements, $pre_bubbling_cid) { // Form submissions rely on the form being built during the POST request, // and render caching of forms prevents this from happening. // @todo remove the isMethodSafe() check when @@ -532,7 +551,67 @@ protected function cacheSet(array &$elements) { $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : Cache::PERMANENT; - $this->cacheFactory->get($bin)->set($cid, $data, $expire, $data['#cache']['tags']); + $cache = $this->cacheFactory->get($bin); + + // Two-tier caching: detect different CID post-bubbling, create redirect, + // update redirect if different set of cache contexts. + // @see ::doRender() + // @see ::cacheGet() + if (isset($pre_bubbling_cid) && $pre_bubbling_cid !== $cid) { + // The set of cache contexts for this element, including the bubbled ones, + // for which we are handling a cache miss. + $cache_contexts = $data['#cache']['contexts']; + + // Get the contexts by which this element should be varied according to + // the current redirecting cache item, if any. + $stored_cache_contexts = []; + $stored_cache_tags = []; + if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { + $stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts']; + $stored_cache_tags = $stored_cache_redirect->data['#cache']['tags']; + } + + // Calculate the union of the cache contexts for this request and the + // stored cache contexts. + $merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts); + + // Stored cache contexts incomplete: this request causes cache contexts to + // be added to the redirecting cache item. + if (count(array_diff($merged_cache_contexts, $stored_cache_contexts)) > 0) { + $redirect_data = [ + '#cache_redirect' => TRUE, + '#cache' => [ + // The cache keys of the current element; this remains the same + // across requests. + 'keys' => $elements['#cache']['keys'], + // The union of the current element's and stored cache contexts. + 'contexts' => $merged_cache_contexts, + // The union of the current element's and stored cache tags. + 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']), + ], + ]; + $cache->set($pre_bubbling_cid, $redirect_data, $expire, $redirect_data['#cache']['tags']); + } + + // Current cache contexts incomplete: this request only uses a subset of + // the cache contexts stored in the redirecting cache item. Vary by these + // additional (conditional) cache contexts as well, otherwise the + // redirecting cache item would be pointing to a cache item that can never + // exist. + if (count(array_diff($merged_cache_contexts, $cache_contexts)) > 0) { + // Recalculate the cache ID. + $recalculated_cid_pseudo_element = [ + '#cache' => [ + 'keys' => $elements['#cache']['keys'], + 'contexts' => $merged_cache_contexts, + ] + ]; + $cid = $this->createCacheID($recalculated_cid_pseudo_element); + // Ensure the about-to-be-cached data uses the merged cache contexts. + $data['#cache']['contexts'] = $merged_cache_contexts; + } + } + $cache->set($cid, $data, $expire, $data['#cache']['tags']); } /** @@ -555,6 +634,7 @@ protected function createCacheID(array $elements) { elseif (isset($elements['#cache']['keys'])) { $cid_parts = $elements['#cache']['keys']; if (isset($elements['#cache']['contexts'])) { + sort($elements['#cache']['contexts']); $contexts = $this->cacheContexts->convertTokensToKeys($elements['#cache']['contexts']); $cid_parts = array_merge($cid_parts, $contexts); } @@ -572,6 +652,7 @@ public function getCacheableRenderArray(array $elements) { '#attached' => $elements['#attached'], '#post_render_cache' => $elements['#post_render_cache'], '#cache' => [ + 'contexts' => $elements['#cache']['contexts'], 'tags' => $elements['#cache']['tags'], ], ]; diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 5207215..c4fbe12 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -73,12 +73,19 @@ public function renderPlain(&$elements); * the parent array. * * An important aspect of rendering is the bubbling of rendering metadata: - * cache tags, attached assets and #post_render_cache metadata all need to be - * bubbled up. That information is needed once the rendering to a HTML string - * is completed: the resulting HTML for the page must know by which cache tags - * it should be invalidated, which (CSS and JavaScript) assets must be loaded, - * and which #post_render_cache callbacks should be executed. A stack data - * structure is used to perform this bubbling. + * cache contexts, cache tags, attached assets and #post_render_cache metadata + * all need to be bubbled up. That information is needed once the rendering to + * a HTML string is completed: we must know the bubbled cache contexts to be + * able to calculate the correct cache ID for render caching, the the + * resulting HTML for the page must know by which cache tags it should be + * invalidated, which (CSS and JavaScript) assets must be loaded, and which + * #post_render_cache callbacks should be executed. A stack data structure is + * used to perform this bubbling. + * + * Another important aspect is render caching. A two-tier caching approach + * (i.e. with one cache item redirecting to another one) to make render + * caching compatible with the bubbling of cache contexts, because cache + * contexts affect the cache ID. * * The process of rendering an element is recursive unless the element defines * an implemented theme hook in #theme. During each call to @@ -111,6 +118,20 @@ public function renderPlain(&$elements); * metadata from the element retrieved from render cache. Then, this stack * frame is bubbled: the two topmost frames are popped from the stack, * they are merged, and the result is pushed back onto the stack. + * However, also in case of a cache miss we have to do something. Note + * that a Renderer renders top-down, which means that we try to render a + * parent first, and we try to avoid the work of rendering the children by + * using the render cache. Though in this case, we are dealing with a + * cache miss. So a Renderer traverses down the tree, rendering all + * children. In doing so, the render stack is updated with the bubbleable + * metadata of the children. That means that once the children are + * rendered, we can render cache this element. But the cache ID may have + * *changed* at that point, because the children's cache contexts have + * been bubbled! + * It is for that case that we must store the current (pre-bubbling) cache + * ID, so that we can compare it with the new (post-bubbling) cache ID + * when writing to the cache. We store the current cache ID in + * #cid_pre_bubbling. * - If this element has #type defined and the default attributes for this * element have not already been merged in (#defaults_loaded = TRUE) then * the defaults for this type of element, defined in hook_element_info(), @@ -210,6 +231,12 @@ public function renderPlain(&$elements); * - If this element has #cache defined, the rendered output of this element * is saved to Renderer::render()'s internal cache. This includes the * changes made by #post_render. + * At the same time, if #cid_pre_bubbling is set, it is compared to the + * calculated cache ID. If they are different, then a redirecting cache + * item is created, containing the #cache metadata of the current element, + * and written to cache using the value of #cid_pre_bubbling as the cache + * ID. This ensures the pre-bubbling ("wrong") cache ID redirects to the + * post-bubbling ("right") cache ID. * - If this element has an array of #post_render_cache functions defined, * or any of its children has (which we would know thanks to the stack * having been updated just before the render caching step), they are diff --git a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php index 02c3107..be747b4 100644 --- a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php +++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php @@ -157,6 +157,7 @@ public function viewElements(FieldItemListInterface $items) { // Unpublished comments are not included in // $entity->get($field_name)->comment_count, but unpublished comments // should display if the user is an administrator. + $elements['#cache']['contexts'][] = 'user.roles'; if ($this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) { // This is a listing of Comment entities, so associate its list cache // tag for correct invalidation. @@ -182,6 +183,7 @@ public function viewElements(FieldItemListInterface $items) { // display below the entity. Do not show the form for the print view mode. if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == CommentItemInterface::FORM_BELOW && $this->viewMode != 'print') { // Only show the add comment form if the user has permission. + $elements['#cache']['contexts'][] = 'user.roles'; if ($this->currentUser->hasPermission('post comments')) { // All users in the "anonymous" role can use the same form: it is fine // for this form to be stored in the render cache. diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php index f983c8d..a7cb32a 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php @@ -127,6 +127,11 @@ public function viewElements(FieldItemListInterface $items) { // Display the date using theme datetime. $elements[$delta] = array( + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], '#theme' => 'time', '#text' => $formatted_date, '#html' => FALSE, diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php index 24fc69c..3f68a7f 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php @@ -46,7 +46,14 @@ public function viewElements(FieldItemListInterface $items) { } $output = $date->format($format); } - $elements[$delta] = array('#markup' => $output); + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#markup' => $output, + ]; } return $elements; diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index f620868..cbed27e 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -86,6 +86,13 @@ class FilterProcessResult { protected $cacheTags; /** + * The associated cache contexts. + * + * @var array + */ + protected $cacheContexts; + + /** * The associated #post_render_cache callbacks. * * @see _drupal_render_process_post_render_cache() @@ -105,6 +112,7 @@ public function __construct($processed_text) { $this->assets = array(); $this->cacheTags = array(); + $this->cacheContexts = array(); $this->postRenderCacheCallbacks = array(); } @@ -175,6 +183,41 @@ public function setCacheTags(array $cache_tags) { } /** + * Gets cache contexts associated with the processed text. + * + * @return array + */ + public function getCacheContexts() { + return $this->cacheContexts; + } + + /** + * Adds cache contexts associated with the processed text. + * + * @param array $cache_contexts + * The cache contexts to be added. + * + * @return $this + */ + public function addCacheContexts(array $cache_contexts) { + $this->cacheContexts = array_unique(array_merge($this->cacheTags, $cache_contexts)); + return $this; + } + + /** + * Sets cache contexts associated with the processed text. + * + * @param array $cache_contexts + * The cache contexts to be associated. + * + * @return $this + */ + public function setCacheContexts(array $cache_contexts) { + $this->cacheContexts= $cache_contexts; + return $this; + } + + /** * Gets assets associated with the processed text. * * @return array @@ -257,6 +300,7 @@ public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) public function getBubbleableMetadata() { return new BubbleableMetadata( $this->getCacheTags(), + $this->getCacheContexts(), $this->getAssets(), $this->getPostRenderCacheCallbacks() ); diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 50210b5..e139da9 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -216,6 +216,10 @@ function testProcessedTextElement() { 'weight' => 0, 'status' => TRUE, ), + 'filter_test_cache_contexts' => array( + 'weight' => 0, + 'status' => TRUE, + ), 'filter_test_post_render_cache' => array( 'weight' => 1, 'status' => TRUE, @@ -253,6 +257,11 @@ function testProcessedTextElement() { 'foo:baz', ); $this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.'); + $expected_cache_contexts = [ + // The cache context set by the filter_test_cache_contexts filter. + 'language', + ]; + $this->assertEqual($expected_cache_contexts, $build['#cache']['contexts']);//, 'Expected cache contexts present.'); $expected_markup = '

Hello, world!

This is a dynamic llama.

'; $this->assertEqual($expected_markup, $build['#markup'], 'Expected #post_render_cache callback has been applied.'); } diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheContexts.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheContexts.php new file mode 100644 index 0000000..6a98956 --- /dev/null +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheContexts.php @@ -0,0 +1,35 @@ +addCacheContexts(['language']); + return $result; + } + +} diff --git a/core/modules/node/src/Tests/NodeCacheTagsTest.php b/core/modules/node/src/Tests/NodeCacheTagsTest.php index cd46f79..333a111 100644 --- a/core/modules/node/src/Tests/NodeCacheTagsTest.php +++ b/core/modules/node/src/Tests/NodeCacheTagsTest.php @@ -43,6 +43,21 @@ protected function createEntity() { } /** + * Returns the additional (non-standard) cache contexts for the tested entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be tested, as created by createEntity(). + * + * @return string[] + * An array of the additional cache contexts. + * + * @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity() + */ + protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) { + return ['timezone']; + } + + /** * {@inheritdoc} * * Each node must have an author. diff --git a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php index 3d7c93f..3bfe637 100644 --- a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php @@ -128,6 +128,21 @@ protected static function generateStandardizedInfo($entity_type_label, $group) { abstract protected function createEntity(); /** + * Returns the additional (non-standard) cache contexts for the tested entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be tested, as created by createEntity(). + * + * @return string[] + * An array of the additional cache contexts. + * + * @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity() + */ + protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) { + return []; + } + + /** * Returns the additional (non-standard) cache tags for the tested entity. * * @param \Drupal\Core\Entity\EntityInterface $entity @@ -337,15 +352,16 @@ public function testReferencedEntity() { // Verify a cache hit, but also the presence of the correct cache tags. $this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags)); // Also verify the existence of an entity render cache entry. - $cid = 'entity_view:entity_test:' . $this->referencing_entity->id() . ':full:classy:r.anonymous:' . date_default_timezone_get(); - $this->verifyRenderCache($cid, $referencing_entity_cache_tags); + $cid = 'entity_view:entity_test:' . $this->referencing_entity->id() . ':full:classy:r.anonymous'; + $redirected_cid = $this->buildRedirectedCid($cid); + $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid); $this->pass("Test non-referencing entity.", 'Debug'); $this->verifyPageCache($non_referencing_entity_url, 'MISS'); // Verify a cache hit, but also the presence of the correct cache tags. $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. - $cid = 'entity_view:entity_test:' . $this->non_referencing_entity->id() . ':full:classy:r.anonymous:' . date_default_timezone_get(); + $cid = 'entity_view:entity_test:' . $this->non_referencing_entity->id() . ':full:classy:r.anonymous'; $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags); @@ -579,20 +595,63 @@ public function testReferencedEntity() { } /** + * Build the redirected CID, if any. + * + * If a subclass overrides ::getAdditionalCacheContextsForEntity(), it can + * specify the additional cache contexts by which the given entity must be + * varied, because those are the cache contexts that are bubbled from the + * field formatters. + * + * @param string $cid + * The regular (pre-bubbling) CID. + * + * @return null|string + * The redirected (post-bubbling) CID, if any. + */ + protected function buildRedirectedCid($cid) { + $redirected_cid = NULL; + $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencing_entity); + if (count($additional_cache_contexts)) { + $redirected_cid = $cid . ':' . implode(':', \Drupal::service('cache_contexts')->convertTokensToKeys($additional_cache_contexts)); + } + return $redirected_cid; + } + + /** * Verify that a given render cache entry exists, with the correct cache tags. * * @param string $cid * The render cache item ID. * @param array $tags * An array of expected cache tags. + * @param string|null $redirected_cid + * (optional) The redirected render cache item ID. */ - protected function verifyRenderCache($cid, array $tags) { + protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) { // Also verify the existence of an entity render cache entry. $cache_entry = \Drupal::cache('render')->get($cid); $this->assertTrue($cache_entry, 'A render cache entry exists.'); sort($cache_entry->tags); sort($tags); $this->assertIdentical($cache_entry->tags, $tags); + if ($redirected_cid === NULL) { + $this->assertTrue(!isset($cache_entry->data['#cache_redirect']), 'Render cache entry is not a redirect.'); + } + else { + // Verify that $cid contains a cache redirect. + $this->assertTrue(isset($cache_entry->data['#cache_redirect']), 'Render cache entry is a redirect.'); + // Verify that the cache redirect points to the expected CID. + $redirect_cache_metadata = $cache_entry->data['#cache']; + $cache_keys = array_merge( + $redirect_cache_metadata['keys'], + \Drupal::service('cache_contexts')->convertTokensToKeys($redirect_cache_metadata['contexts']) + ); + $actual_redirection_cid = implode(':', $cache_keys); + $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); + } } } diff --git a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php index 0c8cd1d..ae0873c 100644 --- a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php @@ -45,10 +45,10 @@ public function testEntityUri() { // Also verify the existence of an entity render cache entry, if this entity // type supports render caching. if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) { - $cid = 'entity_view:' . $entity_type . ':' . $this->entity->id() . ':' . $view_mode . ':classy:r.anonymous:' . date_default_timezone_get(); - $cache_entry = \Drupal::cache('render')->get($cid); + $cid = 'entity_view:' . $entity_type . ':' . $this->entity->id() . ':' . $view_mode . ':classy:r.anonymous'; + $redirected_cid = $this->buildRedirectedCid($cid); $expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag, $this->getAdditionalCacheTagsForEntity($this->entity), array($render_cache_tag)); - $this->verifyRenderCache($cid, $expected_cache_tags); + $this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid); } // Verify that after modifying the entity, there is a cache miss. diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php index efc4c0d..623c808 100644 --- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php +++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php @@ -34,6 +34,8 @@ class TextDefaultFormatter extends FormatterBase { public function viewElements(FieldItemListInterface $items) { $elements = array(); + // The ProcessedText element already handles cache context & tag bubbling. + // @see \Drupal\filter\Element\ProcessedText::preRenderText() foreach ($items as $delta => $item) { $elements[$delta] = array( '#type' => 'processed_text', diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php index ed1c8c3..105bfd5 100644 --- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php +++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php @@ -81,6 +81,8 @@ public function viewElements(FieldItemListInterface $items) { $element['#text_summary_trim_length'] = $this->getSetting('trim_length'); }; + // The ProcessedText element already handles cache context & tag bubbling. + // @see \Drupal\filter\Element\ProcessedText::preRenderText() foreach ($items as $delta => $item) { $elements[$delta] = array( '#type' => 'processed_text', diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index c3fd7ba..88999e9 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -35,12 +35,13 @@ public function providerTestApplyTo() { $data = []; $empty_metadata = new BubbleableMetadata(); - $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]); + $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['qux'], ['settings' => ['foo' => 'bar']]); $empty_render_array = []; $nonempty_render_array = [ '#cache' => [ 'tags' => ['llamas:are:awesome:but:kittens:too'], + 'contexts' => ['qux'], ], '#attached' => [ 'library' => [ @@ -53,7 +54,8 @@ public function providerTestApplyTo() { $expected_when_empty_metadata = [ '#cache' => [ - 'tags' => [] + 'tags' => [], + 'contexts' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -61,7 +63,10 @@ public function providerTestApplyTo() { $data[] = [$empty_metadata, $empty_render_array, $expected_when_empty_metadata]; $data[] = [$empty_metadata, $nonempty_render_array, $expected_when_empty_metadata]; $expected_when_nonempty_metadata = [ - '#cache' => ['tags' => ['foo:bar']], + '#cache' => [ + 'tags' => ['foo:bar'], + 'contexts' => ['qux'], + ], '#attached' => [ 'settings' => [ 'foo' => 'bar', @@ -92,12 +97,13 @@ public function providerTestCreateFromRenderArray() { $data = []; $empty_metadata = new BubbleableMetadata(); - $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]); + $nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['qux'], ['settings' => ['foo' => 'bar']]); $empty_render_array = []; $nonempty_render_array = [ '#cache' => [ 'tags' => ['foo:bar'], + 'contexts' => ['qux'], ], '#attached' => [ 'settings' => [ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php index 34c988c..3b9f82d 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php @@ -34,6 +34,9 @@ public function testBubblingWithoutPreRender() { $this->elementInfo->expects($this->any()) ->method('getInfo') ->willReturn([]); + $this->cacheContexts->expects($this->any()) + ->method('convertTokensToKeys') + ->willReturnArgument(0); // Create an element with a child and subchild. Each element loads a // different library using #attached. @@ -68,6 +71,320 @@ public function testBubblingWithoutPreRender() { } /** + * Tests cache context bubbling in edge cases, because it affects the CID. + * + * ::testBubblingWithPrerender() already tests the common case. + * + * @dataProvider providerTestContextBubblingEdgeCases + */ + public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, array $expected_cache_items) { + $this->setUpRequest(); + $this->setupMemoryCache(); + $this->cacheContexts->expects($this->any()) + ->method('convertTokensToKeys') + ->willReturnArgument(0); + + $this->renderer->render($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); + } + } + + public function providerTestContextBubblingEdgeCases() { + $data = []; + + // Bubbled cache contexts cannot override a cache ID set by #cache['cid']. + // But the cache context is bubbled nevertheless. + $test_element = [ + '#cache' => [ + 'cid' => 'parent', + ], + '#markup' => 'parent', + 'child' => [ + '#cache' => [ + 'contexts' => ['foo'], + ], + ], + ]; + $expected_cache_items = [ + 'parent' => [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ], + ]; + $data[] = [$test_element, ['foo'], $expected_cache_items]; + + // Cache contexts of inaccessible children aren't bubbled (because those + // children are not rendered at all). + $test_element = [ + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => [], + ], + '#markup' => 'parent', + 'child' => [ + '#access' => FALSE, + '#cache' => [ + 'contexts' => ['foo'], + ], + ], + ]; + $expected_cache_items = [ + 'parent' => [ + '#attached' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ], + ]; + $data[] = [$test_element, [], $expected_cache_items]; + + // Assert cache contexts are sorted when they are used to generate a CID. + // (Necessary to ensure that different render arrays where the same keys + + // set of contexts are present point to the same cache item. Regardless of + // the contexts' order. A sad necessity because PHP doesn't have sets.) + $test_element = [ + '#cache' => [ + 'keys' => ['set_test'], + 'contexts' => [], + ], + ]; + $expected_cache_items = [ + 'set_test:bar:baz:foo' => [ + '#attached' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], + '#post_render_cache' => [], + '#markup' => '', + ], + ]; + $context_orders = [ + ['foo', 'bar', 'baz'], + ['foo', 'baz', 'bar'], + ['bar', 'foo', 'baz'], + ['bar', 'baz', 'foo'], + ['baz', 'foo', 'bar'], + ['baz', 'bar', 'foo'], + ]; + foreach ($context_orders as $context_order) { + $test_element['#cache']['contexts'] = $context_order; + $expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order; + $data[] = [$test_element, $context_order, $expected_cache_items]; + } + + // A parent with a certain set of cache contexts is unaffected by a child + // that has a subset of those contexts. + $test_element = [ + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => ['foo', 'bar', 'baz'], + ], + '#markup' => 'parent', + 'child' => [ + '#cache' => [ + 'contexts' => ['foo', 'baz'], + ], + ], + ]; + $expected_cache_items = [ + 'parent:bar:baz:foo' => [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['foo', 'bar', 'baz'], + 'tags' => ['rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ], + ]; + $data[] = [$test_element, ['foo', 'bar', 'baz'], $expected_cache_items]; + + // 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 + // created pre-bubbling (without the child's additional cache contexts). It + // points to a cache item with a post-bubbling cache ID (i.e. with the + // child's additional cache contexts). + // Furthermore, the redirecting cache item also includes the children's + // cache tags, since changes in the children may cause those children to get + // different cache contexts and therefore cause different cache contexts to + // be stored in the redirecting cache item. + $test_element = [ + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => ['foo'], + 'tags' => ['yar', 'har'], + ], + '#markup' => 'parent', + 'child' => [ + '#cache' => [ + 'contexts' => ['bar'], + 'tags' => ['fiddle', 'dee'], + ], + '#markup' => '', + ], + ]; + $expected_cache_items = [ + 'parent:foo' => [ + '#cache_redirect' => TRUE, + '#cache' => [ + // The keys + contexts this redirects to. + 'keys' => ['parent'], + 'contexts' => ['bar', 'foo'], + // The 'rendered' cache tag is also present for the redirecting cache + // item, to ensure it is considered to be part of the render cache + // and thus invalidated along with everything else. + 'tags' => ['dee', 'fiddle', 'har', 'rendered', 'yar'], + ], + ], + 'parent:bar:foo' => [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['foo', 'bar'], + 'tags' => ['dee', 'fiddle', 'har', 'yar', 'rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ], + ]; + $data[] = [$test_element, ['foo', 'bar'], $expected_cache_items]; + + return $data; + } + + /** + * Tests the self-healing of the redirect with conditional cache contexts. + */ + public function testConditionalCacheContextBubblingSelfHealing() { + global $foo_cache_context; + $this->setUpRequest(); + $this->setupMemoryCache(); + $this->cacheContexts->expects($this->any()) + ->method('convertTokensToKeys') + ->willReturnCallback(function($context_tokens) use ($foo_cache_context) { + global $foo_cache_context; + $keys = []; + foreach ($context_tokens as $context_id) { + if ($context_id === 'foo') { + $keys[] = $context_id . '.' . ($foo_cache_context ? '1' : '0'); + } + else { + $keys[] = $context_id; + } + } + return $keys; + }); + + $test_element = [ + '#cache' => [ + 'keys' => ['parent'], + 'tags' => ['a'], + ], + '#markup' => 'parent', + 'child' => [ + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['b'], + ], + 'grandchild' => [ + '#cache' => [ + 'contexts' => ['bar'], + 'tags' => ['c'], + ], + ], + ], + ]; + + // Request 1: grandchild is inaccessible => bubbled cache contexts: foo. + $element = $test_element; + $foo_cache_context = FALSE; + $element['child']['grandchild']['#access'] = FALSE; + $this->renderer->render($element); + $this->assertRenderCacheItem('parent', [ + '#cache_redirect' => TRUE, + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => ['foo'], + 'tags' => ['a', 'b', 'rendered'], + ], + ]); + $this->assertRenderCacheItem('parent:foo.0', [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['a', 'b', 'rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ]); + + // Request 2: grandchild is accessible => bubbled cache contexts: foo, bar. + $element = $test_element; + $foo_cache_context = TRUE; + $this->renderer->render($element); + $this->assertRenderCacheItem('parent', [ + '#cache_redirect' => TRUE, + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => ['bar', 'foo'], + 'tags' => ['a', 'b', 'c', 'rendered'], + ], + ]); + $this->assertRenderCacheItem('parent:bar:foo.1', [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['foo', 'bar'], + 'tags' => ['a', 'b', 'c', 'rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ]); + + // Request 3: grandchild is inaccessible again => bubbled cache contexts: + // foo; but that's a subset of the already-bubbled cache contexts, so + // nothing is actually changed in the redirecting cache item. However, the + // cache item we were looking for in request 1 is technically the same one + // we're looking for now (it's the exact same request), but with one + // additional cache context. This is necessary to avoid "cache ping-pong". + // (Requests 1 and 3 are identical, but without the right merging logic to + // handle request 2, the redirecting cache item would toggle between only + // the 'foo' cache context and only the 'bar' cache context, resulting in + // cache miss every time.) + $element = $test_element; + $foo_cache_context = FALSE; + $element['child']['grandchild']['#access'] = FALSE; + $this->renderer->render($element); + $this->assertRenderCacheItem('parent', [ + '#cache_redirect' => TRUE, + '#cache' => [ + 'keys' => ['parent'], + 'contexts' => ['bar', 'foo'], + 'tags' => ['a', 'b', 'c', 'rendered'], + ], + ]); + $this->assertRenderCacheItem('parent:bar:foo.0', [ + '#attached' => [], + '#cache' => [ + 'contexts' => ['bar', 'foo'], + 'tags' => ['a', 'b', 'rendered'], + ], + '#post_render_cache' => [], + '#markup' => 'parent', + ]); + } + + /** * Tests bubbling of bubbleable metadata added by #pre_render callbacks. * * @dataProvider providerTestBubblingWithPrerender @@ -94,13 +411,14 @@ 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' => ['tags' => []], '#post_render_cache' => []]); + $this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []], '#post_render_cache' => []]); // Simulate the rendering of an entire response (i.e. a root call). $output = $this->renderer->renderRoot($test_element); // First, assert the render array is of the expected form. - $this->assertEquals('Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.'); + $this->assertEquals('Cache context!Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.'); + $this->assertEquals(['child.cache_context'], $test_element['#cache']['contexts'], 'Expected cache contexts found.'); $this->assertEquals(['child:cache_tag'], $test_element['#cache']['tags'], 'Expected cache tags found.'); $expected_attached = [ 'drupalSettings' => ['foo' => 'bar'], @@ -155,6 +473,12 @@ public static function bubblingPreRender($elements) { ]; $placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context); $elements += [ + 'child_cache_context' => [ + '#cache' => [ + 'contexts' => ['child.cache_context'], + ], + '#markup' => 'Cache context!', + ], 'child_cache_tag' => [ '#cache' => [ 'tags' => ['child:cache_tag'], diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php index cb7ccc3..46a49be 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php @@ -89,7 +89,10 @@ public function testPostRenderCacheWithColdCache() { '#markup' => '

#cache enabled, GET

', '#attached' => $test_element['#attached'], '#post_render_cache' => $test_element['#post_render_cache'], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -222,7 +225,10 @@ public function testRenderChildrenPostRenderCacheDifferentContexts() { $context_3, ] ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $dom = Html::load($cached_element['#markup']); @@ -314,7 +320,10 @@ public function testRenderChildrenPostRenderCacheComplex() { $context_3, ] ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $dom = Html::load($cached_parent_element['#markup']); @@ -337,7 +346,10 @@ public function testRenderChildrenPostRenderCacheComplex() { $context_3, ] ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $dom = Html::load($cached_child_element['#markup']); @@ -448,7 +460,10 @@ public function testPlaceholder() { $context ], ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -543,7 +558,10 @@ public function testChildElementPlaceholder() { $context, ], ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -568,7 +586,10 @@ public function testChildElementPlaceholder() { $context, ], ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -596,7 +617,10 @@ public function testChildElementPlaceholder() { $context, ], ], - '#cache' => ['tags' => ['rendered']], + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 2f259b6..455834e 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -17,7 +17,10 @@ class RendererTest extends RendererTestBase { protected $defaultThemeVars = [ - '#cache' => ['tags' => []], + '#cache' => [ + 'contexts' => [], + 'tags' => [], + ], '#attached' => [], '#post_render_cache' => [], '#children' => '', diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index f4a051a..cf08554 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -139,6 +139,22 @@ protected function setUpRequest($method = 'GET') { $this->requestStack->push($request); } + /** + * Asserts a render cache item. + * + * @param string $cid + * The expected cache ID. + * @param mixed $data + * The expected data for that cache ID. + */ + protected function assertRenderCacheItem($cid, $data) { + $cached = $this->memoryCache->get($cid); + $this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', $cid)); + if ($cached !== FALSE) { + $this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', $cid)); + } + } + }