diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index b5a1e4643468bf372e61cfd9809e6564385c4474..99c88116aabc533d3eaddc64f62b93d576bf3c0d 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -6,6 +6,7 @@ use Drupal\Core\Block\TitleBlockPluginInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheOptionalInterface; use Drupal\Core\Entity\EntityViewBuilder; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -50,7 +51,6 @@ public function viewMultiple(array $entities = [], $view_mode = 'full', $langcod // @see template_preprocess_block(). $build[$entity_id] = [ '#cache' => [ - 'keys' => ['entity_view', 'block', $entity->id()], 'contexts' => Cache::mergeContexts( $entity->getCacheContexts(), $plugin->getCacheContexts() @@ -61,6 +61,21 @@ public function viewMultiple(array $entities = [], $view_mode = 'full', $langcod '#weight' => $entity->getWeight(), ]; + // For block plugins implementing CacheOptionalInterface, the expectation + // is that the cost of rendering them is less than retrieving them from + // cache. Only add cache keys to the block render array if the block + // plugin does not implement CacheOptionalInterface. + // If any CacheOptionalInterface block is set to render as a placeholder + // (createPlaceholder() returns TRUE), the cached response in the internal + // page cache or external caches and proxies will include the block + // markup, but the block is not cached anywhere else. If a + // CacheOptionalInterface block is not set to render as a placeholder, + // then its rendered markup is cached within the rendered page in the + // dynamic page cache. + if (!$plugin instanceof CacheOptionalInterface) { + $build[$entity_id]['#cache']['keys'] = ['entity_view', 'block', $entity->id()]; + } + // Allow altering of cacheability metadata or setting #create_placeholder. $this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin); diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheOptionalBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheOptionalBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..e39a9d7a870c5de677174f837ab78b30e938458e --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestCacheOptionalBlock.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\block_test\Plugin\Block; + +use Drupal\Core\Block\Attribute\Block; +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Cache\CacheOptionalInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Provides a block to test caching. + */ +#[Block( + id: "test_cache_optional", + admin_label: new TranslatableMarkup("Test block cache optional") +)] +class TestCacheOptionalBlock extends BlockBase implements CacheOptionalInterface { + + /** + * {@inheritdoc} + */ + public function build() { + $content = \Drupal::keyValue('block_test')->get('content'); + + $build = []; + if (!empty($content)) { + $build['#markup'] = $content; + } + return $build; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return \Drupal::state()->get('block_test.cache_contexts', []); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return \Drupal::state()->get('block_test.cache_max_age', parent::getCacheMaxAge()); + } + +} diff --git a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php index a6b66adee2b69e4f52084fe6bfb7f25f2ae5bf31..c384fc40735feda5999739f239aec5fcf69de6b5 100644 --- a/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php +++ b/core/modules/block/tests/src/Kernel/BlockViewBuilderTest.php @@ -228,6 +228,43 @@ protected function verifyRenderCacheHandling(): void { $request->setMethod($request_method); } + /** + * Tests block render cache handling of cache-optional blocks. + */ + public function testBlockViewBuilderCacheOptional(): void { + // Verify cache handling for a non-empty block. + $this->verifyRenderCacheHandling(); + + // Create a block for a plugin implementing CacheOptionalInterface. + $this->block = $this->controller->create([ + 'id' => 'test_block', + 'theme' => 'stark', + 'plugin' => 'test_cache_optional', + ]); + $this->block->save(); + \Drupal::keyValue('block_test')->set('content', 'This is content for a block that is not render cached.'); + + /** @var \Drupal\Core\Cache\VariationCacheFactoryInterface $variation_cache_factory */ + $variation_cache_factory = \Drupal::service('variation_cache_factory'); + $cache_bin = $variation_cache_factory->get('render'); + + // Force a request via GET so we can test the render cache. + $request = \Drupal::request(); + $request_method = $request->server->get('REQUEST_METHOD'); + $request->setMethod('GET'); + + // Test that a cache entry is not created. + $build = $this->getBlockRenderArray(); + $cache_keys = ['entity_view', 'block', 'test_block']; + $markup = $this->renderer->renderRoot($build); + $this->assertTrue(str_contains((string) $markup, 'This is content for a block that is not render cached.')); + $this->assertFalse($cache_bin->get($cache_keys, CacheableMetadata::createFromRenderArray($build)), 'The block render element has not been cached.'); + // Confirm that build render array has no cache keys. + $this->assertArrayNotHasKey('keys', $build['#cache']); + // Restore the previous request method. + $request->setMethod($request_method); + } + /** * Tests block view altering. * diff --git a/core/modules/language/src/Plugin/Block/LanguageBlock.php b/core/modules/language/src/Plugin/Block/LanguageBlock.php index 8b146db4adbd31b2f96328a9148e3be1b5b611c2..10a9e7fde2ecb55e61678f0c2710626b477c5e94 100644 --- a/core/modules/language/src/Plugin/Block/LanguageBlock.php +++ b/core/modules/language/src/Plugin/Block/LanguageBlock.php @@ -5,7 +5,9 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Cache\CacheOptionalInterface; use Drupal\Core\Path\PathMatcherInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -23,7 +25,7 @@ category: new TranslatableMarkup("System"), deriver: LanguageBlockDeriver::class )] -class LanguageBlock extends BlockBase implements ContainerFactoryPluginInterface { +class LanguageBlock extends BlockBase implements ContainerFactoryPluginInterface, CacheOptionalInterface { /** * The language manager. @@ -101,6 +103,11 @@ public function build() { } $links = $this->languageManager->getLanguageSwitchLinks($type, $url); + // In any render cache items wrapping this block, account for variations + // by user access to each switcher link, the current path and query + // arguments, and language negotiation. + $cache_metadata = BubbleableMetadata::createFromRenderArray($build) + ->addCacheContexts(['url.path', 'url.query_args', 'url.site', 'languages:' . $type]); if (isset($links->links)) { $build = [ '#theme' => 'links__language_block', @@ -112,17 +119,23 @@ public function build() { ], '#set_active_class' => TRUE, ]; + + foreach ($links->links as $link) { + if ($link['url'] instanceof Url) { + $cache_metadata->addCacheableDependency($link['url']->access(NULL, TRUE)); + } + } } + $cache_metadata->applyTo($build); + return $build; } /** * {@inheritdoc} - * - * @todo Make cacheable in https://www.drupal.org/node/2232375. */ - public function getCacheMaxAge() { - return 0; + public function createPlaceholder(): bool { + return TRUE; } }