Commit 88ad0137 authored by xjm's avatar xjm
Browse files

Issue #3016473 by tim.plunkett, xjm, tedbow, phenaproxima, EclipseGc,...

Issue #3016473 by tim.plunkett, xjm, tedbow, phenaproxima, EclipseGc, larowlan, samuel.mortenson: Allow a single SectionStorage plugin to be returned for a set of available contexts
parent 5a929b6a
......@@ -221,3 +221,16 @@ function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions,
}
}
}
/**
* Implements hook_layout_builder_section_storage_alter().
*/
function layout_builder_layout_builder_section_storage_alter(array &$definitions) {
// @todo Until https://www.drupal.org/node/3016420 is resolved, context
// definition annotations cannot specify any constraints. Alter
// \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage to
// add the constraint of having the required layout field.
/** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $definitions */
$definitions['overrides']->getContextDefinition('entity')
->addConstraint('EntityHasField', OverridesSectionStorage::FIELD_NAME);
}
......@@ -58,3 +58,10 @@ function layout_builder_post_update_add_extra_fields(&$sandbox = NULL) {
return $result;
});
}
/**
* Clear caches due to changes to section storage annotation changes.
*/
function layout_builder_post_update_section_storage_context_definitions() {
// Empty post-update hook.
}
......@@ -13,6 +13,7 @@ services:
plugin.manager.layout_builder.section_storage:
class: Drupal\layout_builder\SectionStorage\SectionStorageManager
parent: default_plugin_manager
arguments: ['@context.handler']
layout_builder.routes:
class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
arguments: ['@plugin.manager.layout_builder.section_storage']
......
......@@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
*/
public $id;
/**
* The plugin weight, optional (defaults to 0).
*
* When an entity with layout is rendered, section storage plugins are
* checked, in order of their weight, to determine which one should be used
* to render the layout.
*
* @var int
*/
public $weight = 0;
/**
* Any required context definitions, optional.
*
* When an entity with layout is rendered, all section storage plugins which
* match a particular set of contexts are checked, in order of their weight,
* to determine which plugin should be used to render the layout.
*
* @var array
*
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
*/
public $context_definitions = [];
/**
* {@inheritdoc}
*/
......
......@@ -46,7 +46,7 @@ protected function getAvailableContexts(SectionStorageInterface $section_storage
});
// Add in the per-section_storage contexts.
$contexts += $section_storage->getContexts();
$contexts += $section_storage->getContextsDuringPreview();
return $contexts;
}
......
......@@ -15,8 +15,6 @@
use Drupal\field_ui\FieldUI;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\SectionListInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
......@@ -25,6 +23,9 @@
*
* @SectionStorage(
* id = "defaults",
* context_definitions = {
* "display" = @ContextDefinition("entity:entity_view_display"),
* },
* )
*
* @internal
......@@ -90,12 +91,8 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
public function setSectionList(SectionListInterface $section_list) {
if (!$section_list instanceof LayoutEntityDisplayInterface) {
throw new \InvalidArgumentException('Defaults expect a display-based section list');
}
return parent::setSectionList($section_list);
protected function getSectionList() {
return $this->getContextValue('display');
}
/**
......@@ -238,6 +235,7 @@ protected function getEntityTypes() {
* {@inheritdoc}
*/
public function extractIdFromRoute($value, $definition, $name, array $defaults) {
@trigger_error('\Drupal\layout_builder\SectionStorageInterface::extractIdFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() should be used instead. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
if (is_string($value) && strpos($value, '.') !== FALSE) {
return $value;
}
......@@ -257,6 +255,7 @@ public function extractIdFromRoute($value, $definition, $name, array $defaults)
* {@inheritdoc}
*/
public function getSectionListFromId($id) {
@trigger_error('\Drupal\layout_builder\SectionStorageInterface::getSectionListFromId() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
if (strpos($id, '.') === FALSE) {
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
}
......@@ -278,15 +277,76 @@ public function getSectionListFromId($id) {
/**
* {@inheritdoc}
*/
public function getContexts() {
public function getContextsDuringPreview() {
$contexts = parent::getContextsDuringPreview();
// During preview add a sample entity for the target entity type and bundle.
$display = $this->getDisplay();
$entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle());
$contexts = [];
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
return $contexts;
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
$contexts = [];
if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
$contexts['display'] = EntityContext::fromEntity($entity);
}
return $contexts;
}
/**
* Extracts an entity from the route values.
*
* @param mixed $value
* The raw value from the route.
* @param array $defaults
* The route defaults array.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity for the route, or NULL if none exist.
*
* @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute()
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
*/
private function extractEntityFromRoute($value, array $defaults) {
// If a bundle is not provided but a value corresponding to the bundle key
// is, use that for the bundle value.
if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) {
$defaults['bundle'] = $defaults[$defaults['bundle_key']];
}
if (is_string($value) && strpos($value, '.') !== FALSE) {
list($entity_type_id, $bundle, $view_mode) = explode('.', $value, 3);
}
elseif (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) {
$entity_type_id = $defaults['entity_type_id'];
$bundle = $defaults['bundle'];
$view_mode = $defaults['view_mode_name'];
$value = "$entity_type_id.$bundle.$view_mode";
}
else {
return NULL;
}
$storage = $this->entityTypeManager->getStorage('entity_view_display');
// If the display does not exist, create a new one.
if (!$display = $storage->load($value)) {
$display = $storage->create([
'targetEntityType' => $entity_type_id,
'bundle' => $bundle,
'mode' => $view_mode,
'status' => TRUE,
]);
}
return $display;
}
/**
* {@inheritdoc}
*/
......
......@@ -7,14 +7,12 @@
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionListInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
......@@ -23,6 +21,10 @@
*
* @SectionStorage(
* id = "overrides",
* context_definitions = {
* "entity" = @ContextDefinition("entity"),
* "view_mode" = @ContextDefinition("string", required = FALSE),
* }
* )
*
* @internal
......@@ -86,12 +88,8 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
public function setSectionList(SectionListInterface $section_list) {
if (!$section_list instanceof FieldItemListInterface) {
throw new \InvalidArgumentException('Overrides expect a field-based section list');
}
return parent::setSectionList($section_list);
protected function getSectionList() {
return $this->getEntity()->get(static::FIELD_NAME);
}
/**
......@@ -101,7 +99,7 @@ public function setSectionList(SectionListInterface $section_list) {
* The entity storing the overrides.
*/
protected function getEntity() {
return $this->getSectionList()->getEntity();
return $this->getContextValue('entity');
}
/**
......@@ -116,6 +114,7 @@ public function getStorageId() {
* {@inheritdoc}
*/
public function extractIdFromRoute($value, $definition, $name, array $defaults) {
@trigger_error('\Drupal\layout_builder\SectionStorageInterface::extractIdFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() should be used instead. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
if (strpos($value, '.') !== FALSE) {
return $value;
}
......@@ -131,6 +130,7 @@ public function extractIdFromRoute($value, $definition, $name, array $defaults)
* {@inheritdoc}
*/
public function getSectionListFromId($id) {
@trigger_error('\Drupal\layout_builder\SectionStorageInterface::getSectionListFromId() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
if (strpos($id, '.') !== FALSE) {
list($entity_type_id, $entity_id) = explode('.', $id, 2);
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
......@@ -141,6 +141,50 @@ public function getSectionListFromId($id) {
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
$contexts = [];
if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
$contexts['entity'] = EntityContext::fromEntity($entity);
}
return $contexts;
}
/**
* Extracts an entity from the route values.
*
* @param mixed $value
* The raw value from the route.
* @param array $defaults
* The route defaults array.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity for the route, or NULL if none exist.
*
* @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute()
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
*/
private function extractEntityFromRoute($value, array $defaults) {
if (strpos($value, '.') !== FALSE) {
list($entity_type_id, $entity_id) = explode('.', $value, 2);
}
elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) {
$entity_type_id = $defaults['entity_type_id'];
$entity_id = $defaults[$entity_type_id];
}
else {
return NULL;
}
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
return $entity;
}
}
/**
* {@inheritdoc}
*/
......@@ -257,9 +301,14 @@ public function getLayoutBuilderUrl($rel = 'view') {
/**
* {@inheritdoc}
*/
public function getContexts() {
$entity = $this->getEntity();
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
public function getContextsDuringPreview() {
$contexts = parent::getContextsDuringPreview();
// @todo Remove this in https://www.drupal.org/node/3018782.
if (isset($contexts['entity'])) {
$contexts['layout_builder.entity'] = $contexts['entity'];
unset($contexts['entity']);
}
return $contexts;
}
......
......@@ -2,7 +2,7 @@
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListInterface;
......@@ -16,23 +16,29 @@
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
abstract class SectionStorageBase extends PluginBase implements SectionStorageInterface {
abstract class SectionStorageBase extends ContextAwarePluginBase implements SectionStorageInterface {
use LayoutBuilderRoutesTrait;
/**
* The section storage instance.
* Sets the section list on the storage.
*
* @var \Drupal\layout_builder\SectionListInterface|null
*/
protected $sectionList;
/**
* {@inheritdoc}
* @param \Drupal\layout_builder\SectionListInterface $section_list
* The section list.
*
* @internal
* As of Drupal 8.7.0, this method should no longer be used. It previously
* should only have been used during storage instantiation.
*
* @throws \Exception
*
* @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. This
* method should no longer be used. The section list should be derived from
* context. See https://www.drupal.org/node/3016262.
*/
public function setSectionList(SectionListInterface $section_list) {
$this->sectionList = $section_list;
return $this;
@trigger_error('\Drupal\layout_builder\SectionStorageInterface::setSectionList() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. This method should no longer be used. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
throw new \Exception('\Drupal\layout_builder\SectionStorageInterface::setSectionList() must no longer be called. The section list should be derived from context. See https://www.drupal.org/node/3016262.');
}
/**
......@@ -40,16 +46,8 @@ public function setSectionList(SectionListInterface $section_list) {
*
* @return \Drupal\layout_builder\SectionListInterface
* The section list.
*
* @throws \RuntimeException
* Thrown if ::setSectionList() is not called first.
*/
protected function getSectionList() {
if (!$this->sectionList) {
throw new \RuntimeException(sprintf('%s::setSectionList() must be called first', static::class));
}
return $this->sectionList;
}
abstract protected function getSectionList();
/**
* {@inheritdoc}
......@@ -103,4 +101,11 @@ public function removeSection($delta) {
return $this;
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview() {
return $this->getContexts();
}
}
......@@ -45,11 +45,18 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
if (isset($defaults['section_storage_type']) && $this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) {
if ($section_storage = $this->sectionStorageManager->loadFromRoute($defaults['section_storage_type'], $value, $definition, $name, $defaults)) {
// Pass the plugin through the tempstore repository.
return $this->layoutTempstoreRepository->get($section_storage);
}
// If no section storage type is specified or if it is invalid, return.
if (!isset($defaults['section_storage_type']) || !$this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) {
return NULL;
}
$type = $defaults['section_storage_type'];
// Load an empty instance and derive the available contexts.
$contexts = $this->sectionStorageManager->loadEmpty($type)->deriveContextsFromRoute($value, $definition, $name, $defaults);
// Attempt to load a full instance based on the context.
if ($section_storage = $this->sectionStorageManager->load($type, $contexts)) {
// Pass the plugin through the tempstore repository.
return $this->layoutTempstoreRepository->get($section_storage);
}
}
......
......@@ -2,6 +2,8 @@
namespace Drupal\layout_builder\SectionStorage;
use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionInterface;
use Drupal\Component\Plugin\Definition\ContextAwarePluginDefinitionTrait;
use Drupal\Component\Plugin\Definition\PluginDefinition;
/**
......@@ -12,7 +14,16 @@
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class SectionStorageDefinition extends PluginDefinition {
class SectionStorageDefinition extends PluginDefinition implements ContextAwarePluginDefinitionInterface {
use ContextAwarePluginDefinitionTrait;
/**
* The plugin weight.
*
* @var int
*/
protected $weight = 0;
/**
* Any additional properties and values.
......@@ -28,6 +39,16 @@ class SectionStorageDefinition extends PluginDefinition {
* An array of values from the annotation.
*/
public function __construct(array $definition = []) {
// If there are context definitions in the plugin definition, they should
// be added to this object using ::addContextDefinition() so that they can
// be manipulated using other ContextAwarePluginDefinitionInterface methods.
if (isset($definition['context_definitions'])) {
foreach ($definition['context_definitions'] as $name => $context_definition) {
$this->addContextDefinition($name, $context_definition);
}
unset($definition['context_definitions']);
}
foreach ($definition as $property => $value) {
$this->set($property, $value);
}
......@@ -72,4 +93,14 @@ public function set($property, $value) {
return $this;
}
/**
* Returns the plugin weight.
*
* @return int
* The plugin weight.
*/
public function getWeight() {
return $this->weight;
}
}
......@@ -2,8 +2,10 @@
namespace Drupal\layout_builder\SectionStorage;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\layout_builder\Annotation\SectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
......@@ -11,6 +13,12 @@
/**
* Provides the Section Storage type plugin manager.
*
* Note that while this class extends \Drupal\Core\Plugin\DefaultPluginManager
* and includes many additional public methods, only some of them are available
* via \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface.
* While internally depending on the parent class is necessary, external code
* should only use the methods available on that interface.
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
......@@ -18,6 +26,13 @@
*/
class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface {
/**
* The context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* Constructs a new SectionStorageManager object.
*
......@@ -28,14 +43,67 @@ class SectionStorageManager extends DefaultPluginManager implements SectionStora
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
* The context handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ContextHandlerInterface $context_handler = NULL) {
parent::__construct('Plugin/SectionStorage', $namespaces, $module_handler, SectionStorageInterface::class, SectionStorage::class);
if (!$context_handler) {
@trigger_error('The context.handler service must be passed to \Drupal\layout_builder\SectionStorage\SectionStorageManager::__construct(); it was added in Drupal 8.7.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
$context_handler = \Drupal::service('context.handler');
}
$this->contextHandler = $context_handler;
$this->alterInfo('layout_builder_section_storage');
$this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins');
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
// Sort the definitions by their weight while preserving the original order
// for those with matching weights.
$weights = array_map(function (SectionStorageDefinition $definition) {
return $definition->getWeight();
}, $definitions);
$ids = array_keys($definitions);
array_multisort($weights, $ids, $definitions);
return $definitions;
}
/**
* {@inheritdoc}
*/
public function load($type, array $contexts = []) {
$plugin = $this->loadEmpty($type);
try {
$this->contextHandler->applyContextMapping($plugin, $contexts);
}
catch (ContextException $e) {
return NULL;
}
return $plugin;