.../block/lib/Drupal/block/BlockViewBuilder.php | 18 +- .../Drupal/block/Tests/BlockStorageUnitTest.php | 54 +-- .../Drupal/block/Tests/BlockViewBuilderTest.php | 346 ++++++++++++++++++++ .../tests/modules/block_test/block_test.module | 28 ++ .../block_test/Plugin/Block/TestCacheBlock.php | 10 +- 5 files changed, 390 insertions(+), 66 deletions(-) diff --git a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php index 4ccfcf4..a6a925a 100644 --- a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php +++ b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php @@ -103,6 +103,11 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la // @todo Remove after fixing http://drupal.org/node/1989568. $build[$key]['#block'] = $entity; + + // Don't run in ::buildBlock() to ensure cache keys can be altered. If an + // alter hook wants to modify the block contents, it can append another + // #pre_render hook. + $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin); } return $build; } @@ -114,12 +119,10 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la * - if there is no content, aborts rendering, and makes sure the block won't * be rendered. * - if there is content, moves the contextual links from the block content to - * the block itself, and invokes the alter hooks. + * the block itself. */ public function buildBlock($build) { - $plugin = $build['#block']->getPlugin(); - $content = $plugin->build(); - + $content = $build['#block']->getPlugin()->build(); if (!empty($content)) { // Place the $content returned by the block plugin into a 'content' child // element, as a way to allow the plugin to have complete control of its @@ -138,18 +141,13 @@ public function buildBlock($build) { } } $build['content'] = $content; - - $base_id = $plugin->getBasePluginId(); - $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build, $plugin); - - return $build; } else { // Abort rendering: render as the empty string and ensure this block is // render cached, so we can avoid the work of having to repeatedly // determine whether the block is empty. E.g. modifying or adding entities // could cause the block to no longer be empty. - return array( + $build = array( '#markup' => '', '#cache' => $build['#cache'], ); diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php index d95edca..6a5be54 100644 --- a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php @@ -56,7 +56,6 @@ public function testBlockCRUD() { // Run each test method in the same installation. $this->createTests(); $this->loadTests(); - $this->renderTests(); $this->deleteTests(); } @@ -115,7 +114,7 @@ protected function createTests() { } /** - * Tests the rendering of blocks. + * Tests the loading of blocks. */ protected function loadTests() { $entity = $this->controller->load('test_block'); @@ -130,57 +129,6 @@ protected function loadTests() { } /** - * Tests the rendering of blocks. - */ - protected function renderTests() { - // Test the rendering of a block. - $entity = entity_load('block', 'test_block'); - $output = entity_view($entity, 'block'); - $expected = array(); - $expected[] = '
'; - $expected[] = ' '; - $expected[] = ' '; - $expected[] = ''; - $expected[] = '
'; - $expected[] = ' '; - $expected[] = '
'; - $expected[] = '
'; - $expected[] = ''; - $expected_output = implode("\n", $expected); - $this->assertEqual(drupal_render($output), $expected_output); - - // Reset the HTML IDs so that the next render is not affected. - drupal_static_reset('drupal_html_id'); - - // Test the rendering of a block with a given title. - $entity = $this->controller->create(array( - 'id' => 'test_block2', - 'theme' => 'stark', - 'plugin' => 'test_html', - 'settings' => array( - 'label' => 'Powered by Bananas', - ), - )); - $entity->save(); - $output = entity_view($entity, 'block'); - $expected = array(); - $expected[] = '
'; - $expected[] = ' '; - $expected[] = '

Powered by Bananas

