Commit 539061c7 authored by webchick's avatar webchick

Issue #2286357 by tim.plunkett: Introduce Display Variants, use for the block rendering flow.

parent 234ed4f8
......@@ -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'
......@@ -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:
......
<?php
/**
* @file
* Contains \Drupal\Core\Display\Annotation\DisplayVariant.
*/
namespace Drupal\Core\Display\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a display variant annotation object.
*
* Display variants are used to dictate the output of a given Display, which
* can be used to control the output of many parts of Drupal. For example, the
* FullPageVariant is used by the Block module to control regions and output
* block content placed in those regions.
*
* Variants are usually chosen by some selection criteria, and are instantiated
* directly. Each variant must define its own approach to rendering, and can
* either load its own data or be injected with data from another Display
* object.
*
* @todo: Revise description when/if Displays are added to core:
* https://www.drupal.org/node/2292733
*
* @see \Drupal\Core\Display\VariantInterface
* @see \Drupal\Core\Display\VariantManager
*
* @Annotation
*/
class DisplayVariant extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The administrative label.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $admin_label = '';
}
<?php
/**
* @file
* Contains \Drupal\Core\Display\VariantBase.
*/
namespace Drupal\Core\Display;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\PluginDependencyTrait;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a base class for DisplayVariant plugins.
*/
abstract class VariantBase extends PluginBase implements VariantInterface {
use PluginDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Display\VariantInterface.
*/
namespace Drupal\Core\Display;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an interface for DisplayVariant plugins.
*
* @see \Drupal\Core\Display\Annotation\DisplayVariant
* @see \Drupal\Core\Display\VariantManager
*/
interface VariantInterface extends PluginInspectionInterface, ConfigurablePluginInterface, PluginFormInterface {
/**
* Returns the user-facing display variant label.
*
* @return string
* The display variant label.
*/
public function label();
/**
* Returns the admin-facing display variant label.
*
* This is for the type of display variant, not the configured variant itself.
*
* @return string
* The display variant administrative label.
*/
public function adminLabel();
/**
* Returns the unique ID for the display variant.
*
* @return string
* The display variant ID.
*/
public function id();
/**
* Returns the weight of the display variant.
*
* @return int
* The display variant weight.
*/
public function getWeight();
/**
* Sets the weight of the display variant.
*
* @param int $weight
* The weight to set.
*/
public function setWeight($weight);
/**
* Determines if this display variant is accessible.
*
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*
* @return bool
* TRUE if this display variant is accessible, FALSE otherwise.
*/
public function access(AccountInterface $account = NULL);
/**
* Builds and returns the renderable array for the display variant.
*
* @return array
* A render array for the display variant.
*/
public function build();
}
<?php
/**
* @file
* Contains \Drupal\Core\Display\VariantManager.
*/
namespace Drupal\Core\Display;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages discovery of display variant plugins.
*
* @see \Drupal\Core\Display\Annotation\DisplayVariant
* @see \Drupal\Core\Display\VariantInterface
*/
class VariantManager extends DefaultPluginManager {
/**
* Constructs a new VariantManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/DisplayVariant', $namespaces, $module_handler, 'Drupal\Core\Display\Annotation\DisplayVariant');
$this->setCacheBackend($cache_backend, 'variant_plugins');
$this->alterInfo('display_variant_plugin');
}
}
......@@ -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().
*/
......
<?php
/**
* @file
* Contains \Drupal\block\Plugin\DisplayVariant\FullPageVariant.
*/
namespace Drupal\block\Plugin\DisplayVariant;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Display\VariantBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a display variant that represents the full page.
*
* @DisplayVariant(
* id = "full_page",
* admin_label = @Translation("Full page")
* )
*/
class FullPageVariant extends VariantBase implements ContainerFactoryPluginInterface {
/**
* The block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockStorage;
/**
* The block view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $blockViewBuilder;
/**
* The current theme.
*
* @var string
*/
protected $theme;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The theme negotiator.
*
* @var \Drupal\Core\Theme\ThemeNegotiatorInterface
*/
protected $themeNegotiator;
/**
* Constructs a new FullPageVariant.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $block_storage
* The block entity storage.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $block_view_builder
* The block view builder.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
* The theme negotiator.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $block_storage, EntityViewBuilderInterface $block_view_builder, RouteMatchInterface $route_match, ThemeNegotiatorInterface $theme_negotiator) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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;
}
}
<?php
/**
* @file
* Contains \Drupal\block\Tests\Plugin\DisplayVariant\FullPageVariantTest.
*/
namespace Drupal\block\Tests\Plugin\DisplayVariant;
use Drupal\Tests\UnitTestCase;
/**
* Tests the full page display variant.
*
* @coversDefaultClass \Drupal\block\Plugin\DisplayVariant\FullPageVariant
*
* @group Drupal
* @group Block
* @group Display
*/
class FullPageVariantTest extends UnitTestCase {
/**
* The block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $blockStorage;
/**
* The block view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $blockViewBuilder;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $routeMatch;
/**
* The theme negotiator.
*
* @var \Drupal\Core\Theme\ThemeNegotiatorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $themeNegotiator;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => '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',
)));