From 539061c75bbb6de1c8a8a628cf8ff6ae121e611d Mon Sep 17 00:00:00 2001 From: webchick Date: Wed, 25 Jun 2014 13:07:49 -0700 Subject: [PATCH] Issue #2286357 by tim.plunkett: Introduce Display Variants, use for the block rendering flow. --- core/config/schema/core.data_types.schema.yml | 17 ++ core/core.services.yml | 3 + .../Display/Annotation/DisplayVariant.php | 51 ++++++ core/lib/Drupal/Core/Display/VariantBase.php | 134 ++++++++++++++ .../Drupal/Core/Display/VariantInterface.php | 85 +++++++++ .../Drupal/Core/Display/VariantManager.php | 40 ++++ core/modules/block/block.module | 85 +-------- .../Plugin/DisplayVariant/FullPageVariant.php | 171 ++++++++++++++++++ .../DisplayVariant/FullPageVariantTest.php | 158 ++++++++++++++++ core/modules/system/core.api.php | 22 +++ .../Tests/Core/Display/DisplayVariantTest.php | 165 +++++++++++++++++ 11 files changed, 851 insertions(+), 80 deletions(-) create mode 100644 core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php create mode 100644 core/lib/Drupal/Core/Display/VariantBase.php create mode 100644 core/lib/Drupal/Core/Display/VariantInterface.php create mode 100755 core/lib/Drupal/Core/Display/VariantManager.php create mode 100644 core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php create mode 100644 core/modules/block/tests/src/Plugin/DisplayVariant/FullPageVariantTest.php create mode 100644 core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index cc50e0facbc0..0c92bdc6e368 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -312,3 +312,20 @@ condition.plugin: label: 'Context assignments' sequence: - type: string + +display_variant.plugin: + type: mapping + label: 'Display variant' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + weight: + type: integer + label: 'Weight' + uuid: + type: string + label: 'UUID' diff --git a/core/core.services.yml b/core/core.services.yml index e1613195685a..ce0db128a554 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -263,6 +263,9 @@ services: plugin.manager.menu.contextual_link: class: Drupal\Core\Menu\ContextualLinkManager arguments: ['@controller_resolver', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user', '@request_stack'] + plugin.manager.display_variant: + class: Drupal\Core\Display\VariantManager + parent: default_plugin_manager plugin.cache_clearer: class: Drupal\Core\Plugin\CachedDiscoveryClearer request: diff --git a/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php new file mode 100644 index 000000000000..08817a136306 --- /dev/null +++ b/core/lib/Drupal/Core/Display/Annotation/DisplayVariant.php @@ -0,0 +1,51 @@ +setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->configuration['label']; + } + + /** + * {@inheritdoc} + */ + public function adminLabel() { + return $this->pluginDefinition['admin_label']; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->configuration['uuid']; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return (int) $this->configuration['weight']; + } + + /** + * {@inheritdoc} + */ + public function setWeight($weight) { + $this->configuration['weight'] = (int) $weight; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return array( + 'id' => $this->getPluginId(), + ) + $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration + $this->defaultConfiguration(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'label' => '', + 'uuid' => '', + 'weight' => 0, + ); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, array &$form_state) { + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#description' => $this->t('The label for this display variant.'), + '#default_value' => $this->label(), + '#maxlength' => '255', + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, array &$form_state) { + $this->configuration['label'] = $form_state['values']['label']; + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account = NULL) { + return TRUE; + } + +} diff --git a/core/lib/Drupal/Core/Display/VariantInterface.php b/core/lib/Drupal/Core/Display/VariantInterface.php new file mode 100644 index 000000000000..780b47a32514 --- /dev/null +++ b/core/lib/Drupal/Core/Display/VariantInterface.php @@ -0,0 +1,85 @@ +setCacheBackend($cache_backend, 'variant_plugins'); + $this->alterInfo('display_variant_plugin'); + } + +} diff --git a/core/modules/block/block.module b/core/modules/block/block.module index ce880be0fcce..69030dfd6283 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -86,22 +86,11 @@ function block_page_build(&$page) { // Fetch a list of regions for the current theme. $all_regions = system_region_list($theme); if (\Drupal::routeMatch()->getRouteName() != 'block.admin_demo') { - // Load all region content assigned via blocks. - foreach (array_keys($all_regions) as $region) { - // Assign blocks to region. - if ($blocks = block_get_blocks_by_region($region)) { - $page[$region] = $blocks; - } - } - // Once we've finished attaching all blocks to the page, clear the static - // cache to allow modules to alter the block list differently in different - // contexts. For example, any code that triggers hook_page_build() more - // than once in the same page request may need to alter the block list - // differently each time, so that only certain parts of the page are - // actually built. We do not clear the cache any earlier than this, though, - // because it is used each time block_get_blocks_by_region() gets called - // above. - drupal_static_reset('block_list'); + // Create a full page display variant, which will load blocks into their + // regions. + $page += \Drupal::service('plugin.manager.display_variant') + ->createInstance('full_page') + ->build(); } else { // Append region description if we are rendering the regions demo page. @@ -123,33 +112,6 @@ function block_page_build(&$page) { } } -/** - * Gets a renderable array of a region containing all enabled blocks. - * - * @param $region - * The requested region. - * - * @return - * A renderable array of a region containing all enabled blocks. - */ -function block_get_blocks_by_region($region) { - $build = array(); - if ($list = block_list($region)) { - foreach ($list as $key => $block) { - if ($block->access('view')) { - $build[$key] = entity_view($block, 'block'); - } - } - // If none of the blocks in this region are visible, then don't set anything - // else in the render array, because that would cause the region to show up. - if (!empty($build)) { - // block_list() already returned the blocks in sorted order. - $build['#sorted'] = TRUE; - } - } - return $build; -} - /** * Returns an array of block class instances by theme. * @@ -240,43 +202,6 @@ function block_theme_initialize($theme) { } } -/** - * Returns all blocks in the specified region for the current user. - * - * @param $region - * The name of a region. - * - * @return - * An array of block objects, indexed with the configuration object name - * that represents the configuration. If you are displaying your blocks in - * one or two sidebars, you may check whether this array is empty to see - * how many columns are going to be displayed. - */ -function block_list($region) { - $blocks = &drupal_static(__FUNCTION__); - - if (!isset($blocks)) { - global $theme; - $blocks = array(); - foreach (entity_load_multiple_by_properties('block', array('theme' => $theme)) as $block_id => $block) { - // Onlye include valid blocks in the list. - // @todo Remove this check as part of https://drupal.org/node/1776830. - if ($block->getPlugin()) { - $blocks[$block->get('region')][$block_id] = $block; - } - } - } - - // Create an empty array if there are no entries. - if (!isset($blocks[$region])) { - $blocks[$region] = array(); - } - - uasort($blocks[$region], 'Drupal\block\Entity\Block::sort'); - - return $blocks[$region]; -} - /** * Implements hook_rebuild(). */ diff --git a/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php new file mode 100644 index 000000000000..7243362fc091 --- /dev/null +++ b/core/modules/block/src/Plugin/DisplayVariant/FullPageVariant.php @@ -0,0 +1,171 @@ +blockStorage = $block_storage; + $this->blockViewBuilder = $block_view_builder; + $this->routeMatch = $route_match; + $this->themeNegotiator = $theme_negotiator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager')->getStorage('block'), + $container->get('entity.manager')->getViewBuilder('block'), + $container->get('current_route_match'), + $container->get('theme.negotiator') + ); + } + + /** + * Gets the current theme for this page. + * + * @return string + * The current theme. + */ + protected function getTheme() { + return $this->themeNegotiator->determineActiveTheme($this->routeMatch); + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = array(); + // Load all region content assigned via blocks. + foreach ($this->getRegionAssignments() as $region => $blocks) { + /** @var $blocks \Drupal\block\BlockInterface[] */ + foreach ($blocks as $key => $block) { + if ($block->access('view')) { + $build[$region][$key] = $this->blockViewBuilder->view($block); + } + } + if (!empty($build[$region])) { + // self::getRegionAssignments() returns the blocks in sorted order. + $build[$region]['#sorted'] = TRUE; + } + } + return $build; + } + + /** + * Returns the human-readable list of regions keyed by machine name. + * + * @return array + * An array of human-readable region names keyed by machine name. + */ + protected function getRegionNames() { + return system_region_list($this->getTheme()); + } + + /** + * Returns an array of regions and their block entities. + * + * @return array + * The array is first keyed by region machine name, with the values + * containing an array keyed by block ID, with block entities as the values. + */ + protected function getRegionAssignments() { + // Build an array of the region names in the right order. + $empty = array_fill_keys(array_keys($this->getRegionNames()), array()); + + $full = array(); + foreach ($this->blockStorage->loadByProperties(array('theme' => $this->getTheme())) as $block_id => $block) { + $full[$block->get('region')][$block_id] = $block; + } + + // Merge it with the actual values to maintain the region ordering. + $assignments = array_intersect_key(array_merge($empty, $full), $empty); + foreach ($assignments as &$assignment) { + // Suppress errors because PHPUnit will indirectly modify the contents, + // triggering https://bugs.php.net/bug.php?id=50688. + @uasort($assignment, 'Drupal\block\Entity\Block::sort'); + } + return $assignments; + } + +} diff --git a/core/modules/block/tests/src/Plugin/DisplayVariant/FullPageVariantTest.php b/core/modules/block/tests/src/Plugin/DisplayVariant/FullPageVariantTest.php new file mode 100644 index 000000000000..051b1b03996a --- /dev/null +++ b/core/modules/block/tests/src/Plugin/DisplayVariant/FullPageVariantTest.php @@ -0,0 +1,158 @@ + 'Full page variant test', + 'description' => '', + 'group' => 'Block', + ); + } + + /** + * Sets up a display variant plugin for testing. + * + * @param array $configuration + * An array of plugin configuration. + * @param array $definition + * The plugin definition array. + * + * @return \Drupal\block\Plugin\DisplayVariant\FullPageVariant|\PHPUnit_Framework_MockObject_MockObject + * A mocked display variant plugin. + */ + public function setUpDisplayVariant($configuration = array(), $definition = array()) { + $this->blockStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface'); + $this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface'); + $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); + $this->themeNegotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); + return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\FullPageVariant') + ->setConstructorArgs(array($configuration, 'test', $definition, $this->blockStorage, $this->blockViewBuilder, $this->routeMatch, $this->themeNegotiator)) + ->setMethods(array('getRegionNames')) + ->getMock(); + } + + /** + * Tests the building of a full page variant. + * + * @covers ::build + * @covers ::getRegionAssignments + */ + public function testBuild() { + $theme = $this->randomName(); + $display_variant = $this->setUpDisplayVariant(); + $this->themeNegotiator->expects($this->any()) + ->method('determineActiveTheme') + ->with($this->routeMatch) + ->will($this->returnValue($theme)); + $display_variant->expects($this->once()) + ->method('getRegionNames') + ->will($this->returnValue(array( + 'top' => 'Top', + 'bottom' => 'Bottom', + ))); + + $blocks_config = array( + 'block1' => array( + TRUE, 'top', 0, + ), + // Test a block without access. + 'block2' => array( + FALSE, 'bottom', 0, + ), + // Test two blocks in the same region with specific weight. + 'block3' => array( + TRUE, 'bottom', 5, + ), + 'block4' => array( + TRUE, 'bottom', -5, + ), + ); + $blocks = array(); + foreach ($blocks_config as $block_id => $block_config) { + $block = $this->getMock('Drupal\block\BlockInterface'); + $block->expects($this->once()) + ->method('access') + ->will($this->returnValue($block_config[0])); + $block->expects($this->any()) + ->method('get') + ->will($this->returnValueMap(array( + array('region', $block_config[1]), + array('weight', $block_config[2]), + array('status', TRUE), + ))); + $blocks[$block_id] = $block; + } + + $this->blockViewBuilder->expects($this->exactly(3)) + ->method('view') + ->will($this->returnValue(array())); + $this->blockStorage->expects($this->once()) + ->method('loadByProperties') + ->with(array('theme' => $theme)) + ->will($this->returnValue($blocks)); + + $expected = array( + 'top' => array( + 'block1' => array(), + '#sorted' => TRUE, + ), + 'bottom' => array( + 'block4' => array(), + 'block3' => array(), + '#sorted' => TRUE, + ), + ); + $this->assertSame($expected, $display_variant->build()); + } + +} diff --git a/core/modules/system/core.api.php b/core/modules/system/core.api.php index e42548dc803f..c890e6073318 100644 --- a/core/modules/system/core.api.php +++ b/core/modules/system/core.api.php @@ -1324,3 +1324,25 @@ * @see validation * @} */ + +/** + * @addtogroup hooks + * @{ + */ + +/** + * Alter display variant plugin definitions. + * + * @param array $definitions + * The array of display variant definitions, keyed by plugin ID. + * + * @see \Drupal\Core\Display\VariantManager + * @see \Drupal\Core\Display\Annotation\DisplayVariant + */ +function hook_display_variant_plugin_alter(array &$definitions) { + $definitions['full_page']['admin_label'] = t('Block layout'); +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php b/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php new file mode 100644 index 000000000000..41a0458552f8 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Display/DisplayVariantTest.php @@ -0,0 +1,165 @@ + 'Display variant', + 'description' => '', + 'group' => 'Display variant', + ); + } + + /** + * Sets up a display variant plugin for testing. + * + * @param array $configuration + * An array of plugin configuration. + * @param array $definition + * The plugin definition array. + * + * @return \Drupal\Core\Display\VariantBase|\PHPUnit_Framework_MockObject_MockObject + * A mocked display variant plugin. + */ + public function setUpDisplayVariant($configuration = array(), $definition = array()) { + return $this->getMockBuilder('Drupal\Core\Display\VariantBase') + ->setConstructorArgs(array($configuration, 'test', $definition)) + ->setMethods(array('build')) + ->getMock(); + } + + /** + * Tests the label() method. + * + * @covers ::label + */ + public function testLabel() { + $display_variant = $this->setUpDisplayVariant(array('label' => 'foo')); + $this->assertSame('foo', $display_variant->label()); + } + + /** + * Tests the label() method using a default value. + * + * @covers ::label + */ + public function testLabelDefault() { + $display_variant = $this->setUpDisplayVariant(); + $this->assertSame('', $display_variant->label()); + } + + /** + * Tests the getWeight() method. + * + * @covers ::getWeight + */ + public function testGetWeight() { + $display_variant = $this->setUpDisplayVariant(array('weight' => 5)); + $this->assertSame(5, $display_variant->getWeight()); + } + + /** + * Tests the getWeight() method using a default value. + * + * @covers ::getWeight + */ + public function testGetWeightDefault() { + $display_variant = $this->setUpDisplayVariant(); + $this->assertSame(0, $display_variant->getWeight()); + } + + /** + * Tests the getConfiguration() method. + * + * @covers ::getConfiguration + * + * @dataProvider providerTestGetConfiguration + */ + public function testGetConfiguration($configuration, $expected) { + $display_variant = $this->setUpDisplayVariant($configuration); + + $this->assertSame($expected, $display_variant->getConfiguration()); + } + + /** + * Provides test data for testGetConfiguration(). + */ + public function providerTestGetConfiguration() { + $data = array(); + $data[] = array( + array(), + array( + 'id' => 'test', + 'label' => '', + 'uuid' => '', + 'weight' => 0, + ), + ); + $data[] = array( + array('label' => 'Test'), + array( + 'id' => 'test', + 'label' => 'Test', + 'uuid' => '', + 'weight' => 0, + ), + ); + $data[] = array( + array('id' => 'foo'), + array( + 'id' => 'test', + 'label' => '', + 'uuid' => '', + 'weight' => 0, + ), + ); + return $data; + } + + /** + * Tests the access() method. + * + * @covers ::access + */ + public function testAccess() { + $display_variant = $this->setUpDisplayVariant(); + $this->assertTrue($display_variant->access()); + } + + /** + * Tests the submitConfigurationForm() method. + * + * @covers ::submitConfigurationForm + */ + public function testSubmitConfigurationForm() { + $display_variant = $this->setUpDisplayVariant(); + $this->assertSame('', $display_variant->label()); + + $form = array(); + $label = $this->randomName(); + $form_state['values']['label'] = $label; + $display_variant->submitConfigurationForm($form, $form_state); + $this->assertSame($label, $display_variant->label()); + } + +}