core/modules/block/block.api.php | 55 +++++++++++ core/modules/block/src/BlockViewBuilder.php | 11 ++- .../block/src/Tests/BlockViewBuilderTest.php | 109 ++++++++++++++++++++- .../tests/modules/block_test/block_test.module | 24 +++++ 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/core/modules/block/block.api.php b/core/modules/block/block.api.php index 468f893..a60f35e 100644 --- a/core/modules/block/block.api.php +++ b/core/modules/block/block.api.php @@ -127,6 +127,61 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B } /** + * Alter the result of \Drupal\Core\Block\BlockBase::build(). + * + * Unlike hook_block_view_alter(), this hook is called very early, before the + * block is being assembled. Therefore, it is early enough to alter the + * cacheability metadata (change #cache), or to explicitly placeholder the block + * (set #create_placeholder). + * + * In addition to hook_block_build_alter(), which is called for all blocks, + * there is hook_block_build_BASE_BLOCK_ID_alter(), which can be used to target + * a specific block or set of similar blocks. + * + * @param array &$build + * A renderable array of data, only containing #cache. + * @param \Drupal\Core\Block\BlockPluginInterface $block + * The block plugin instance. + * + * @see hook_block_build_BASE_BLOCK_ID_alter() + * @see entity_crud + * + * @ingroup block_api + */ +function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) { + // Add the 'user' cache context to some blocks. + if ($some_condition) { + $build['#contexts'][] = 'user'; + } +} + +/** + * Provide a block plugin specific block_build alteration. + * + * In this hook name, BASE_BLOCK_ID refers to the block implementation's plugin + * id, regardless of whether the plugin supports derivatives. For example, for + * the \Drupal\system\Plugin\Block\SystemPoweredByBlock block, this would be + * 'system_powered_by_block' as per that class's annotation. And for the + * \Drupal\system\Plugin\Block\SystemMenuBlock block, it would be + * 'system_menu_block' as per that class's annotation, regardless of which menu + * the derived block is for. + * + * @param array $build + * A renderable array of data, only containing #cache. + * @param \Drupal\Core\Block\BlockPluginInterface $block + * The block plugin instance. + * + * @see hook_block_build_alter() + * @see entity_crud + * + * @ingroup block_api + */ +function hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) { + // Explicitly enable placeholdering of the specific block. + $build['#create_placeholder'] = TRUE; +} + +/** * Control access to a block instance. * * Modules may implement this hook if they want to have a say in whether or not diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index 0f0cc3c..daad7a5 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -40,6 +40,7 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { + $module_handler = \Drupal::service('module_handler'); /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); foreach ($entities as $entity) { @@ -63,6 +64,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la ], ); + // Allow altering of cacheability metadata or setting #create_placeholder. + $module_handler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin); + if ($plugin instanceof StatefulBlockPluginInterface) { // Immediately build a #pre_render-able block, since this block cannot // be built lazily. @@ -124,10 +128,9 @@ protected static function buildPreRenderableBlock($entity, ModuleHandlerInterfac $build['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']); - // Don't run in ::preRender() to ensure cache keys can be altered. If an - // alter hook wants to modify the block contents, it can append another - // #pre_render hook. - $module_handler->alter(array('block_view', "block_view_$base_id"), $build, $plugin); + // If an alter hook wants to modify the block contents, it can append + // another #pre_render hook. + $module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin); return $build; } diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php index 1175480..e4a64ff 100644 --- a/core/modules/block/src/Tests/BlockViewBuilderTest.php +++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php @@ -186,8 +186,11 @@ protected function verifyRenderCacheHandling() { /** * Tests block view altering. + * + * @see hook_block_view_alter() + * @see hook_block_view_BASE_BLOCK_ID_alter() */ - public function testBlockViewBuilderAlter() { + public function testBlockViewBuilderViewAlter() { // Establish baseline. $build = $this->getBlockRenderArray(); $this->setRawContent((string)$this->renderer->renderRoot($build)); @@ -214,6 +217,110 @@ public function testBlockViewBuilderAlter() { } /** + * Tests block build altering. + * + * @see hook_block_build_alter() + * @see hook_block_build_BASE_BLOCK_ID_alter() + */ + public function testBlockViewBuilderBuildAlter() { + // 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'); + + $default_keys = ['entity_view', 'block', 'test_block']; + $default_contexts = []; + $default_tags = ['block_view', 'config:block.block.test_block']; + $default_max_age = Cache::PERMANENT; + + // hook_block_build_alter() adds an additional cache key. + $alter_add_key = $this->randomMachineName(); + \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key); + $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), $default_contexts, $default_tags, $default_max_age); + \Drupal::state()->set('block_test_block_alter_cache_key', NULL); + + // hook_block_build_alter() adds an additional cache context. + $alter_add_context = 'url.query_args:' . $this->randomMachineName(); + \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context); + $this->assertBlockRenderedWithExpectedCacheability($default_keys, Cache::mergeContexts($default_contexts, [$alter_add_context]), $default_tags, $default_max_age); + \Drupal::state()->set('block_test_block_alter_cache_context', NULL); + + // hook_block_build_alter() adds an additional cache tag. + $alter_add_tag = $this->randomMachineName(); + \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag); + $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, Cache::mergeTags($default_tags, [$alter_add_tag]), $default_max_age); + \Drupal::state()->set('block_test_block_alter_cache_tag', NULL); + + // hook_block_build_alter() alters the max-age. + $alter_max_age = 300; + \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age); + $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, $default_tags, $alter_max_age); + \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL); + + // hook_block_build_alter() alters cache keys, contexts, tags and max-age. + \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key); + \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context); + \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag); + \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age); + $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), Cache::mergeContexts($default_contexts, [$alter_add_context]), Cache::mergeTags($default_tags, [$alter_add_tag]), $alter_max_age); + \Drupal::state()->set('block_test_block_alter_cache_key', NULL); + \Drupal::state()->set('block_test_block_alter_cache_context', NULL); + \Drupal::state()->set('block_test_block_alter_cache_tag', NULL); + \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL); + + // hook_block_build_alter() sets #create_placeholder. + \Drupal::state()->set('block_test_block_alter_create_placeholder', TRUE); + $build = $this->getBlockRenderArray(); + $this->assertTrue(isset($build['#create_placeholder'])); + $this->assertTrue($build['#create_placeholder']); + \Drupal::state()->set('block_test_block_alter_create_placeholder', NULL); + + // Restore the previous request method. + $request->setMethod($request_method); + } + + /** + * Asserts that a block is built/rendered/cached with expected cacheability. + * + * @param string[] $expected_keys + * The expected cache keys. + * @param string[] $expected_contexts + * The expected cache contexts. + * @param string[] $expected_tags + * The expected cache tags. + * @param int $expected_max_age + * The expected max-age. + */ + protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) { + $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; + + // Check that the expected cacheability metadata is present in: + // - the built render array; + $this->pass('Built render array'); + $build = $this->getBlockRenderArray(); + $this->assertIdentical($expected_keys, $build['#cache']['keys']); + $this->assertIdentical($expected_contexts, $build['#cache']['contexts']); + $this->assertIdentical($expected_tags, $build['#cache']['tags']); + $this->assertIdentical($expected_max_age, $build['#cache']['max-age']); + $this->assertFalse(isset($build['#create_placeholder'])); + // - the rendered render array; + $this->pass('Rendered render array'); + $this->renderer->renderRoot($build); + // - the render cache item. + $this->pass('Render cache item'); + $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts); + $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys()); + $cache_item = $this->container->get('cache.render')->get($cid); + $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.'); + $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags); + $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']); + $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']); + $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']); + + $this->container->get('cache.render')->delete($cid); + } + + /** * Get a fully built render array for a block. * * @return array diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module index 00f4ce9..7f058b6 100644 --- a/core/modules/block/tests/modules/block_test/block_test.module +++ b/core/modules/block/tests/modules/block_test/block_test.module @@ -30,6 +30,30 @@ function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterf } /** + * Implements hook_block_build_BASE_BLOCK_ID_alter(). + */ +function block_test_block_build_test_cache_alter(array &$build, BlockPluginInterface $block) { + // Test altering cache keys, contexts, tags and max-age. + if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) { + $build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key'); + } + if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) { + $build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context'); + } + if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) { + $build['#cache']['tags'][] = \Drupal::state()->get('block_test_block_alter_cache_tag'); + } + if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) { + $build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age'); + } + + // Test setting #create_placeholder. + if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) { + $build['#create_placeholder'] = TRUE; + } +} + +/** * #pre_render callback for a block to alter its content. */ function block_test_pre_render_alter_content($build) {