From 7ec35d9af273c5977d29d341e8f3e070a3328205 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 26 Feb 2024 16:36:03 +0000 Subject: [PATCH] Issue #3304772 by tstoeckler, kksandr, Murz, smustgrave: Cache tags from Computed fields do not bubble up to Entity render array --- core/lib/Drupal/Core/Field/FormatterBase.php | 12 ++- .../EntityTestComputedFieldTest.php | 1 + .../src/Entity/EntityTestComputedField.php | 11 +++ .../ComputedTestCacheableIntegerItemList.php | 36 +++++++++ .../ComputedTestCacheableStringItemList.php | 4 + .../EntityTestComputedFieldNormalizerTest.php | 6 ++ .../Entity/EntityComputedFieldTest.php | 76 +++++++++++++++++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableIntegerItemList.php create mode 100644 core/modules/system/tests/src/Functional/Entity/EntityComputedFieldTest.php diff --git a/core/lib/Drupal/Core/Field/FormatterBase.php b/core/lib/Drupal/Core/Field/FormatterBase.php index a0ed3ea11e71..aa415d39cad1 100644 --- a/core/lib/Drupal/Core/Field/FormatterBase.php +++ b/core/lib/Drupal/Core/Field/FormatterBase.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Field; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -88,8 +90,16 @@ public function view(FieldItemListInterface $items, $langcode = NULL) { } $elements = $this->viewElements($items, $langcode); + // Field item lists, in particular for computed fields, may carry cacheable + // metadata which must be bubbled. + if ($items instanceof CacheableDependencyInterface) { + (new CacheableMetadata()) + ->addCacheableDependency($items) + ->applyTo($elements); + } + // If there are actual renderable children, use #theme => field, otherwise, - // let access cacheability metadata pass through for correct bubbling. + // let cacheability metadata pass through for correct bubbling. if (Element::children($elements)) { $entity = $items->getEntity(); $entity_type = $entity->getEntityTypeId(); diff --git a/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php b/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php index e3f9a9838dd4..33c855c8a97c 100644 --- a/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php +++ b/core/modules/jsonapi/tests/src/Functional/EntityTestComputedFieldTest.php @@ -114,6 +114,7 @@ protected function getExpectedDocument() { 'drupal_internal__id' => 1, 'computed_string_field' => NULL, 'computed_test_cacheable_string_field' => 'computed test cacheable string field', + 'computed_test_cacheable_integer_field' => 0, ], 'relationships' => [ 'computed_reference_field' => [ diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestComputedField.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestComputedField.php index 6cf035700278..9ade6b8344a5 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestComputedField.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestComputedField.php @@ -6,6 +6,7 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\entity_test\Plugin\Field\ComputedReferenceTestFieldItemList; +use Drupal\entity_test\Plugin\Field\ComputedTestCacheableIntegerItemList; use Drupal\entity_test\Plugin\Field\ComputedTestCacheableStringItemList; use Drupal\entity_test\Plugin\Field\ComputedTestFieldItemList; @@ -49,12 +50,22 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setSetting('target_type', 'entity_test') ->setClass(ComputedReferenceTestFieldItemList::class); + // Cacheable metadata can either be provided via the field item properties + // or via the field item list class directly. Add a computed string field + // which does the former and a computed integer field which does the latter. $fields['computed_test_cacheable_string_field'] = BaseFieldDefinition::create('computed_test_cacheable_string_item') ->setLabel(new TranslatableMarkup('Computed Cacheable String Field Test')) ->setComputed(TRUE) ->setClass(ComputedTestCacheableStringItemList::class) ->setReadOnly(FALSE) ->setInternal(FALSE); + $fields['computed_test_cacheable_integer_field'] = BaseFieldDefinition::create('integer') + ->setLabel(new TranslatableMarkup('Computed Cacheable Integer Field Test')) + ->setComputed(TRUE) + ->setClass(ComputedTestCacheableIntegerItemList::class) + ->setReadOnly(FALSE) + ->setInternal(FALSE) + ->setDisplayOptions('view', ['weight' => 10]); return $fields; } diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableIntegerItemList.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableIntegerItemList.php new file mode 100644 index 000000000000..6c9a85b9aed7 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableIntegerItemList.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\entity_test\Plugin\Field; + +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableDependencyTrait; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Field\FieldItemList; +use Drupal\Core\TypedData\ComputedItemListTrait; + +/** + * Item list class for computed cacheable string field. + * + * This class sets the cacheable metadata on the field item list directly. + * + * @see \Drupal\entity_test\Plugin\Field\ComputedTestCacheableStringItemList + */ +class ComputedTestCacheableIntegerItemList extends FieldItemList implements CacheableDependencyInterface { + + use CacheableDependencyTrait, ComputedItemListTrait; + + /** + * {@inheritdoc} + */ + protected function computeValue() { + $value = \Drupal::state()->get('entity_test_computed_integer_value', 0); + $item = $this->createItem(0, $value); + $cacheability = (new CacheableMetadata()) + ->setCacheContexts(['url.query_args:computed_test_cacheable_integer_field']) + ->setCacheTags(['field:computed_test_cacheable_integer_field']) + ->setCacheMaxAge(31536000); + $this->setCacheability($cacheability); + $this->list[0] = $item; + } + +} diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableStringItemList.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableStringItemList.php index 21aaa9f7b0c7..307409babcee 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableStringItemList.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestCacheableStringItemList.php @@ -8,6 +8,10 @@ /** * Item list class for computed cacheable string field. + * + * This class sets the cacheable metadata on the field item properties. + * + * @see \Drupal\entity_test\Plugin\Field\ComputedTestCacheableIntegerItemList */ class ComputedTestCacheableStringItemList extends FieldItemList { diff --git a/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestComputedFieldNormalizerTest.php b/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestComputedFieldNormalizerTest.php index b6c914e32d09..2090db49aa60 100644 --- a/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestComputedFieldNormalizerTest.php +++ b/core/modules/system/tests/modules/entity_test/tests/src/Functional/Rest/EntityTestComputedFieldNormalizerTest.php @@ -56,6 +56,12 @@ protected function getExpectedNormalizedEntity() { 'value' => 'computed test cacheable string field', ], ]; + // @see \Drupal\entity_test\Plugin\Field\ComputedTestCacheableIntegerItemList::computeValue(). + $expected['computed_test_cacheable_integer_field'] = [ + [ + 'value' => 0, + ], + ]; $expected['uuid'] = [ 0 => [ diff --git a/core/modules/system/tests/src/Functional/Entity/EntityComputedFieldTest.php b/core/modules/system/tests/src/Functional/Entity/EntityComputedFieldTest.php new file mode 100644 index 000000000000..56e939986f7f --- /dev/null +++ b/core/modules/system/tests/src/Functional/Entity/EntityComputedFieldTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\Tests\system\Functional\Entity; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\State\StateInterface; +use Drupal\entity_test\Entity\EntityTestComputedField; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests that entities with computed fields work correctly. + * + * @group Entity + */ +class EntityComputedFieldTest extends BrowserTestBase { + + /** + * Modules to enable. + * + * @var array + */ + protected static $modules = ['entity_test']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'olivero'; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected StateInterface $state; + + protected function setUp(): void { + parent::setUp(); + + $this->state = $this->container->get('state'); + } + + /** + * Tests that formatters bubble the cacheable metadata of computed fields. + */ + public function testFormatterComputedFieldCacheableMetadata() { + $this->drupalLogin($this->drupalCreateUser(['administer entity_test content'])); + + $entity = EntityTestComputedField::create([ + 'name' => 'Test entity with a cacheable, computed field', + ]); + $entity->save(); + + $this->state->set('entity_test_computed_integer_value', 2024); + $this->drupalGet($entity->toUrl('canonical')->toString()); + $field_item_selector = '.field--name-computed-test-cacheable-integer-field .field__item'; + $this->assertSession()->elementTextEquals('css', $field_item_selector, 2024); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'url.query_args:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'field:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', "31536000"); + + $this->state->set('entity_test_computed_integer_value', 2025); + $this->drupalGet($entity->toUrl('canonical')->toString()); + $this->assertSession()->elementTextEquals('css', $field_item_selector, 2024); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'url.query_args:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'field:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', "31536000"); + + Cache::invalidateTags(['field:computed_test_cacheable_integer_field']); + $this->drupalGet($entity->toUrl('canonical')->toString()); + $this->assertSession()->elementTextEquals('css', $field_item_selector, 2025); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'url.query_args:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'field:computed_test_cacheable_integer_field'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', "31536000"); + } + +} -- GitLab