core/lib/Drupal/Core/Cache/Cache.php | 20 ++++++++++ core/lib/Drupal/Core/Entity/EntityViewBuilder.php | 3 -- .../EntityReferenceLabelFormatter.php | 12 +++++- .../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 | 32 +++++++++++++++- 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 +++++++++++++++++ .../Plugin/Field/FieldFormatter/LinkFormatter.php | 6 +++ .../Plugin/Field/FieldFormatter/PlainFormatter.php | 3 ++ .../Field/FieldFormatter/TaxonomyFormatterBase.php | 18 ++++++++- .../Field/FieldFormatter/TextDefaultFormatter.php | 2 + .../Field/FieldFormatter/TextTrimmedFormatter.php | 2 + .../Tests/Core/Render/BubbleableMetadataTest.php | 14 +++++-- 20 files changed, 262 insertions(+), 20 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..af5abe0 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -185,9 +185,6 @@ 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, ); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php index dbc8958..0651bbc 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php @@ -64,7 +64,16 @@ public function viewElements(FieldItemListInterface $items) { $elements = array(); $output_as_link = $this->getSetting('link'); - foreach ($this->getEntitiesToView($items) as $delta => $entity) { + $entities = $this->getEntitiesToView($items); + + // Cache contexts: if one entity is translatable, then they all are. + $cache_contexts = []; + $first_entity = reset($entities); + if (count($first_entity->getTranslationLanguages()) > 1) { + $cache_contexts[] = 'language'; + } + + foreach ($entities as $delta => $entity) { $label = $entity->label(); // If the link is to be displayed and the entity has a uri, display a // link. @@ -100,6 +109,7 @@ public function viewElements(FieldItemListInterface $items) { else { $elements[$delta] = array('#markup' => String::checkPlain($label)); } + $elements[$delta]['#cache']['contexts'] = $cache_contexts; $elements[$delta]['#cache']['tags'] = $entity->getCacheTags(); } 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 c56b421..c01edf4 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 = drupal_merge_attached($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 e0253b6..148e0f8 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -184,6 +184,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)) { + $elements['#cid_pre_bubbling'] = $cid; + } + } } // If the default values for this element have not been loaded yet, populate @@ -205,6 +214,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(); @@ -493,6 +503,12 @@ protected function cacheGet(array $elements) { if (!empty($cid) && $cache = $this->cacheFactory->get($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; } @@ -530,7 +546,20 @@ 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']); + $tags = $data['#cache']['tags']; + $cache = $this->cacheFactory->get($bin); + + // Two-tier caching: detect different CID post-bubbling, create redirect. + // @see ::doRender() + // @see ::cacheGet() + if (isset($elements['#cid_pre_bubbling']) && $elements['#cid_pre_bubbling'] !== $cid) { + $redirect_data = [ + '#cache_redirect' => TRUE, + '#cache' => $elements['#cache'], + ]; + $cache->set($elements['#cid_pre_bubbling'], $redirect_data, $expire, $tags); + } + $cache->set($cid, $data, $expire, $tags); } /** @@ -570,6 +599,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 06f4968..0010b86 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..251dce0 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. + $output['comments']['#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. + $output['comments']['#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/taxonomy/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/LinkFormatter.php index d94e2bf..847e1fd 100644 --- a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/LinkFormatter.php +++ b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/LinkFormatter.php @@ -34,6 +34,9 @@ public function viewElements(FieldItemListInterface $items) { foreach ($items as $delta => $item) { if ($item->hasNewEntity()) { $elements[$delta] = array( + '#cache' => [ + 'contexts' => $item->_cacheContexts, + ], '#markup' => String::checkPlain($item->entity->label()), ); } @@ -41,6 +44,9 @@ public function viewElements(FieldItemListInterface $items) { /** @var $term \Drupal\taxonomy\TermInterface */ $term = $item->entity; $elements[$delta] = array( + '#cache' => [ + 'contexts' => $item->_cacheContexts, + ], '#type' => 'link', '#title' => $term->getName(), '#url' => $term->urlInfo(), diff --git a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/PlainFormatter.php b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/PlainFormatter.php index 1bf4ab4..0161791 100644 --- a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/PlainFormatter.php +++ b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/PlainFormatter.php @@ -31,6 +31,9 @@ public function viewElements(FieldItemListInterface $items) { foreach ($items as $delta => $item) { $elements[$delta] = array( + '#cache' => [ + 'contexts' => $item->_cacheContexts, + ], '#markup' => String::checkPlain($item->entity->label()), ); } diff --git a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php index 6109674..43273b1 100644 --- a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php +++ b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php @@ -7,6 +7,8 @@ namespace Drupal\taxonomy\Plugin\Field\FieldFormatter; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableInterface; use Drupal\Core\Field\FormatterBase; /** @@ -21,7 +23,8 @@ * unsets values for invalid terms that do not exist. */ public function prepareView(array $entities_items) { - $terms = array(); + $terms = []; + $cache_contexts_per_term = []; // Collect every possible term attached to any of the fieldable entities. /* @var \Drupal\Core\Field\EntityReferenceFieldItemList $items */ @@ -31,12 +34,21 @@ public function prepareView(array $entities_items) { $active_langcode = $parent->language()->getId(); /* @var \Drupal\taxonomy\Entity\Term $term */ foreach ($items->referencedEntities() as $term) { + $cache_contexts = []; + if (count($term->getTranslationLanguages()) > 1) { + $cache_contexts[] = 'language'; + } if ($term->hasTranslation($active_langcode)) { $translated_term = $term->getTranslation($active_langcode); - if ($translated_term->access('view')) { + $access_result = $translated_term->access('view', NULL, TRUE); + if ($access_result instanceof CacheableInterface) { + $cache_contexts = Cache::mergeContexts($cache_contexts, $access_result->getCacheContexts()); + } + if ($access_result->isAllowed()) { $term = $translated_term; } } + $cache_contexts_per_term[$term->id()] = $cache_contexts; if (!$term->isNew()) { $terms[$term->id()] = $term; } @@ -53,10 +65,12 @@ public function prepareView(array $entities_items) { if (isset($terms[$item->target_id])) { // Replace the instance value with the term data. $item->entity = $terms[$item->target_id]; + $item->_cacheContexts = $cache_contexts_per_term[$item->target_id]; } // Terms to be created are not in $terms, but are still legitimate. elseif ($item->hasNewEntity()) { // Leave the item in place. + $item->_cacheContexts = $cache_contexts_per_term[$item->target_id]; } // Otherwise, unset the instance value, since the term does not exist. else { 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..23168d3 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'], [], ['settings' => ['foo' => 'bar']]); $empty_render_array = []; $nonempty_render_array = [ '#cache' => [ 'tags' => ['llamas:are:awesome:but:kittens:too'], + 'contexts' => [], ], '#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' => [], + ], '#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'], [], ['settings' => ['foo' => 'bar']]); $empty_render_array = []; $nonempty_render_array = [ '#cache' => [ 'tags' => ['foo:bar'], + 'contexts' => [], ], '#attached' => [ 'settings' => [