.../EntityReferenceLabelFormatter.php | 14 ++--- core/modules/node/src/Tests/NodeCacheTagsTest.php | 15 +++++ .../modules/system/src/Tests/Common/RenderTest.php | 40 ++++++++++--- .../src/Tests/Entity/EntityCacheTagsTestBase.php | 67 ++++++++++++++++++++-- .../Entity/EntityWithUriCacheTagsTestBase.php | 6 +- .../Field/FieldFormatter/TaxonomyFormatterBase.php | 3 +- 6 files changed, 121 insertions(+), 24 deletions(-) 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 0651bbc..485a088 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceLabelFormatter.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TypedData\TranslatableInterface; /** * Plugin implementation of the 'entity reference label' formatter. @@ -66,13 +67,6 @@ public function viewElements(FieldItemListInterface $items) { $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 @@ -109,6 +103,12 @@ public function viewElements(FieldItemListInterface $items) { else { $elements[$delta] = array('#markup' => String::checkPlain($label)); } + + $cache_contexts = []; + if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { + $cache_contexts[] = 'language'; + } + $elements[$delta]['#cache']['contexts'] = $cache_contexts; $elements[$delta]['#cache']['tags'] = $entity->getCacheTags(); } 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/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php index b137d19..f8140da 100644 --- a/core/modules/system/src/Tests/Common/RenderTest.php +++ b/core/modules/system/src/Tests/Common/RenderTest.php @@ -530,7 +530,10 @@ function testDrupalRenderPostRenderCache() { '#markup' => '

#cache enabled, GET

', '#attached' => $test_element['#attached'], '#post_render_cache' => $test_element['#post_render_cache'], - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -647,7 +650,10 @@ function testDrupalRenderChildrenPostRenderCache() { $context_3, ) ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $dom = Html::load($cached_element['#markup']); @@ -714,7 +720,10 @@ function testDrupalRenderChildrenPostRenderCache() { $context_3, ) ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $dom = Html::load($cached_parent_element['#markup']); @@ -740,7 +749,10 @@ function testDrupalRenderChildrenPostRenderCache() { $context_3, ) ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $dom = Html::load($cached_child_element['#markup']); @@ -849,7 +861,10 @@ function testDrupalRenderRenderCachePlaceholder() { $context ), ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -941,7 +956,10 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $context, ), ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $this->assertIdentical($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.'); @@ -966,7 +984,10 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $context, ), ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $this->assertIdentical($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.'); @@ -994,7 +1015,10 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $context, ), ), - '#cache' => array('tags' => array('rendered')), + '#cache' => [ + 'contexts' => [], + 'tags' => ['rendered'], + ], ); $this->assertIdentical($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/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/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php index 43273b1..1a830da 100644 --- a/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php +++ b/core/modules/taxonomy/src/Plugin/Field/FieldFormatter/TaxonomyFormatterBase.php @@ -48,8 +48,8 @@ public function prepareView(array $entities_items) { $term = $translated_term; } } - $cache_contexts_per_term[$term->id()] = $cache_contexts; if (!$term->isNew()) { + $cache_contexts_per_term[$term->id()] = $cache_contexts; $terms[$term->id()] = $term; } } @@ -70,7 +70,6 @@ public function prepareView(array $entities_items) { // 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 {