.../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;
}
}