'; - $expected[] = ' '; - $expected[] = ''; - $expected[] = '
'; - $expected[] = ' '; - $expected[] = '
'; - $expected[] = '
'; - $expected[] = ''; - $expected_output = implode("\n", $expected); - $this->assertEqual(drupal_render($output), $expected_output); - // Clean up this entity. - $entity->delete(); - } - - /** * Tests the deleting of blocks. */ protected function deleteTests() { diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockViewBuilderTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockViewBuilderTest.php new file mode 100644 index 0000000..aa7dd2e --- /dev/null +++ b/core/modules/block/lib/Drupal/block/Tests/BlockViewBuilderTest.php @@ -0,0 +1,346 @@ + 'Block rendering', + 'description' => 'Tests the block view builder.', + 'group' => 'Block', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->controller = $this->container + ->get('entity.manager') + ->getStorageController('block'); + + \Drupal::state()->set('block_test.content', 'Llamas > unicorns!'); + + // Create a block with only required values. + $this->block = $this->controller->create(array( + 'id' => 'test_block', + 'theme' => 'stark', + 'plugin' => 'test_cache', + )); + $this->block->save(); + + $this->container->get('cache.block')->deleteAll(); + } + + /** + * Tests the rendering of blocks. + */ + public function testBasicRendering() { + \Drupal::state()->set('block_test.content', ''); + + $entity = $this->controller->create(array( + 'id' => 'test_block1', + 'theme' => 'stark', + 'plugin' => 'test_html', + )); + $entity->save(); + + // Test the rendering of a block. + $entity = entity_load('block', 'test_block1'); + $output = entity_view($entity, 'block'); + $expected = array(); + $expected[] = '
'; + $expected[] = ' '; + $expected[] = ' '; + $expected[] = ''; + $expected[] = '
'; + $expected[] = ' '; + $expected[] = '
'; + $expected[] = '
'; + $expected[] = ''; + $expected_output = implode("\n", $expected); + $this->assertEqual(drupal_render($output), $expected_output); + + // Reset the HTML IDs so that the next render is not affected. + drupal_static_reset('drupal_html_id'); + + // Test the rendering of a block with a given title. + $entity = $this->controller->create(array( + 'id' => 'test_block2', + 'theme' => 'stark', + 'plugin' => 'test_html', + 'settings' => array( + 'label' => 'Powered by Bananas', + ), + )); + $entity->save(); + $output = entity_view($entity, 'block'); + $expected = array(); + $expected[] = '
'; + $expected[] = ' '; + $expected[] = '

Powered by Bananas

