diff --git a/core/lib/Drupal/Core/Field/FormatterBase.php b/core/lib/Drupal/Core/Field/FormatterBase.php index a0ed3ea11e71212a43bc5db010bb48dd949bbced..aa415d39cad14517f6c99103fc5c00de83f7e849 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 e3f9a9838dd46f288cafbd045fe5882f82132aca..33c855c8a97ce21519889db0331d9ffcda03c962 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 6cf035700278023e5c6cffe144da93b1fc1a394c..9ade6b8344a5b5d9eafb0c0cdeaaa321f9dfa91f 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 0000000000000000000000000000000000000000..6c9a85b9aed741b34ee481fb9bfe51d606edf8fb --- /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 21aaa9f7b0c7a990d384d78b3c11152f46f69deb..307409babcee5d89bdf0b4ece28fc9c52073bcc0 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 b6c914e32d091d240dc58ff43f4b8b4b093f750c..2090db49aa60b40b0ac424c4363b5e24cf98ab62 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 0000000000000000000000000000000000000000..56e939986f7f33b26bc5916d1b388466267dbc3c --- /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"); + } + +}