diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 48f62e98cf..dbb19bd010 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -202,3 +202,15 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation } return AccessResult::forbidden(); } + +/** + * Implements hook_layout_builder_section_storage_alter(). + */ +function layout_builder_layout_builder_section_storage_alter(array &$definitions) { + // The \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage + // plugin defines the entity context via its annotation but cannot specify the + // constraint inline. + /** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $definitions */ + $definitions['overrides']->getContextDefinition('entity') + ->addConstraint('EntityHasField', 'layout_builder__layout'); +} diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 56b4bf88bd..f0c517d995 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -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'] diff --git a/core/modules/layout_builder/src/Annotation/SectionStorage.php b/core/modules/layout_builder/src/Annotation/SectionStorage.php index 42f4a47fe6..9a52cff5da 100644 --- a/core/modules/layout_builder/src/Annotation/SectionStorage.php +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php @@ -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 = []; + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 0118768f77..dd52fdda3e 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -5,6 +5,8 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\EntityContext; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\field\Entity\FieldConfig; @@ -274,11 +276,21 @@ public function buildMultiple(array $entities) { * The sections. */ protected function getRuntimeSections(FieldableEntityInterface $entity) { - if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) { - return $entity->get('layout_builder__layout')->getSections(); - } + $sections = NULL; - return $this->getSections(); + if ($this->isOverridable()) { + /** @var \Drupal\layout_builder\SectionStorageInterface $storage */ + $storage = \Drupal::service('plugin.manager.layout_builder.section_storage') + ->findByContext('view', [ + 'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()), + 'entity' => EntityContext::fromEntity($entity), + ]); + $sections = $storage ? $storage->getSections() : []; + } + // If we don't have a section list yet (i.e., no section storage plugin + // was able to derive a section list from context, or this display is not + // overridable), use this display as the section list. + return $sections ?: $this->getSections(); } /** diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php index d35041d03d..06e5a2036e 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -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 = { + * "entity" = @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('entity'); } /** @@ -237,34 +234,49 @@ protected function getEntityTypes() { /** * {@inheritdoc} */ - public function extractIdFromRoute($value, $definition, $name, array $defaults) { - if (is_string($value) && strpos($value, '.') !== FALSE) { - return $value; + public function getContextsFromRoute($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. + */ + protected 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 (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) { - return $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name']; + if (is_string($value) && strpos($value, '.') !== FALSE) { + list($entity_type_id, $bundle, $view_mode) = explode('.', $value, 3); } - } - - /** - * {@inheritdoc} - */ - public function getSectionListFromId($id) { - if (strpos($id, '.') === FALSE) { - throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); + 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($id)) { - list($entity_type_id, $bundle, $view_mode) = explode('.', $id, 3); + if (!$display = $storage->load($value)) { $display = $storage->create([ 'targetEntityType' => $entity_type_id, 'bundle' => $bundle, diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index c623c5ede3..b1db3c5a36 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -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 = { + * "entity" = @ContextDefinition("entity"), + * "view_mode" = @ContextDefinition("string", required = FALSE), + * } * ) * * @internal @@ -79,12 +81,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('layout_builder__layout'); } /** @@ -94,7 +92,7 @@ public function setSectionList(SectionListInterface $section_list) { * The entity storing the overrides. */ protected function getEntity() { - return $this->getSectionList()->getEntity(); + return $this->getContextValue('entity'); } /** @@ -108,30 +106,42 @@ public function getStorageId() { /** * {@inheritdoc} */ - public function extractIdFromRoute($value, $definition, $name, array $defaults) { + public function getContextsFromRoute($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. + */ + protected function extractEntityFromRoute($value, array $defaults) { if (strpos($value, '.') !== FALSE) { - return $value; + list($entity_type_id, $entity_id) = explode('.', $value, 2); } - - if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { + 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]; - return $entity_type_id . '.' . $entity_id; } - } + else { + return NULL; + } - /** - * {@inheritdoc} - */ - public function getSectionListFromId($id) { - if (strpos($id, '.') !== FALSE) { - list($entity_type_id, $entity_id) = explode('.', $id, 2); - $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id); - if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { - return $entity->get('layout_builder__layout'); - } + $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id); + if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { + return $entity; } - throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); } /** diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php index c419060c45..8e8178355b 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php @@ -2,10 +2,9 @@ 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; use Drupal\layout_builder\SectionStorageInterface; /** @@ -16,40 +15,17 @@ * 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. - * - * @var \Drupal\layout_builder\SectionListInterface|null - */ - protected $sectionList; - - /** - * {@inheritdoc} - */ - public function setSectionList(SectionListInterface $section_list) { - $this->sectionList = $section_list; - return $this; - } - /** * Gets the 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 +79,22 @@ public function removeSection($delta) { return $this; } + /** + * {@inheritdoc} + * + * @todo Remove after https://www.drupal.org/project/drupal/issues/2982626. + */ + public function getContextDefinition($name) { + return $this->getPluginDefinition()->getContextDefinition($name); + } + + /** + * {@inheritdoc} + * + * @todo Remove after https://www.drupal.org/project/drupal/issues/2982626. + */ + public function getContextDefinitions() { + return $this->getPluginDefinition()->getContextDefinitions(); + } + } diff --git a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php index 263b767f72..e5976ca0bd 100644 --- a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php +++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php @@ -45,11 +45,17 @@ 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; + } + + // Load an empty instance and derive the available contexts. + $contexts = $this->sectionStorageManager->loadEmpty($defaults['section_storage_type'])->getContextsFromRoute($value, $definition, $name, $defaults); + // Attempt to load a full instance based on the context. + if ($section_storage = $this->sectionStorageManager->load($defaults['section_storage_type'], $contexts)) { + // Pass the plugin through the tempstore repository. + return $this->layoutTempstoreRepository->get($section_storage); } } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php index 61b975a471..267a499aff 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php @@ -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'])) { + foreach ($definition['context'] as $name => $context_definition) { + $this->addContextDefinition($name, $context_definition); + } + unset($definition['context']); + } + 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; + } + } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php index 18147cd1d8..eaa3082e21 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php @@ -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; @@ -18,6 +20,13 @@ */ class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface { + /** + * The context handler. + * + * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected $contextHandler; + /** * Constructs a new SectionStorageManager object. * @@ -28,10 +37,14 @@ 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) { parent::__construct('Plugin/SectionStorage', $namespaces, $module_handler, SectionStorageInterface::class, SectionStorage::class); + $this->contextHandler = $context_handler; + $this->alterInfo('layout_builder_section_storage'); $this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins'); } @@ -39,33 +52,53 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac /** * {@inheritdoc} */ - public function loadEmpty($id) { - return $this->createInstance($id); + 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 loadFromStorageId($type, $id) { - /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ - $plugin = $this->createInstance($type); - return $plugin->setSectionList($plugin->getSectionListFromId($id)); + public function load($type, array $contexts = []) { + $plugin = $this->loadEmpty($type); + try { + $this->contextHandler->applyContextMapping($plugin, $contexts); + } + catch (ContextException $e) { + return NULL; + } + return $plugin; } /** * {@inheritdoc} */ - public function loadFromRoute($type, $value, $definition, $name, array $defaults) { - /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ - $plugin = $this->createInstance($type); - if ($id = $plugin->extractIdFromRoute($value, $definition, $name, $defaults)) { - try { - return $plugin->setSectionList($plugin->getSectionListFromId($id)); - } - catch (\InvalidArgumentException $e) { - // Intentionally empty. + public function findByContext($operation, array $contexts) { + $storage_types = array_keys($this->contextHandler->filterPluginDefinitionsByContexts($contexts, $this->getDefinitions())); + + foreach ($storage_types as $type) { + $plugin = $this->load($type, $contexts); + if ($plugin && $plugin->access($operation)) { + return $plugin; } } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function loadEmpty($type) { + return $this->createInstance($type); } } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php index 3b269fcbba..63d8b4ad5e 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php @@ -7,6 +7,11 @@ /** * Provides the interface for a plugin manager of section storage types. * + * Note that this interface purposefully does not implement + * \Drupal\Component\Plugin\PluginManagerInterface, as the below methods exist + * to serve the use case of \Drupal\Component\Plugin\Factory\FactoryInterface + * and \Drupal\Component\Plugin\Mapper\MapperInterface. + * * @internal * Layout Builder is currently experimental and should only be leveraged by * experimental modules and development releases of contributed modules. @@ -15,51 +20,46 @@ interface SectionStorageManagerInterface extends DiscoveryInterface { /** - * Loads a section storage with no associated section list. + * Loads a section storage with the provided contexts applied. * - * @param string $id - * The ID of the section storage being instantiated. + * @param string $type + * The section storage type. + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * (optional) The contexts available for this storage to use. * - * @return \Drupal\layout_builder\SectionStorageInterface - * The section storage. + * @return \Drupal\layout_builder\SectionStorageInterface|null + * The section storage or NULL if its context requirements are not met. */ - public function loadEmpty($id); + public function load($type, array $contexts = []); /** - * Loads a section storage populated with an existing section list. + * Finds the section storage to load based on available contexts. * - * @param string $type - * The section storage type. - * @param string $id - * The section list ID. + * @param string $operation + * The access operation. See \Drupal\Core\Access\AccessibleInterface. + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * The contexts which should be used to determine which storage to return. * - * @return \Drupal\layout_builder\SectionStorageInterface - * The section storage. - * - * @throws \InvalidArgumentException - * Thrown if the ID is invalid. + * @return \Drupal\layout_builder\SectionStorageInterface|null + * The section storage if one matched all contexts, or NULL otherwise. */ - public function loadFromStorageId($type, $id); + public function findByContext($operation, array $contexts); /** - * Loads a section storage populated with a section list derived from a route. + * Loads an empty section storage with no associated section list. * * @param string $type - * The section storage type. - * @param string $value - * The raw value. - * @param mixed $definition - * The parameter definition provided in the route options. - * @param string $name - * The name of the parameter. - * @param array $defaults - * The route defaults array. + * The type of section storage being instantiated. * - * @return \Drupal\layout_builder\SectionStorageInterface|null - * The section storage if it could be loaded, or NULL otherwise. + * @return \Drupal\layout_builder\SectionStorageInterface + * The section storage. * - * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert() + * @internal + * Section storage relies on context to load section lists. Use ::load() + * when that context is available. This method is intended for use by + * collaborators of the plugins in build-time situations when section + * storage type must be consulted. */ - public function loadFromRoute($type, $value, $definition, $name, array $defaults); + public function loadEmpty($type); } diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index 90ce9072fd..28cc2f915c 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -4,6 +4,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Access\AccessibleInterface; +use Drupal\Core\Plugin\ContextAwarePluginInterface; use Symfony\Component\Routing\RouteCollection; /** @@ -14,7 +15,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface, AccessibleInterface { +interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface, ContextAwarePluginInterface, AccessibleInterface { /** * Returns an identifier for this storage. @@ -34,36 +35,6 @@ public function getStorageId(); */ public function getStorageType(); - /** - * Sets the section list on the storage. - * - * @param \Drupal\layout_builder\SectionListInterface $section_list - * The section list. - * - * @return $this - * - * @internal - * This should only be called during section storage instantiation. - */ - public function setSectionList(SectionListInterface $section_list); - - /** - * Derives the section list from the storage ID. - * - * @param string $id - * The storage ID, see ::getStorageId(). - * - * @return \Drupal\layout_builder\SectionListInterface - * The section list. - * - * @throws \InvalidArgumentException - * Thrown if the ID is invalid. - * - * @internal - * This should only be called during section storage instantiation. - */ - public function getSectionListFromId($id); - /** * Provides the routes needed for Layout Builder UI. * @@ -79,27 +50,7 @@ public function getSectionListFromId($id); public function buildRoutes(RouteCollection $collection); /** - * Gets the URL used when redirecting away from the Layout Builder UI. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getRedirectUrl(); - - /** - * Gets the URL used to display the Layout Builder UI. - * - * @param string $rel - * (optional) The link relationship type, for example: 'view' or 'disable'. - * Defaults to 'view'. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getLayoutBuilderUrl($rel = 'view'); - - /** - * Configures the plugin based on route values. + * Derives the required plugin contexts from route values. * * @param mixed $value * The raw value. @@ -110,21 +61,30 @@ public function getLayoutBuilderUrl($rel = 'view'); * @param array $defaults * The route defaults array. * - * @return string|null - * The section storage ID if it could be extracted, NULL otherwise. - * - * @internal - * This should only be called during section storage instantiation. - */ - public function extractIdFromRoute($value, $definition, $name, array $defaults); - - /** - * Provides any available contexts for the object using the sections. - * * @return \Drupal\Core\Plugin\Context\ContextInterface[] - * The array of context objects. + * The required plugin contexts. */ - public function getContexts(); + public function getContextsFromRoute($value, $definition, $name, array $defaults); + + /** + * Gets the URL used when redirecting away from the Layout Builder UI. + * + * @return \Drupal\Core\Url + * The URL object. + */ + public function getRedirectUrl(); + + /** + * Gets the URL used to display the Layout Builder UI. + * + * @param string $rel + * (optional) The link relationship type, for example: 'view' or 'disable'. + * Defaults to 'view'. + * + * @return \Drupal\Core\Url + * The URL object. + */ + public function getLayoutBuilderUrl($rel = 'view'); /** * Gets the label for the object using the sections. diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/config/schema/layout_builder_test.schema.yml b/core/modules/layout_builder/tests/modules/layout_builder_test/config/schema/layout_builder_test.schema.yml new file mode 100644 index 0000000000..75050837fa --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/config/schema/layout_builder_test.schema.yml @@ -0,0 +1,7 @@ +layout_builder_test.test_simple_config.*: + type: config_object + mapping: + sections: + type: sequence + sequence: + type: layout_builder.section diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module index e7d310a6cb..8e9991f8cd 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module @@ -7,6 +7,7 @@ use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\layout_builder_test\Plugin\SectionStorage\TestOverridesSectionStorage; /** * Implements hook_plugin_filter_TYPE__CONSUMER_alter(). @@ -58,3 +59,15 @@ function layout_builder_test_node_view(array &$build, EntityInterface $entity, E ]; } } + +/** + * Implements hook_layout_builder_section_storage_alter(). + */ +function layout_builder_test_layout_builder_section_storage_alter(array &$storages) { + /** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $storages */ + $storages['overrides']->setClass(TestOverridesSectionStorage::class); + $storages['overrides']->set('weight', -10); + + $storages['overrides_heavy'] = clone $storages['overrides']; + $storages['overrides_heavy']->set('weight', -8); +} diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php new file mode 100644 index 0000000000..9de5f2561a --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php @@ -0,0 +1,211 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function getStorageType() { + return $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function getStorageId() { + return $this->getContextValue('config_id'); + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->getStorageId(); + } + + /** + * Returns the name to be used to store in the config system. + */ + protected function getConfigName() { + return 'layout_builder_test.' . $this->getStorageType() . '.' . $this->getStorageId(); + } + + /** + * {@inheritdoc} + */ + public function getSections() { + if (is_null($this->sections)) { + $sections = $this->configFactory->get($this->getConfigName())->get('sections') ?: []; + $this->setSections(array_map([Section::class, 'fromArray'], $sections)); + } + return $this->sections; + } + + /** + * {@inheritdoc} + */ + protected function setSections(array $sections) { + $this->sections = array_values($sections); + return $this; + } + + /** + * {@inheritdoc} + */ + public function save() { + $sections = array_map(function (Section $section) { + return $section->toArray(); + }, $this->getSections()); + + $config = $this->configFactory->getEditable($this->getConfigName()); + $return = $config->get('sections') ? SAVED_UPDATED : SAVED_NEW; + $config->set('sections', $sections)->save(); + return $return; + } + + /** + * {@inheritdoc} + */ + public function buildRoutes(RouteCollection $collection) { + $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), 'layout-builder-test-simple-config/{id}'); + } + + /** + * {@inheritdoc} + */ + public function getContextsFromRoute($value, $definition, $name, array $defaults) { + $contexts['config_id'] = new Context(new ContextDefinition('string'), $value ?: $defaults['id']); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function buildLocalTasks($base_plugin_definition) { + $type = $this->getStorageType(); + $local_tasks = []; + $local_tasks["layout_builder.$type.view"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.$type.view", + 'title' => $this->t('Layout'), + 'base_route' => "layout_builder.$type.view", + ]; + $local_tasks["layout_builder.$type.save"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.$type.save", + 'title' => $this->t('Save Layout'), + 'parent_id' => "layout_builder_ui:layout_builder.$type.view", + ]; + $local_tasks["layout_builder.$type.cancel"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.$type.cancel", + 'title' => $this->t('Cancel Layout'), + 'parent_id' => "layout_builder_ui:layout_builder.$type.view", + 'weight' => 5, + ]; + return $local_tasks; + } + + /** + * {@inheritdoc} + */ + public function getLayoutBuilderUrl($rel = 'view') { + return Url::fromRoute("layout_builder.{$this->getStorageType()}.$rel", ['id' => $this->getStorageId()]); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + return $this->getLayoutBuilderUrl(); + } + + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + * + * @todo Remove after https://www.drupal.org/project/drupal/issues/2982626. + */ + public function getContextDefinition($name) { + return $this->getPluginDefinition()->getContextDefinition($name); + } + + /** + * {@inheritdoc} + * + * @todo Remove after https://www.drupal.org/project/drupal/issues/2982626. + */ + public function getContextDefinitions() { + return $this->getPluginDefinition()->getContextDefinitions(); + } + +} diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/TestOverridesSectionStorage.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/TestOverridesSectionStorage.php new file mode 100644 index 0000000000..d4fd459343 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/TestOverridesSectionStorage.php @@ -0,0 +1,23 @@ +set('layout_builder_test_storage', [ + $this->getPluginDefinition()->get('weight'), + $this->getContextValue('view_mode'), + ]); + return parent::getSectionList(); + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 2d22829752..a53e3a1cef 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\layout_builder\Functional; +use Drupal\layout_builder\Section; use Drupal\node\Entity\Node; use Drupal\Tests\BrowserTestBase; use Drupal\views\Entity\View; @@ -557,4 +558,58 @@ public function testBlockPlaceholder() { $assert_session->pageTextContains($block_content); } + /** + * Tests that section loading is delegated to plugins during rendering. + */ + public function testRenderByContextAwarePluginDelegate() { + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + ])); + + $state_key = 'layout_builder_test_storage'; + /** @var \Drupal\Core\State\StateInterface $state */ + $state = $this->container->get('state'); + + $this->drupalGet('node/1'); + $this->assertEmpty($state->get($state_key)); + + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default'); + $this->drupalPostForm(NULL, ['layout[enabled]' => TRUE], 'Save'); + $this->drupalPostForm(NULL, ['layout[allow_custom]' => TRUE], 'Save'); + + $this->drupalGet('node/1'); + // During layout rendering, the storage plugin used for testing will set the + // state key to an array containing the plugin weight and view mode, which + // proves that the plugin matched the appropriate contexts and was actually + // used to render the layout. + list ($weight, $view_mode) = $state->get($state_key); + $this->assertSame(-10, $weight); + $this->assertSame('default', $view_mode); + } + + /** + * Tests that the test implementation of Layout Builder works as expected. + */ + public function testSimpleConfigBasedLayout() { + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->createUser(['configure any layout'])); + + // Prepare an object with a pre-existing section. + $this->container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.existing') + ->set('sections', [(new Section('layout_twocol'))->toArray()]) + ->save(); + + // The pre-existing section is found. + $this->drupalGet('layout-builder-test-simple-config/existing'); + $assert_session->elementsCount('css', '.layout', 1); + $assert_session->elementsCount('css', '.layout--twocol', 1); + + // The default layout is selected for a new object. + $this->drupalGet('layout-builder-test-simple-config/new'); + $assert_session->elementsCount('css', '.layout', 1); + $assert_session->elementsCount('css', '.layout--onecol', 1); + } + } diff --git a/core/modules/layout_builder/tests/src/Kernel/SimpleConfigSectionStorageTest.php b/core/modules/layout_builder/tests/src/Kernel/SimpleConfigSectionStorageTest.php new file mode 100644 index 0000000000..282bb2022c --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/SimpleConfigSectionStorageTest.php @@ -0,0 +1,43 @@ +container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.foobar'); + $section_data = array_map(function (Section $section) { + return $section->toArray(); + }, $section_data); + $config->set('sections', $section_data)->save(); + + $definition = new SectionStorageDefinition(['id' => 'test_simple_config']); + $plugin = SimpleConfigSectionStorage::create($this->container, [], 'test_simple_config', $definition); + $plugin->setContext('config_id', new Context(new ContextDefinition('string'), 'foobar')); + return $plugin; + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php index d30917a446..726d62dcab 100644 --- a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Plugin\Context\ContextInterface; use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator; use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; use Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage; @@ -61,8 +62,12 @@ protected function setUp() { public function testThirdPartySettings() { // Set an initial value on the section list. $section_list = $this->prophesize(LayoutEntityDisplayInterface::class); + + $context = $this->prophesize(ContextInterface::class); + $context->getContextValue()->willReturn($section_list->reveal()); + $this->plugin->setContext('entity', $context->reveal()); + $section_list->getThirdPartySetting('the_module', 'the_key', NULL)->willReturn('value 1'); - $this->plugin->setSectionList($section_list->reveal()); // The plugin returns the initial value. $this->assertSame('value 1', $this->plugin->getThirdPartySetting('the_module', 'the_key')); @@ -79,58 +84,11 @@ public function testThirdPartySettings() { } /** - * @covers ::extractIdFromRoute + * @covers ::extractEntityFromRoute * - * @dataProvider providerTestExtractIdFromRoute + * @dataProvider providerTestExtractEntityFromRoute */ - public function testExtractIdFromRoute($expected, $value, array $defaults) { - $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); - $this->assertSame($expected, $result); - } - - /** - * Provides data for ::testExtractIdFromRoute(). - */ - public function providerTestExtractIdFromRoute() { - $data = []; - $data['with value'] = [ - 'foo.bar.baz', - 'foo.bar.baz', - [], - ]; - $data['empty value, without bundle'] = [ - 'my_entity_type.bundle_name.default', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'view_mode_name' => 'default', - 'bundle_key' => 'my_bundle', - 'my_bundle' => 'bundle_name', - ], - ]; - $data['empty value, with bundle'] = [ - 'my_entity_type.bundle_name.default', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'view_mode_name' => 'default', - 'bundle' => 'bundle_name', - ], - ]; - $data['without value, empty defaults'] = [ - NULL, - '', - [], - ]; - return $data; - } - - /** - * @covers ::getSectionListFromId - * - * @dataProvider providerTestGetSectionListFromId - */ - public function testGetSectionListFromId($success, $expected_entity_id, $value) { + public function testExtractEntityFromRoute($success, $expected_entity_id, $value, array $defaults = []) { if ($expected_entity_id) { $entity_storage = $this->prophesize(EntityStorageInterface::class); $entity_storage->load($expected_entity_id)->willReturn('the_return_value'); @@ -143,26 +101,48 @@ public function testGetSectionListFromId($success, $expected_entity_id, $value) $this->entityTypeManager->getStorage('entity_view_display')->shouldNotBeCalled(); } - if (!$success) { - $this->setExpectedException(\InvalidArgumentException::class); - } - - $result = $this->plugin->getSectionListFromId($value); + $method = new \ReflectionMethod($this->plugin, 'extractEntityFromRoute'); + $method->setAccessible(TRUE); + $result = $method->invoke($this->plugin, $value, $defaults); if ($success) { $this->assertEquals('the_return_value', $result); } + else { + $this->assertNull($result); + } } /** - * Provides data for ::testGetSectionListFromId(). + * Provides data for ::testExtractEntityFromRoute(). */ - public function providerTestGetSectionListFromId() { + public function providerTestExtractEntityFromRoute() { $data = []; $data['with value'] = [ TRUE, 'foo.bar.baz', 'foo.bar.baz', ]; + $data['empty value, without bundle'] = [ + TRUE, + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle_key' => 'my_bundle', + 'my_bundle' => 'bundle_name', + ], + ]; + $data['empty value, with bundle'] = [ + TRUE, + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle' => 'bundle_name', + ], + ]; $data['without value, empty defaults'] = [ FALSE, NULL, @@ -172,9 +152,9 @@ public function providerTestGetSectionListFromId() { } /** - * @covers ::getSectionListFromId + * @covers ::extractEntityFromRoute */ - public function testGetSectionListFromIdCreate() { + public function testExtractEntityFromRouteCreate() { $expected = 'the_return_value'; $value = 'foo.bar.baz'; $expected_create_values = [ @@ -190,7 +170,9 @@ public function testGetSectionListFromIdCreate() { $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); - $result = $this->plugin->getSectionListFromId($value); + $method = new \ReflectionMethod($this->plugin, 'extractEntityFromRoute'); + $method->setAccessible(TRUE); + $result = $method->invoke($this->plugin, $value, []); $this->assertSame($expected, $result); } diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php index 5f8dc051e3..1091cd586f 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php @@ -32,9 +32,11 @@ public function testConvert() { $expected = 'the_return_value'; $section_storage_manager->hasDefinition('my_type')->willReturn(TRUE); - $section_storage_manager->loadFromRoute('my_type', $value, $definition, $name, $defaults)->willReturn($section_storage); + $section_storage_manager->loadEmpty('my_type')->willReturn($section_storage->reveal()); + $section_storage->getContextsFromRoute($value, $definition, $name, $defaults)->willReturn([]); + $section_storage_manager->load('my_type', [])->willReturn($section_storage->reveal()); - $layout_tempstore_repository->get($section_storage->reveal())->willReturn($expected); + $layout_tempstore_repository->get($section_storage)->willReturn($expected); $result = $converter->convert($value, $definition, $name, $defaults); $this->assertEquals($expected, $result); @@ -54,7 +56,7 @@ public function testConvertNoType() { $defaults = ['section_storage_type' => NULL]; $section_storage_manager->hasDefinition()->shouldNotBeCalled(); - $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); + $section_storage_manager->load()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); @@ -75,7 +77,7 @@ public function testConvertInvalidConverter() { $defaults = ['section_storage_type' => 'invalid']; $section_storage_manager->hasDefinition('invalid')->willReturn(FALSE); - $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); + $section_storage_manager->load()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php index 110ec2021e..d3574be6dd 100644 --- a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -60,87 +60,42 @@ protected function setUp() { } /** - * @covers ::extractIdFromRoute + * @covers ::extractEntityFromRoute * - * @dataProvider providerTestExtractIdFromRoute + * @dataProvider providerTestExtractEntityFromRoute */ - public function testExtractIdFromRoute($expected, $value, array $defaults) { - $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); - $this->assertSame($expected, $result); - } - - /** - * Provides data for ::testExtractIdFromRoute(). - */ - public function providerTestExtractIdFromRoute() { - $data = []; - $data['with value, with layout'] = [ - 'my_entity_type.entity_with_layout', - 'my_entity_type.entity_with_layout', - [], - ]; - $data['with value, without layout'] = [ - NULL, - 'my_entity_type', - [], - ]; - $data['empty value, populated defaults'] = [ - 'my_entity_type.entity_with_layout', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'my_entity_type' => 'entity_with_layout', - ], - ]; - $data['empty value, empty defaults'] = [ - NULL, - '', - [], - ]; - return $data; - } - - /** - * @covers ::getSectionListFromId - * - * @dataProvider providerTestGetSectionListFromId - */ - public function testGetSectionListFromId($success, $expected_entity_type_id, $id) { - $defaults['the_parameter_name'] = $id; - + public function testExtractEntityFromRoute($success, $expected_entity_type_id, $value, array $defaults = []) { if ($expected_entity_type_id) { $entity_storage = $this->prophesize(EntityStorageInterface::class); $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE); - $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled(); $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE); - $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value'); $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal()); - $this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal()); } else { $this->entityTypeManager->getStorage(Argument::any())->shouldNotBeCalled(); } - if (!$success) { - $this->setExpectedException(\InvalidArgumentException::class); - } - - $result = $this->plugin->getSectionListFromId($id); + $method = new \ReflectionMethod($this->plugin, 'extractEntityFromRoute'); + $method->setAccessible(TRUE); + $result = $method->invoke($this->plugin, $value, $defaults); if ($success) { - $this->assertEquals('the_return_value', $result); + $this->assertInstanceOf(FieldableEntityInterface::class, $result); + } + else { + $this->assertNull($result); } } /** - * Provides data for ::testGetSectionListFromId(). + * Provides data for ::testExtractEntityFromRoute(). */ - public function providerTestGetSectionListFromId() { + public function providerTestExtractEntityFromRoute() { $data = []; $data['with value, with layout'] = [ TRUE, @@ -152,6 +107,15 @@ public function providerTestGetSectionListFromId() { 'my_entity_type', 'my_entity_type.entity_without_layout', ]; + $data['empty value, populated defaults'] = [ + TRUE, + 'my_entity_type', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'my_entity_type' => 'entity_with_layout', + ], + ]; $data['empty value, empty defaults'] = [ FALSE, NULL, diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php index 392cd8939e..3a33ede44f 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php @@ -2,10 +2,16 @@ namespace Drupal\Tests\layout_builder\Unit; +use Drupal\Component\Plugin\Context\ContextInterface; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Factory\FactoryInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\layout_builder\SectionListInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\ContextHandlerInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; use Drupal\layout_builder\SectionStorage\SectionStorageManager; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; @@ -20,16 +26,30 @@ class SectionStorageManagerTest extends UnitTestCase { /** * The section storage manager. * - * @var \Drupal\layout_builder\SectionStorage\SectionStorageManager + * @var \Drupal\Tests\layout_builder\Unit\TestSectionStorageManager */ protected $manager; /** - * The plugin. + * The plugin discovery. * - * @var \Drupal\layout_builder\SectionStorageInterface + * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface */ - protected $plugin; + protected $discovery; + + /** + * The plugin factory. + * + * @var \Drupal\Component\Plugin\Factory\FactoryInterface + */ + protected $factory; + + /** + * The context handler. + * + * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected $contextHandler; /** * {@inheritdoc} @@ -37,64 +57,147 @@ class SectionStorageManagerTest extends UnitTestCase { protected function setUp() { parent::setUp(); + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $this->factory = $this->prophesize(FactoryInterface::class); $cache = $this->prophesize(CacheBackendInterface::class); $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); - - $this->plugin = $this->prophesize(SectionStorageInterface::class); - - $factory = $this->prophesize(FactoryInterface::class); - $factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal()); - $reflection_property = new \ReflectionProperty($this->manager, 'factory'); - $reflection_property->setAccessible(TRUE); - $reflection_property->setValue($this->manager, $factory->reveal()); + $this->contextHandler = $this->prophesize(ContextHandlerInterface::class); + $this->manager = new TestSectionStorageManager($this->discovery->reveal(), $this->factory->reveal(), $cache->reveal(), $module_handler->reveal(), $this->contextHandler->reveal()); } /** * @covers ::loadEmpty */ public function testLoadEmpty() { + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); + $result = $this->manager->loadEmpty('the_plugin_id'); - $this->assertInstanceOf(SectionStorageInterface::class, $result); + $this->assertSame($plugin->reveal(), $result); } /** - * @covers ::loadFromStorageId + * @covers ::load */ - public function testLoadFromStorageId() { - $section_list = $this->prophesize(SectionListInterface::class); - $this->plugin->setSectionList($section_list->reveal())->will(function () { - return $this; - }); - $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); - - $result = $this->manager->loadFromStorageId('the_plugin_id', 'the_storage_id'); - $this->assertInstanceOf(SectionStorageInterface::class, $result); - } + public function testLoad() { + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); - /** - * @covers ::loadFromRoute - */ - public function testLoadFromRoute() { - $section_list = $this->prophesize(SectionListInterface::class); - $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', [])->willReturn('the_storage_id'); - $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); - $this->plugin->setSectionList($section_list->reveal())->will(function () { - return $this; - }); - - $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []); - $this->assertInstanceOf(SectionStorageInterface::class, $result); + $contexts = [ + 'the_context' => $this->prophesize(ContextInterface::class)->reveal(), + ]; + + $this->contextHandler->applyContextMapping($plugin, $contexts)->shouldBeCalled(); + + $result = $this->manager->load('the_plugin_id', $contexts); + $this->assertSame($plugin->reveal(), $result); } /** - * @covers ::loadFromRoute + * @covers ::load */ - public function testLoadFromRouteNull() { - $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn(NULL); + public function testLoadNull() { + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); + + $contexts = [ + 'the_context' => $this->prophesize(ContextInterface::class)->reveal(), + ]; + + $this->contextHandler->applyContextMapping($plugin, $contexts)->willThrow(new ContextException()); - $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']); + $result = $this->manager->load('the_plugin_id', $contexts); $this->assertNull($result); } + /** + * @covers ::findDefinitions + */ + public function testFindDefinitions() { + $this->discovery->getDefinitions()->willReturn([ + 'plugin1' => new SectionStorageDefinition(), + 'plugin2' => new SectionStorageDefinition(['weight' => -5]), + 'plugin3' => new SectionStorageDefinition(['weight' => -5]), + 'plugin4' => new SectionStorageDefinition(['weight' => 10]), + ]); + + $expected = [ + 'plugin2', + 'plugin3', + 'plugin1', + 'plugin4', + ]; + $result = $this->manager->getDefinitions(); + $this->assertSame($expected, array_keys($result)); + } + + /** + * @covers ::findByContext + * + * @dataProvider providerTestFindByContext + */ + public function testFindByContext($access) { + $contexts = [ + 'foo' => new Context(new ContextDefinition('foo')), + ]; + $definitions = [ + 'no_access' => new SectionStorageDefinition(), + 'missing_contexts' => new SectionStorageDefinition(), + 'provider_access' => new SectionStorageDefinition(), + ]; + $this->discovery->getDefinitions()->willReturn($definitions); + + $provider_access = $this->prophesize(SectionStorageInterface::class); + $provider_access->access('test_operation')->willReturn($access); + + $no_access = $this->prophesize(SectionStorageInterface::class); + $no_access->access('test_operation')->willReturn(FALSE); + + $missing_contexts = $this->prophesize(SectionStorageInterface::class); + + // Do not do any filtering based on context. + $this->contextHandler->filterPluginDefinitionsByContexts($contexts, $definitions)->willReturnArgument(1); + $this->contextHandler->applyContextMapping($no_access, $contexts)->shouldBeCalled(); + $this->contextHandler->applyContextMapping($provider_access, $contexts)->shouldBeCalled(); + $this->contextHandler->applyContextMapping($missing_contexts, $contexts)->willThrow(new ContextException()); + + $this->factory->createInstance('no_access', [])->willReturn($no_access->reveal()); + $this->factory->createInstance('missing_contexts', [])->willReturn($missing_contexts->reveal()); + $this->factory->createInstance('provider_access', [])->willReturn($provider_access->reveal()); + + $result = $this->manager->findByContext('test_operation', $contexts); + if ($access) { + $this->assertSame($provider_access->reveal(), $result); + } + else { + $this->assertNull($result); + } + } + + /** + * Provides test data for ::testFindByContext(). + */ + public function providerTestFindByContext() { + $data = []; + $data['true'] = [TRUE]; + $data['false'] = [FALSE]; + return $data; + } + +} + +/** + * Provides a test manager. + */ +class TestSectionStorageManager extends SectionStorageManager { + + /** + * {@inheritdoc} + */ + public function __construct(DiscoveryInterface $discovery, FactoryInterface $factory, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ContextHandlerInterface $context_handler) { + parent::__construct(new \ArrayObject(), $cache_backend, $module_handler, $context_handler); + $this->discovery = $discovery; + $this->factory = $factory; + } + }