Issue #1817044 by sdboyer, effulgentsia, tim.plunkett, Gábor Hojtsy, yched: Implement Display, a type of config for use by layouts, et. all.

8.0.x
webchick 2012-11-14 08:08:01 -08:00
parent b28e80dd59
commit 75b5ee1258
17 changed files with 802 additions and 10 deletions

View File

@ -2,4 +2,6 @@ title: One column
category: Columns: 1
template: one-col
regions:
content: 'Content'
content:
label: Middle column
type: content

View File

@ -4,5 +4,9 @@ template: two-col
stylesheets:
- two-col.css
regions:
first: 'First column'
second: 'Second column'
first:
label: Left side
type: content
second:
label: Right side
type: aside

View File

@ -0,0 +1,88 @@
<?php
/**
* @file
* Definition of Drupal\layout\Config\BoundDisplayInterface
*/
namespace Drupal\layout\Config;
use Drupal\layout\Plugin\LayoutInterface;
/**
* Interface for a Display object that is coupled to a specific layout.
*
* Bound displays contains references both to block instances and a specific
* layout, and the blocks are assigned to specific regions in that layout. Bound
* displays are used to serve real pages at request time.
*
* @see \Drupal\layout\Config\DisplayInterface
*/
interface BoundDisplayInterface extends DisplayInterface {
/**
* Sets the layout to be used by this display.
*
* @param string $layout_id
* The id of the desired layout.
*/
public function setLayout($layout_id);
/**
* Returns the blocks in the requested region, ordered by weight.
*
* @param string $region
* The region from which to return the set of blocks.
*
* @return array
* The list of blocks, ordered by their weight within this display. Each
* value in the list is the configuration object name of the block.
*/
public function getSortedBlocksByRegion($region);
/**
* Returns this display's blocks, organized by region and ordered by weight.
*
* @return array
* An array keyed by region name. For each region, the value is the same as
* what is returned by getSortedBlocksByRegion().
*
* @see getSortedBlocksByRegion()
*/
public function getAllSortedBlocks();
/**
* Returns the instantiated layout object to be used by this display.
*
* @return \Drupal\layout\Plugin\LayoutInterface
*/
public function getLayoutInstance();
/**
* Adjusts this display's block placement to work with the provided layout.
*
* Essentially a shortcut that calls DisplayInterface::mapBlocksToLayout(),
* saves the result in the appropriate object property, and finally calls
* BoundDisplayInterface::setLayout().
*
* @param \Drupal\layout\Plugin\LayoutInterface $layout
* The new layout to which blocks should be remapped.
*
* @see \Drupal\layout\Config\DisplayInterface::mapBlocksToLayout()
*/
public function remapToLayout(LayoutInterface $layout);
/**
* Returns an entity with the non-layout-specific configuration of this one.
*
* @param string $id
* The entity id to assign to the newly created entity.
*
* @param string $entity_type
* The type of entity to create. The PHP class for this entity type must
* implement \Drupal\layout\Config\UnboundDisplayInterface.
*
* @return \Drupal\layout\Config\UnboundDisplayInterface
* The newly-created unbound display.
*/
public function generateUnboundDisplay($id, $entity_type = 'unbound_display');
}

View File