'; + $expected[] = ' '; + $expected[] = ''; + $expected[] = '
'; + $expected[] = ' '; + $expected[] = '
'; + $expected[] = '
'; + $expected[] = ''; + $expected_output = implode("\n", $expected); + $this->assertEqual(drupal_render($output), $expected_output); + } + + /** + * Tests block render cache handling. + */ + public function testBlockViewBuilderCache() { + // Verify cache handling for a non-empty block. + $this->verifyRenderCacheHandling(); + + // Create an empty block. + $this->block = $this->controller->create(array( + 'id' => 'test_block', + 'theme' => 'stark', + 'plugin' => 'test_cache', + )); + $this->block->save(); + \Drupal::state()->set('block_test.content', NULL); + + // Verify cache handling for an empty block. + $this->verifyRenderCacheHandling(); + } + + /** + * Verifies render cache handling of the block being tested. + * + * @see ::testBlockViewBuilderCache() + */ + protected function verifyRenderCacheHandling() { + // Force a request via GET so we can get drupal_render() cache working. + $request_method = \Drupal::request()->server->get('REQUEST_METHOD'); + $this->container->get('request')->setMethod('GET'); + + // Test that entities with caching disabled do not generate a cache entry. + $build = $this->getBlockRenderArray(); + $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags'), 'The render array element of uncacheable blocks is not cached, but does have cache tags set.'); + + // Enable block caching. + $this->setBlockCacheConfig(array( + 'max_age' => 600, + )); + + // Test that a cache entry is created. + $build = $this->getBlockRenderArray(); + $cid = drupal_render_cid_create($build); + drupal_render($build); + $this->assertTrue($this->container->get('cache.block')->get($cid), 'The block render element has been cached.'); + + // Re-save the block and check that the cache entry has been deleted. + $this->block->save(); + $this->assertFalse($this->container->get('cache.block')->get($cid), 'The block render cache entry has been cleared when the block was saved.'); + + // Rebuild the render array (creating a new cache entry in the process) and + // delete the block to check the cache entry is deleted. + unset($build['#printed']); + drupal_render($build); + $this->assertTrue($this->container->get('cache.block')->get($cid), 'The block render element has been cached.'); + $this->block->delete(); + $this->assertFalse($this->container->get('cache.block')->get($cid), 'The block render cache entry has been cleared when the block was deleted.'); + + // Restore the previous request method. + $this->container->get('request')->setMethod($request_method); + } + + /** + * Tests block view altering. + */ + public function testBlockViewBuilderAlter() { + // Establish baseline. + $build = $this->getBlockRenderArray(); + $this->assertIdentical(drupal_render($build), 'Llamas > unicorns!'); + + // Enable the block view alter hook that adds a suffix, for basic testing. + \Drupal::state()->set('block_test_view_alter_suffix', TRUE); + + // Basic: non-empty block. + $build = $this->getBlockRenderArray(); + $this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '
Goodbye!', 'A block with content is altered.'); + $this->assertIdentical(drupal_render($build), 'Llamas > unicorns!
Goodbye!'); + + // Basic: empty block. + \Drupal::state()->set('block_test.content', NULL); + $build = $this->getBlockRenderArray(); + $this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '
Goodbye!', 'A block without content is altered.'); + $this->assertIdentical(drupal_render($build), '
Goodbye!'); + + // Disable the block view alter hook that adds a suffix, for basic testing. + \Drupal::state()->set('block_test_view_alter_suffix', FALSE); + + // Force a request via GET so we can get drupal_render() cache working. + $request_method = \Drupal::request()->server->get('REQUEST_METHOD'); + $this->container->get('request')->setMethod('GET'); + + $default_keys = array('entity_view', 'block', 'test_block', 'en', 'cache_context.theme'); + $default_tags = array('content' => TRUE, 'block_view' => TRUE, 'block' => array('test_block'), 'block_plugin' => array('test_cache')); + + // Advanced: cached block, but an alter hook adds an additional cache key. + $this->setBlockCacheConfig(array( + 'max_age' => 600, + )); + $alter_add_key = $this->randomName(); + \Drupal::state()->set('block_test_view_alter_cache_key', $alter_add_key); + $expected_keys = array_merge($default_keys, array($alter_add_key)); + $build = $this->getBlockRenderArray(); + $this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.'); + $cid = drupal_render_cid_create(array('#cache' => array('keys' => $expected_keys))); + $this->assertIdentical(drupal_render($build), ''); + $cache_entry = $this->container->get('cache.block')->get($cid); + $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.'); + $expected_flattened_tags = array('content:1', 'block_view:1', 'block:test_block', 'block_plugin:test_cache'); + $this->assertIdentical($cache_entry->tags, array_combine($expected_flattened_tags, $expected_flattened_tags)); //, 'The block render element has been cached with the expected cache tags.'); + $this->container->get('cache.block')->delete($cid); + + // Advanced: cached block, but an alter hook adds an additional cache tag. + $alter_add_tag = $this->randomName(); + \Drupal::state()->set('block_test_view_alter_cache_tag', $alter_add_tag); + $expected_tags = NestedArray::mergeDeep($default_tags, array($alter_add_tag => TRUE)); + $build = $this->getBlockRenderArray(); + $this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.'); + $cid = drupal_render_cid_create(array('#cache' => array('keys' => $expected_keys))); + $this->assertIdentical(drupal_render($build), ''); + $cache_entry = $this->container->get('cache.block')->get($cid); + $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.'); + $expected_flattened_tags = array('content:1', 'block_view:1', 'block:test_block', 'block_plugin:test_cache', $alter_add_tag . ':1'); + $this->assertIdentical($cache_entry->tags, array_combine($expected_flattened_tags, $expected_flattened_tags)); //, 'The block render element has been cached with the expected cache tags.'); + $this->container->get('cache.block')->delete($cid); + + // Advanced: cached block, but an alter hook adds a #pre_render callback to + // alter the eventual content. + \Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE); + $build = $this->getBlockRenderArray(); + $this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before calling drupal_render().'); + $this->assertIdentical(drupal_render($build), 'Hiya!
'); + $this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!
', 'A cached block without content is altered.'); + + // Restore the previous request method. + $this->container->get('request')->setMethod($request_method); + } + + /** + * Tests block render cache handling with configurable cache contexts. + * + * This is only intended to test that an existing block can be configured with + * additional contexts, not to test that each context works correctly. + * + * @see \Drupal\block\Tests\BlockCacheTest + */ + public function testBlockViewBuilderCacheContexts() { + // Force a request via GET so we can get drupal_render() cache working. + $request_method = \Drupal::request()->server->get('REQUEST_METHOD'); + $this->container->get('request')->setMethod('GET'); + + // First: no cache context. + $this->setBlockCacheConfig(array( + 'max_age' => 600, + )); + $build = $this->getBlockRenderArray(); + $cid = drupal_render_cid_create($build); + drupal_render($build); + $this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.'); + + // Second: the "per URL" cache context. + $this->setBlockCacheConfig(array( + 'max_age' => 600, + 'contexts' => array('cache_context.url'), + )); + $old_cid = $cid; + $build = $this->getBlockRenderArray(); + $cid = drupal_render_cid_create($build); + drupal_render($build); + $this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.'); + $this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.'); + + // Third: the same block configuration, but a different URL. + $original_url_cache_context = $this->container->get('cache_context.url'); + $temp_context = new UrlCacheContext(Request::create('/foo')); + $this->container->set('cache_context.url', $temp_context); + $old_cid = $cid; + $build = $this->getBlockRenderArray(); + $cid = drupal_render_cid_create($build); + drupal_render($build); + $this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.'); + $this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.'); + $this->container->set('cache_context.url', $original_url_cache_context); + + // Restore the previous request method. + $this->container->get('request')->setMethod($request_method); + } + + /** + * Sets the test block's cache configuration. + * + * @param array $cache_config + * The desired cache configuration. + */ + protected function setBlockCacheConfig(array $cache_config) { + $block = $this->block->getPlugin(); + $block->setConfigurationValue('cache', $cache_config); + $this->block->save(); + } + + /** + * Get a fully built render array for a block. + * + * @return array + * The render array. + */ + protected function getBlockRenderArray() { + $build = $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block'); + + // Mock the build array to not require the theme registry. + unset($build['#theme']); + + return $build; + } + +} 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 1530f2c..a527ae1 100644 --- a/core/modules/block/tests/modules/block_test/block_test.module +++ b/core/modules/block/tests/modules/block_test/block_test.module @@ -5,6 +5,8 @@ * Provide test blocks. */ +use Drupal\block\BlockPluginInterface; + /** * Implements hook_block_alter(). */ @@ -13,3 +15,29 @@ function block_test_block_alter(&$block_info) { $block_info['test_block_instantiation']['category'] = t('Custom category'); } } + +/** + * Implements hook_block_view_BASE_BLOCK_ID_alter(). + */ +function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterface $block) { + if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) { + $build['#suffix'] = '
Goodbye!'; + } + if (\Drupal::state()->get('block_test_view_alter_cache_key') !== NULL) { + $build['#cache']['keys'][] = \Drupal::state()->get('block_test_view_alter_cache_key'); + } + if (\Drupal::state()->get('block_test_view_alter_cache_tag') !== NULL) { + $build['#cache']['tags'][\Drupal::state()->get('block_test_view_alter_cache_tag')] = TRUE; + } + if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) { + $build['#pre_render'][] = 'block_test_pre_render_alter_content'; + } +} + +/** + * #pre_render callback for a block to alter its content. + */ +function block_test_pre_render_alter_content($build) { + $build['#prefix'] = 'Hiya!
'; + return $build; +} diff --git a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php index 0216fa1..b6508a8 100644 --- a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php +++ b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php @@ -23,9 +23,13 @@ class TestCacheBlock extends BlockBase { * {@inheritdoc} */ public function build() { - return array( - '#markup' => \Drupal::state()->get('block_test.content'), - ); + $content = \Drupal::state()->get('block_test.content'); + + $build = array(); + if (!empty($content)) { + $build['#markup'] = $content; + } + return $build; } }