@ -0,0 +1,138 @@
<?php
/**
* @file
* Definition of Drupal\layout\Config\DisplayBase.
*/
namespace Drupal\layout\Config;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\layout\Plugin\LayoutInterface;
/**
* Base class for 'display' and 'unbound_display' configuration entities.
*
* @see \Drupal\layout\Config\DisplayInterface
*/
abstract class DisplayBase extends ConfigEntityBase implements DisplayInterface {
/**
* The ID (config name) identifying a specific display object.
*
* @var string
*/
public $id;
/**
* The UUID identifying a specific display object.
*
* @var string
*/
public $uuid;
/**
* Contains all block configuration.
*
* There are two levels to the configuration contained herein: display-level
* block configuration, and then block instance configuration.
*
* Block instance configuration is stored in a separate config object. This
* array is keyed by the config name that uniquely identifies each block
* instance. At runtime, various object methods will retrieve this additional
* config and return it to calling code.
*
* Display-level block configuration is data that determines the behavior of
* a block *in this display*. The most important examples of this are the
* region to which the block is assigned, and its weighting in that region.
*
* @code
* array(
* 'block1-configkey' => array(
* 'region' => 'content',
* // store the region type name here so that we can do type conversion w/out
* // needing to have access to the original layout plugin
* 'region-type' => 'content',
* // increment by 100 so there is ALWAYS plenty of space for manual insertion
* 'weight' => -100,
* ),
* 'block2-configkey' => array(
* 'region' => 'sidebar_first',
* 'region-type' => 'aside',
* 'weight' => -100,
* ),
* 'block2-configkey' => array(
* 'region' => 'sidebar_first',
* 'region-type' => 'aside',
* 'weight' => 0,
* ),
* 'maincontent' => array(
* 'region' => 'content',
* 'region-type' => 'content',
* 'weight' => -200,
* ),
* );
* @endcode
*
* @var array
*/
protected $blockInfo = array();
/**
* Implements DisplayInterface::getAllBlockInfo().
*/
public function getAllBlockInfo() {
return $this->blockInfo;
}
/**
* Implements DisplayInterface::mapBlocksToLayout().
*
* @todo Decouple this implementation from this class, so that it could be
* more easily customized.
*/
public function mapBlocksToLayout(LayoutInterface $layout) {
$types = array();
$layout_regions = $layout->getRegions();
$layout_regions_indexed = array_keys($layout_regions);
foreach ($layout_regions as $name => $info) {
$types[$info['type']][] = $name;
}
$remapped_config = array();
foreach ($this->blockInfo as $name => $info) {
// First, if there's a direct region name match, use that.
if (!empty($info['region']) && isset($layout_regions[$info['region']])) {
// No need to do anything.
}
// Then, try to remap using region types.
else if (!empty($types[$info['region-type']])) {
$info['region'] = reset($types[$info['region-type']]);
}
// Finally, fall back to dumping everything in the layout's first region.
else {
if (!isset($first_region)) {
reset($layout_regions);
$first_region = key($layout_regions);
}
$info['region'] = $first_region;
}
$remapped_config[$name] = $info;
}
return $remapped_config;
}
/**
* Implements DisplayInterface::getAllRegionTypes().
*/
public function getAllRegionTypes() {
$types = array();
foreach ($this->blockInfo as $info) {
$types[] = $info['region-type'];
}
return array_unique($types);
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* @file
* Definition of Drupal\layout\Config\DisplayInterface
*/
namespace Drupal\layout\Config;
use Drupal\layout\Plugin\LayoutInterface;
/**
* Interface describing a Display configuration object.
*
* Displays are configuration that describe the placement of block instances
* in regions. Drupal includes two types of Display objects:
* - Bound displays include a reference to a specific layout, and each block is
* specified to display in a specific region of that layout. Bound displays
* are used to serve real pages at request time.
* - Unbound displays do not include a reference to any layout, and each block
* is assigned a region type, but not a specific region. Developers including
* default displays with their modules or distributions are encouraged to use
* unbound displays in order to minimize dependencies on specific layouts and
* allow site-specific configuration to dictate the layout.
*
* This interface defines what is common to all displays, whether bound or
* unbound.
*
* @see \Drupal\layout\Config\BoundDisplayInterface
* @see \Drupal\layout\Config\UnboundDisplayInterface
*/
interface DisplayInterface {
/**
* Returns the display-specific configuration of all blocks in this display.
*
* For each block that exists in Drupal (e.g., the "Who's Online" block),
* multiple "configured instances" can be created (e.g., a "Who's been online
* in the last 5 minutes" instance and a "Who's been online in the last 60
* minutes" instance). Each configured instance can be referenced by multiple
* displays (e.g., by a "regular" page, by an administrative page, and within
* one or more dashboards). This function returns the block instances that
* have been added to this display. Each key of the returned array is the
* block instance's configuration object name, and config() may be called on
* it in order to retrieve the full configuration that is shared across all
* displays. For each key, the value is an array of display-specific
* configuration, primarily the 'region' and 'weight', and anything else that
* affects the placement of the block within the layout rather than only the
* contents of the block.
*
* @return array
* An array keyed on each block's configuration object name. Each value is
* an array of information that determines the placement of the block within
* a layout, including:
* - region: The region in which to display the block (for bound displays
* only).
* - region-type: The type of region that is most appropriate for the block.
* Usually one of 'header', 'footer', 'nav', 'content', 'aside', or
* 'system', though custom region types are also allowed. This is
* primarily specified by unbound displays, where specifying a specific
* region name is impossible, because different layouts come with
* different regions.
* - weight: Within a region, blocks are rendered from low to high weight.
*/
public function getAllBlockInfo();
/**
* Maps the contained block info to the provided layout.
*
* @param \Drupal\layout\Plugin\LayoutInterface $layout
*
* @return array
* An array containing block configuration info, identical to that which
* is returned by DisplayInterface::getAllBlockInfo().
*/
public function mapBlocksToLayout(LayoutInterface $layout);
/**
* Returns the names of all region types to which blocks are assigned.
*
* @return array
* An indexed array of unique region type names, or an empty array if no
* region types were assigned.
*/
public function getAllRegionTypes();
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @file
* Definition of Drupal\layout\Config\UnboundDisplayInterface
*/
namespace Drupal\layout\Config;
use Drupal\layout\Plugin\LayoutInterface;
/**
* Interface for a Display that is not coupled with any layout.
*
* Unbound displays contain references to blocks, but not to any particular
* layout. Their primary use case is to express a set of relative block
* placements without necessitating any particular layout be present. This
* allows upstream (module and distribution) developers to express a visual
* composition of blocks without knowing anything about the layouts a
* particular site has available.
*
* @see \Drupal\layout\Config\DisplayInterface
*/
interface UnboundDisplayInterface extends DisplayInterface {
/**
* Returns a bound display entity by binding a layout to this unbound display.
*
* This will DisplayInterface::mapBlocksToLayout() using the provided layout,
* then create and return a new Display object with the output. This is just
* a factory - calling code is responsible for saving the returned object.
*
* @param \Drupal\layout\Plugin\LayoutInterface $layout
* The desired layout.
*
* @param string $id
* The entity id to assign to the newly created entity.
*
* @param string $entity_type
* The type of entity to create. The PHP class for this entity type must
* implement \Drupal\layout\Config\BoundDisplayInterface.
*
* @return \Drupal\layout\Config\BoundDisplayInterface
* The newly created entity.
*/
public function generateDisplay(LayoutInterface $layout, $id, $entity_type = 'display');
}

View File

@ -0,0 +1,186 @@
<?php
/**
* @file
* Definition of Drupal\layout\Plugin\Core\Entity\Display.
*/
namespace Drupal\layout\Plugin\Core\Entity;
use Drupal\layout\Config\DisplayBase;
use Drupal\layout\Config\BoundDisplayInterface;
use Drupal\layout\Config\UnboundDisplayInterface;
use Drupal\layout\Plugin\LayoutInterface;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines the display entity.
*
* @Plugin(
* id = "display",
* label = @Translation("Display"),
* module = "layout",
* controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
* config_prefix = "display.bound",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
* }
* )
*/
class Display extends DisplayBase implements BoundDisplayInterface {
/**
* A two-level array expressing block ordering within regions.
*
* The outer array is associative, keyed on region name. Each inner array is
* indexed, with the config address of a block as values and sorted according
* to order in which those blocks should appear in that region.
*
* This property is not stored statically in config, but is derived at runtime
* by DisplayBase::sortBlocks(). It is not stored statically because that
* would make using weights for ordering more difficult, and weights make
* external mass manipulation of displays much easier.
*
* @var array
*/
protected $blocksInRegions;
/**
* The layout instance being used to serve this page.
*
* @var \Drupal\layout\Plugin\LayoutInterface
*/
protected $layoutInstance;
/**
* The name of the layout plugin to use.
*
* @var string
*/
public $layout;
/**
* The settings with which to instantiate the layout plugin.
*
* @var array
*/
public $layoutSettings = array();
/**
* Implements BoundDisplayInterface::getSortedBlocksByRegion().
*
* @throws \Exception
*/
public function getSortedBlocksByRegion($region) {
if ($this->blocksInRegions === NULL) {
$this->sortBlocks();
}
if (!isset($this->blocksInRegions[$region])) {
throw new \Exception(sprintf("Region %region does not exist in layout %layout", array('%region' => $region, '%layout' => $this->getLayoutInstance()->name)), E_RECOVERABLE_ERROR);
}
return $this->blocksInRegions[$region];
}
/**
* Implements BoundDisplayInterface::getAllSortedBlocks().
*/
public function getAllSortedBlocks() {
if ($this->blocksInRegions === NULL) {
$this->sortBlocks();
}
return $this->blocksInRegions;
}
/**
* Transform the stored blockConfig into a sorted, region-oriented array.
*/
protected function sortBlocks() {
$layout_instance = $this->getLayoutInstance();
if ($this->layout !== $layout_instance->getPluginId()) {
$block_config = $this->mapBlocksToLayout($layout_instance);
}
else {
$block_config = $this->blockInfo;
}
$this->blocksInRegions = array();
$regions = array_fill_keys(array_keys($layout_instance->getRegions()), array());
foreach ($block_config as $config_name => $info) {
$regions[$info['region']][$config_name] = $info;
}
foreach ($regions as $region_name => &$blocks) {
uasort($blocks, 'drupal_sort_weight');
$this->blocksInRegions[$region_name] = array_keys($blocks);
}
}
/**
* Implements BoundDisplayInterface::remapToLayout().
*/
public function remapToLayout(LayoutInterface $layout) {
$this->blockInfo = $this->mapBlocksToLayout($layout);
$this->setLayout($layout->getPluginId());
}
/**
* Set the contained layout plugin.
*
* @param string $plugin_id
* The plugin id of the desired layout plugin.
*/
public function setLayout($plugin_id) {
// @todo verification?
$this->layout = $plugin_id;
$this->layoutInstance = NULL;
$this->blocksInRegions = NULL;
}
/**
* Implements BoundDisplayInterface::generateUnboundDisplay().
*
* @throws \Exception
*/
public function generateUnboundDisplay($id, $entity_type = 'unbound_display') {
$block_info = $this->getAllBlockInfo();
foreach ($block_info as &$info) {
unset($info['region']);
}
$values = array(
'blockInfo' => $block_info,
'id' => $id,
);
$entity = entity_create($entity_type, $values);
if (!$entity instanceof UnboundDisplayInterface) {
throw new \Exception(sprintf('Attempted to create an unbound display using an invalid entity type.'), E_RECOVERABLE_ERROR);
}
return $entity;
}
/**
* Returns the instantiated layout object.
*
* @throws \Exception
*/
public function getLayoutInstance() {
if ($this->layoutInstance === NULL) {
if (empty($this->layout)) {
throw new \Exception(sprintf('Display "%id" had no layout plugin attached.', array('%id' => $this->id())), E_RECOVERABLE_ERROR);
}
$this->layoutInstance = layout_manager()->createInstance($this->layout, $this->layoutSettings);
// @todo add handling for remapping if the layout could not be found
}
return $this->layoutInstance;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Definition of Drupal\layout\Plugin\Core\Entity\Display.
*/
namespace Drupal\layout\Plugin\Core\Entity;
use Drupal\layout\Config\DisplayBase;
use Drupal\layout\Config\BoundDisplayInterface;
use Drupal\layout\Config\UnboundDisplayInterface;
use Drupal\layout\Plugin\LayoutInterface;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines the unbound_display entity.
*
* Unbound displays contain blocks that are not 'bound' to a specific layout,
* and their contained blocks are mapped only to region types, not regions.
*
* @Plugin(
* id = "unbound_display",
* label = @Translation("Unbound Display"),
* module = "layout",
* controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
* config_prefix = "display.unbound",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
* }
* )
*/
class UnboundDisplay extends DisplayBase implements UnboundDisplayInterface {
/**
* Implements UnboundDisplayInterface::generateDisplay().
*
* @throws \Exception
*/
public function generateDisplay(LayoutInterface $layout, $id, $entity_type = 'display') {
$values = array(
'layout' => $layout->getPluginId(),
'blockInfo' => $this->mapBlocksToLayout($layout),
'id' => $id,
);
$entity = entity_create($entity_type, $values);
if (!$entity instanceof BoundDisplayInterface) {
throw new \Exception(sprintf('Attempted to bind an unbound display but provided an invalid entity type.'), E_RECOVERABLE_ERROR);
}
return $entity;
}
}

View File

@ -7,10 +7,12 @@
namespace Drupal\layout\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Defines the shared interface for all layout plugins.
*/
interface LayoutInterface {
interface LayoutInterface extends PluginInspectionInterface {
/**
* Returns a list of regions.

View File

@ -88,11 +88,11 @@ class StaticLayout extends PluginBase implements LayoutInterface {
);
// Render all regions needed for this layout.
foreach ($this->getRegions() as $region => $title) {
foreach ($this->getRegions() as $region => $info) {
// @todo This is just stub code to fill in regions with stuff for now.
// When blocks are related to layouts and not themes, we can make this
// really be filled in with blocks.
$build['#content'][$region] = '<h3>' . $title . '</h3>';
$build['#content'][$region] = '<h3>' . $info['label'] . '</h3>';
}
// Fill in attached CSS and JS files based on metadata.

View File

@ -0,0 +1,132 @@
<?php
/**
* @file
* Definition of \Drupal\layout\Tests\DisplayInternalLogicTest.
*/
namespace Drupal\layout\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\layout\Plugin\Core\Entity\Display;
use Drupal\layout\Plugin\Core\Entity\UnboundDisplay;
/**
* Tests the API and internal logic offered by Displays.
*/
class DisplayInternalLogicTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('layout', 'layout_test');
/**
* The twocol test display.
*
* @var \Drupal\layout\Plugin\Core\Entity\Display
*/
public $twocol;
/**
* The onecol test display.
*
* @var \Drupal\layout\Plugin\Core\Entity\Display
*/
public $onecol;
/**
* The unbound test display.
*
* @var \Drupal\layout\Plugin\Core\Entity\UnboundDisplay
*/
public $unbound;
public static function getInfo() {
return array(
'name' => 'Display behaviors',
'description' => 'Tests internal behaviors of DisplayInterface implementations, such as layout remapping.',
'group' => 'Display',
);
}
public function setUp() {
parent::setUp();
$this->twocol = entity_load('display', 'test_twocol');
$this->onecol = entity_load('display', 'test_onecol');
$this->unbound = entity_load('unbound_display', 'test_unbound_display');
}
/**
* Tests block sorting within regions.
*/
public function testBlockSorting() {
$expected = array(
'left' => array('block.test_block_3', 'block.test_block_1'),
'right' => array('block.test_block_2'),
);
$this->assertIdentical($this->twocol->getSortedBlocksByRegion('left'), $expected['left']);
$this->assertIdentical($this->twocol->getSortedBlocksByRegion('right'), $expected['right']);
$this->assertIdentical($this->twocol->getAllSortedBlocks(), $expected);
}
/**
* Test the various block remapping scenarios allowed for by the assorted
* Display types.
*
* This includes remapping a Display's blocks to a new layout, binding an
* UnboundDisplay with a layout to generate a new Display, and releasing a
* Display from its layout binding to generate an UnboundDisplay.
*/
public function testBlockMapping() {
// Remap from twocol to onecol. All blocks are expected to move to the one
// and only region and be sorted by their original weights.
$expected = array(
'middle' => array('block.test_block_3', 'block.test_block_2', 'block.test_block_1'),
);
$two_to_one = clone($this->twocol);
$two_to_one->remapToLayout($this->onecol->getLayoutInstance());
$this->assertIdentical($two_to_one->getAllSortedBlocks(), $expected);
// Remap from onecol to twocol. Since the blocks are assigned the 'content'
// region type, and twocol's 'left' region has that type, the blocks are
// expected to move to there and be sorted by their original weights.
$expected = array(
'left' => array('block.test_block_2', 'block.test_block_1'),
'right' => array(),
);
$one_to_two = clone($this->onecol);
$one_to_two->remapToLayout($this->twocol->getLayoutInstance());
$this->assertIdentical($one_to_two->getAllSortedBlocks(), $expected);
// Bind the unbound display to the twocol layout:
// - Block 1 is assigned the 'content' region type, so is expected to be
// mapped to the 'left' region, which has that type.
// - Block 2 is assigned the 'aside' region type, so is expected to be
// mapped to the 'right' region, which has that type.
// - Block 3 is assigned the 'nav' region type, and there is no twocol
// region with that type, so it is expected to be mapped to twocol's
// first region, which is 'left'.
$expected = array(
'left' => array('block.test_block_1', 'block.test_block_3'),
'right' => array('block.test_block_2'),
);
$unbound_to_twocol = $this->unbound->generateDisplay($this->twocol->getLayoutInstance(), 'unbound_to_twocol');
$this->assertTrue($unbound_to_twocol instanceof Display, 'Binding the unbound display successfully created a Display object');
$this->assertIdentical($unbound_to_twocol->getAllSortedBlocks(), $expected);
// Generate an unbound display from the twocol display.
$expected = array(
'block.test_block_1' => array('region-type' => 'content', 'weight' => 100),
'block.test_block_2' => array('region-type' => 'aside', 'weight' => 0),
'block.test_block_3' => array('region-type' => 'content', 'weight' => -100),
);
$twocol_to_unbound = $this->twocol->generateUnboundDisplay('twocol_to_unbound');
$this->assertTrue($twocol_to_unbound instanceof UnboundDisplay, 'Unbinding the twocol display successfully created an UnboundDisplay object');
// We can use Equal instead of Identical, because for this, array order and
// integer vs. string data types do not matter.
$this->assertEqual($twocol_to_unbound->getAllBlockInfo(), $expected);
}
}

View File

@ -0,0 +1,13 @@
id: test_onecol
label: Onecol testing display
layout: static_layout:layout_test__one-col
layoutSettings: { }
blockInfo:
block.test_block_1:
region: middle
region-type: content
weight: 100
block.test_block_2:
region: middle
region-type: content
weight: -100

View File

@ -0,0 +1,17 @@
id: test_twocol
label: Twocol testing display
layout: static_layout:layout_test_theme__two-col
layoutSettings: { }
blockInfo:
block.test_block_1:
region: left
region-type: content
weight: 100
block.test_block_2:
region: right
region-type: aside
weight: 0
block.test_block_3:
region: left
region-type: content
weight: -100

View File

@ -0,0 +1,13 @@
id: test_unbound_display
label: Unbound display test
layoutSettings: { }
blockInfo:
block.test_block_1:
region-type: content
weight: -100
block.test_block_2:
region-type: aside
weight: -100
block.test_block_3:
region-type: nav
weight: 0

View File

@ -27,7 +27,10 @@ function layout_test_page() {
global $theme;
$theme = 'layout_test_theme';
theme_enable(array($theme));
$layout = layout_manager()->createInstance('static_layout:layout_test_theme__two-col');
$display = entity_load('display', 'test_twocol');
$layout = $display->getLayoutInstance();
// @todo This tests that the layout can render its regions, but does not test
// block rendering: http://drupal.org/node/1812720.
return $layout->renderLayout();
}

View File

@ -2,4 +2,6 @@ title: Single column
category: Columns: 1
template: one-col
regions:
middle: 'Middle column'
middle:
label: Middle column
type: content

View File

@ -4,5 +4,9 @@ template: two-col
stylesheets:
- two-col.css
regions:
left: 'Left side'
right: 'Right side'
left:
label: Left side
type: content
right:
label: Right side
type: aside