diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 373f7d8a0f..3021b9af93 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -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); +} 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..0af23a493c 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_definitions = []; + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Context/LayoutBuilderContextTrait.php b/core/modules/layout_builder/src/Context/LayoutBuilderContextTrait.php index 3c16332def..0f0ca89d8f 100644 --- a/core/modules/layout_builder/src/Context/LayoutBuilderContextTrait.php +++ b/core/modules/layout_builder/src/Context/LayoutBuilderContextTrait.php @@ -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; } diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 7f10d6f51f..ee95b5b00e 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -255,7 +255,7 @@ public function buildMultiple(array $entities) { $label = new TranslatableMarkup('@entity being viewed', [ '@entity' => $entity->getEntityType()->getSingularLabel(), ]); - $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label); + $contexts['entity'] = EntityContext::fromEntity($entity, $label); foreach ($sections as $delta => $section) { $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts); } @@ -364,7 +364,6 @@ public function setComponent($name, array $options = []) { if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) { $configuration = [ 'label_display' => '0', - 'context_mapping' => ['entity' => 'layout_builder.entity'], ]; if ($is_view_configurable_non_extra_field) { $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name; diff --git a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php index 276e680b43..e8d5237be8 100644 --- a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php @@ -5,6 +5,7 @@ use Drupal\block_content\Access\RefinableDependentAccessInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\PreviewFallbackInterface; use Drupal\Core\Session\AccountInterface; @@ -65,15 +66,19 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { // RefinableDependentAccessInterface object and need to pass on this value. if ($block instanceof RefinableDependentAccessInterface) { $contexts = $event->getContexts(); - if (isset($contexts['layout_builder.entity'])) { - if ($entity = $contexts['layout_builder.entity']->getContextValue()) { + foreach ($contexts as $context) { + $value = $context->hasContextValue() ? $context->getContextValue() : NULL; + if ($value instanceof FieldableEntityInterface) { if ($event->inPreview()) { // If previewing in Layout Builder allow access. $block->setAccessDependency(new LayoutPreviewAccessAllowed()); } else { - $block->setAccessDependency($entity); + $block->setAccessDependency($value); } + + // Break out of the loop once a fieldable entity has been found. + break; } } } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php index d35041d03d..14899a816f 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -25,6 +25,10 @@ * * @SectionStorage( * id = "defaults", + * weight = 10, + * context_definitions = { + * "display" = @ContextDefinition("entity:entity_view_display"), + * }, * ) * * @internal @@ -90,12 +94,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 +238,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 +258,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 +280,73 @@ 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['entity'] = EntityContext::fromEntity($entity); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function deriveContextsFromRoute($value, $definition, $name, array $defaults) { $contexts = []; - $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity); + + 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. + */ + 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 (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} */ diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index bba10ef67f..33cf559f5c 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -23,6 +23,10 @@ * * @SectionStorage( * id = "overrides", + * context_definitions = { + * "entity" = @ContextDefinition("entity"), + * "view_mode" = @ContextDefinition("string", required = FALSE), + * } * ) * * @internal @@ -86,12 +90,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 +101,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 +116,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 +132,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 +143,47 @@ 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. + */ + protected 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} */ @@ -254,15 +297,6 @@ public function getLayoutBuilderUrl($rel = 'view') { return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters); } - /** - * {@inheritdoc} - */ - public function getContexts() { - $entity = $this->getEntity(); - $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity); - return $contexts; - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php index c419060c45..5927701ed5 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php @@ -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,16 @@ * 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; + @trigger_error('\Drupal\layout_builder\SectionStorageInterface::setSectionList() 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); + 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 +33,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 +88,11 @@ public function removeSection($delta) { return $this; } + /** + * {@inheritdoc} + */ + public function getContextsDuringPreview() { + return $this->getContexts(); + } + } diff --git a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php index 263b767f72..712f9ff850 100644 --- a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php +++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php @@ -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); } } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php index 61b975a471..bccd7d1484 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_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; + } + } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php index 18147cd1d8..54d09d9e03 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; @@ -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; + } + + /** + * {@inheritdoc} + */ + 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} */ @@ -47,25 +115,18 @@ public function loadEmpty($id) { * {@inheritdoc} */ public function loadFromStorageId($type, $id) { - /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ - $plugin = $this->createInstance($type); - return $plugin->setSectionList($plugin->getSectionListFromId($id)); + @trigger_error('\Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::loadFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() should be used instead. See https://www.drupal.org/node/3012353.', E_USER_DEPRECATED); + $contexts = $this->loadEmpty($type)->deriveContextsFromRoute($id, [], '', []); + return $this->load($type, $contexts); } /** * {@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. - } - } + @trigger_error('\Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::loadFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() and \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() should be used instead. See https://www.drupal.org/node/3012353.', E_USER_DEPRECATED); + $contexts = $this->loadEmpty($type)->deriveContextsFromRoute($value, $definition, $name, $defaults); + return $this->load($type, $contexts); } } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php index 3b269fcbba..4e09e2721a 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php @@ -14,6 +14,32 @@ */ interface SectionStorageManagerInterface extends DiscoveryInterface { + /** + * Loads a section storage with the provided contexts applied. + * + * @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|null + * The section storage or NULL if its context requirements are not met. + */ + public function load($type, array $contexts = []); + + /** + * Finds the section storage to load based on available contexts. + * + * @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|null + * The section storage if one matched all contexts, or NULL otherwise. + */ + public function findByContext($operation, array $contexts); + /** * Loads a section storage with no associated section list. * @@ -22,6 +48,12 @@ interface SectionStorageManagerInterface extends DiscoveryInterface { * * @return \Drupal\layout_builder\SectionStorageInterface * The section storage. + * + * @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 loadEmpty($id); @@ -38,6 +70,10 @@ public function loadEmpty($id); * * @throws \InvalidArgumentException * Thrown if the ID is invalid. + * + * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. + * \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() + * should be used instead. See https://www.drupal.org/node/3012353. */ public function loadFromStorageId($type, $id); @@ -59,6 +95,11 @@ public function loadFromStorageId($type, $id); * The section storage if it could be loaded, or NULL otherwise. * * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert() + * + * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. + * \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() + * and \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() + * should be used instead. See https://www.drupal.org/node/3012353. */ public function loadFromRoute($type, $value, $definition, $name, array $defaults); diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index 90ce9072fd..b79a702de3 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. @@ -44,6 +45,10 @@ public function getStorageType(); * * @internal * This should only be called during section storage instantiation. + * + * @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. */ public function setSectionList(SectionListInterface $section_list); @@ -61,6 +66,10 @@ public function setSectionList(SectionListInterface $section_list); * * @internal * This should only be called during section storage instantiation. + * + * @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. */ public function getSectionListFromId($id); @@ -115,16 +124,41 @@ public function getLayoutBuilderUrl($rel = 'view'); * * @internal * This should only be called during section storage instantiation. + * + * @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. */ public function extractIdFromRoute($value, $definition, $name, array $defaults); /** - * Provides any available contexts for the object using the sections. + * Derives the available plugin contexts from route values. + * + * @param mixed $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. + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + * The available plugin contexts. + * + * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert() + */ + public function deriveContextsFromRoute($value, $definition, $name, array $defaults); + + /** + * Gets contexts for use during preview. + * + * When not in preview, ::getContexts() will be used. * * @return \Drupal\Core\Plugin\Context\ContextInterface[] - * The array of context objects. + * The plugin contexts suitable for previewing. */ - public function getContexts(); + public function getContextsDuringPreview(); /** * 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/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..a7b3e58e65 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php @@ -0,0 +1,225 @@ +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 deriveContextsFromRoute($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} + */ + public function getContextsDuringPreview() { + return $this->getContexts(); + } + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + @trigger_error('\Drupal\layout_builder\SectionStorageInterface::setSectionList() 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); + return $this; + } + + /** + * {@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); + return $this; + } + + /** + * {@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); + return $value ?: $defaults['id']; + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index e62621a249..35a4a26cdf 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; @@ -648,4 +649,30 @@ public function testBlockUiListing() { $assert_session->pageTextNotContains('Content fields'); } + /** + * Tests a config-based implementation of Layout Builder. + * + * @see \Drupal\layout_builder_test\Plugin\SectionStorage\SimpleConfigSectionStorage + */ + 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/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index e29609b30e..d5a82a4427 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -82,9 +82,6 @@ public function providerTestLayoutSectionFormatter() { 'section' => new Section('layout_onecol', [], [ 'baz' => new SectionComponent('baz', 'content', [ 'id' => 'field_block:node:bundle_with_section_field:body', - 'context_mapping' => [ - 'entity' => 'layout_builder.entity', - ], ]), ]), ], diff --git a/core/modules/layout_builder/tests/src/Kernel/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Kernel/DefaultsSectionStorageTest.php new file mode 100644 index 0000000000..bfaa2b1599 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/DefaultsSectionStorageTest.php @@ -0,0 +1,166 @@ +installSchema('system', ['key_value_expire']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + + $this->plugin = DefaultsSectionStorage::create($this->container, [], 'defaults', new SectionStorageDefinition()); + } + + /** + * @covers ::access + * @dataProvider providerTestAccess + * + * @param bool $expected + * The expected outcome of ::access(). + * @param string $operation + * The operation to pass to ::access(). + * @param bool $is_enabled + * Whether Layout Builder is enabled for this display. + * @param array $section_data + * Data to store as the sections value for Layout Builder. + */ + public function testAccess($expected, $operation, $is_enabled, array $section_data) { + $display = LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]); + if ($is_enabled) { + $display->enableLayoutBuilder(); + } + $display + ->setThirdPartySetting('layout_builder', 'sections', $section_data) + ->save(); + + $this->plugin->setContext('display', EntityContext::fromEntity($display)); + $result = $this->plugin->access($operation); + $this->assertSame($expected, $result); + } + + /** + * Provides test data for ::testAccess(). + */ + public function providerTestAccess() { + $section_data = [ + new Section('layout_onecol', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), + ]), + ]; + + // Data provider values are: + // - the expected outcome of the call to ::access() + // - the operation + // - whether Layout Builder has been enabled for this display + // - whether this display has any section data. + $data = []; + $data['view, disabled, no data'] = [FALSE, 'view', FALSE, []]; + $data['view, enabled, no data'] = [TRUE, 'view', TRUE, []]; + $data['view, disabled, data'] = [FALSE, 'view', FALSE, $section_data]; + $data['view, enabled, data'] = [TRUE, 'view', TRUE, $section_data]; + return $data; + } + + /** + * @covers ::getContexts + */ + public function testGetContexts() { + $display = LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]); + $display->save(); + + $context = EntityContext::fromEntity($display); + $this->plugin->setContext('display', $context); + + $expected = ['display' => $context]; + $this->assertSame($expected, $this->plugin->getContexts()); + } + + /** + * @covers ::getContextsDuringPreview + */ + public function testGetContextsDuringPreview() { + $display = LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]); + $display->save(); + + $context = EntityContext::fromEntity($display); + $this->plugin->setContext('display', $context); + + $result = $this->plugin->getContextsDuringPreview(); + $this->assertEquals(['display', 'entity'], array_keys($result)); + + $this->assertSame($context, $result['display']); + + $this->assertInstanceOf(EntityContext::class, $result['entity']); + $result_value = $result['entity']->getContextValue(); + $this->assertInstanceOf(EntityTest::class, $result_value); + $this->assertSame('entity_test', $result_value->bundle()); + } + + /** + * @covers ::setSectionList + * + * @expectedDeprecation \Drupal\layout_builder\SectionStorageInterface::setSectionList() 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. + * @group legacy + */ + public function testSetSectionList() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->setExpectedException(\Exception::class, '\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.'); + $this->plugin->setSectionList($section_list->reveal()); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php new file mode 100644 index 0000000000..35d283b41a --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php @@ -0,0 +1,151 @@ +installSchema('system', ['key_value_expire']); + $this->installEntitySchema('entity_test'); + + $this->plugin = OverridesSectionStorage::create($this->container, [], 'overrides', new SectionStorageDefinition()); + } + + /** + * @covers ::access + * @dataProvider providerTestAccess + * + * @param bool $expected + * The expected outcome of ::access(). + * @param string $operation + * The operation to pass to ::access(). + * @param bool $is_enabled + * Whether Layout Builder is enabled for this display. + * @param array $section_data + * Data to store as the sections value for Layout Builder. + */ + public function testAccess($expected, $operation, $is_enabled, array $section_data) { + $display = LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]); + if ($is_enabled) { + $display->enableLayoutBuilder(); + } + $display + ->setOverridable() + ->save(); + + $entity = EntityTest::create([OverridesSectionStorage::FIELD_NAME => $section_data]); + $entity->save(); + + $this->plugin->setContext('entity', EntityContext::fromEntity($entity)); + $result = $this->plugin->access($operation); + $this->assertSame($expected, $result); + } + + /** + * Provides test data for ::testAccess(). + */ + public function providerTestAccess() { + $section_data = [ + new Section('layout_default', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), + ]), + ]; + + // Data provider values are: + // - the expected outcome of the call to ::access() + // - the operation + // - whether Layout Builder has been enabled for this display + // - whether this display has any section data. + $data = []; + $data['view, disabled, no data'] = [FALSE, 'view', FALSE, []]; + $data['view, enabled, no data'] = [TRUE, 'view', TRUE, []]; + $data['view, disabled, data'] = [FALSE, 'view', FALSE, $section_data]; + $data['view, enabled, data'] = [TRUE, 'view', TRUE, $section_data]; + return $data; + } + + /** + * @covers ::getContexts + */ + public function testGetContexts() { + $entity = EntityTest::create(); + $entity->save(); + + $context = EntityContext::fromEntity($entity); + $this->plugin->setContext('entity', $context); + + $expected = ['entity' => $context]; + $this->assertSame($expected, $this->plugin->getContexts()); + } + + /** + * @covers ::getContextsDuringPreview + */ + public function testGetContextsDuringPreview() { + $entity = EntityTest::create(); + $entity->save(); + + $context = EntityContext::fromEntity($entity); + $this->plugin->setContext('entity', $context); + + $expected = ['entity' => $context]; + $this->assertSame($expected, $this->plugin->getContextsDuringPreview()); + } + + /** + * @covers ::setSectionList + * + * @expectedDeprecation \Drupal\layout_builder\SectionStorageInterface::setSectionList() 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. + * @group legacy + */ + public function testSetSectionList() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->setExpectedException(\Exception::class, '\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.'); + $this->plugin->setSectionList($section_list->reveal()); + } + +} 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/BlockComponentRenderArrayTest.php b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php index 30820846cd..81c2cc627d 100644 --- a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php +++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php @@ -9,7 +9,7 @@ use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; @@ -72,11 +72,12 @@ public function testOnBuildRender($refinable_dependent_access) { $contexts = []; if ($refinable_dependent_access) { $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); - $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $this->prophesize(FieldableEntityInterface::class); $layout_entity = $layout_entity->reveal(); $context = $this->prophesize(ContextInterface::class); + $context->hasContextValue()->willReturn(TRUE); $context->getContextValue()->willReturn($layout_entity); - $contexts['layout_builder.entity'] = $context->reveal(); + $contexts['entity'] = $context->reveal(); $block->setAccessDependency($layout_entity)->shouldBeCalled(); } @@ -138,11 +139,12 @@ public function testOnBuildRenderDenied($refinable_dependent_access) { if ($refinable_dependent_access) { $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); - $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $this->prophesize(FieldableEntityInterface::class); $layout_entity = $layout_entity->reveal(); $context = $this->prophesize(ContextInterface::class); + $context->hasContextValue()->willReturn(TRUE); $context->getContextValue()->willReturn($layout_entity); - $contexts['layout_builder.entity'] = $context->reveal(); + $contexts['entity'] = $context->reveal(); $block->setAccessDependency($layout_entity)->shouldBeCalled(); } @@ -197,12 +199,13 @@ public function testOnBuildRenderInPreview($refinable_dependent_access) { $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class); $block->setAccessDependency(new LayoutPreviewAccessAllowed())->shouldBeCalled(); - $layout_entity = $this->prophesize(EntityInterface::class); + $layout_entity = $this->prophesize(FieldableEntityInterface::class); $layout_entity = $layout_entity->reveal(); $layout_entity->in_preview = TRUE; $context = $this->prophesize(ContextInterface::class); + $context->hasContextValue()->willReturn(TRUE); $context->getContextValue()->willReturn($layout_entity); - $contexts['layout_builder.entity'] = $context->reveal(); + $contexts['entity'] = $context->reveal(); } else { $block = $this->prophesize(BlockPluginInterface::class); diff --git a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php index d30917a446..ab7b7e8ec0 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; @@ -37,6 +38,13 @@ class DefaultsSectionStorageTest extends UnitTestCase { */ protected $entityTypeManager; + /** + * The sample entity generator. + * + * @var \Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator + */ + protected $sampleEntityGenerator; + /** * {@inheritdoc} */ @@ -45,13 +53,13 @@ protected function setUp() { $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); $entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); - $sample_entity_generator = $this->prophesize(LayoutBuilderSampleEntityGenerator::class); + $this->sampleEntityGenerator = $this->prophesize(LayoutBuilderSampleEntityGenerator::class); $definition = new SectionStorageDefinition([ 'id' => 'defaults', 'class' => DefaultsSectionStorage::class, ]); - $this->plugin = new DefaultsSectionStorage([], '', $definition, $this->entityTypeManager->reveal(), $entity_type_bundle_info->reveal(), $sample_entity_generator->reveal()); + $this->plugin = new DefaultsSectionStorage([], '', $definition, $this->entityTypeManager->reveal(), $entity_type_bundle_info->reveal(), $this->sampleEntityGenerator->reveal()); } /** @@ -61,8 +69,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('display', $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')); @@ -82,6 +94,10 @@ public function testThirdPartySettings() { * @covers ::extractIdFromRoute * * @dataProvider providerTestExtractIdFromRoute + * + * @expectedDeprecation \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. + * + * @group legacy */ public function testExtractIdFromRoute($expected, $value, array $defaults) { $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); @@ -129,6 +145,10 @@ public function providerTestExtractIdFromRoute() { * @covers ::getSectionListFromId * * @dataProvider providerTestGetSectionListFromId + * + * @expectedDeprecation \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. + * + * @group legacy */ public function testGetSectionListFromId($success, $expected_entity_id, $value) { if ($expected_entity_id) { @@ -173,6 +193,10 @@ public function providerTestGetSectionListFromId() { /** * @covers ::getSectionListFromId + * + * @expectedDeprecation \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. + * + * @group legacy */ public function testGetSectionListFromIdCreate() { $expected = 'the_return_value'; @@ -194,6 +218,115 @@ public function testGetSectionListFromIdCreate() { $this->assertSame($expected, $result); } + /** + * @covers ::extractEntityFromRoute + * + * @dataProvider providerTestExtractEntityFromRoute + * + * @param bool $success + * Whether a successful result is expected. + * @param string|null $expected_entity_id + * The expected entity ID. + * @param string $value + * The value to pass to ::extractEntityFromRoute(). + * @param array $defaults + * The defaults to pass to ::extractEntityFromRoute(). + */ + 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'); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + } + else { + $this->entityTypeManager->getDefinition('entity_view_display')->shouldNotBeCalled(); + $this->entityTypeManager->getStorage('entity_view_display')->shouldNotBeCalled(); + } + + $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 ::testExtractEntityFromRoute(). + */ + public function providerTestExtractEntityFromRoute() { + // Data provider values are: + // - whether a successful result is expected + // - the expected entity ID + // - the value to pass to ::extractEntityFromRoute() + // - the defaults to pass to ::extractEntityFromRoute(). + $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, + '', + [], + ]; + return $data; + } + + /** + * @covers ::extractEntityFromRoute + */ + public function testExtractEntityFromRouteCreate() { + $expected = 'the_return_value'; + $value = 'foo.bar.baz'; + $expected_create_values = [ + 'targetEntityType' => 'foo', + 'bundle' => 'bar', + 'mode' => 'baz', + 'status' => TRUE, + ]; + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load($value)->willReturn(NULL); + $entity_storage->create($expected_create_values)->willReturn($expected); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + + $method = new \ReflectionMethod($this->plugin, 'extractEntityFromRoute'); + $method->setAccessible(TRUE); + $result = $method->invoke($this->plugin, $value, []); + $this->assertSame($expected, $result); + } + /** * @covers ::buildRoutes * @covers ::getEntityTypes diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php index 5f8dc051e3..40e202f52b 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php @@ -32,7 +32,9 @@ 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->deriveContextsFromRoute($value, $definition, $name, $defaults)->willReturn([]); + $section_storage_manager->load('my_type', [])->willReturn($section_storage->reveal()); $layout_tempstore_repository->get($section_storage->reveal())->willReturn($expected); @@ -55,6 +57,7 @@ public function testConvertNoType() { $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); @@ -76,6 +79,7 @@ public function testConvertInvalidConverter() { $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 3691fb6c71..6ea0c4bc43 100644 --- a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -63,6 +63,10 @@ protected function setUp() { * @covers ::extractIdFromRoute * * @dataProvider providerTestExtractIdFromRoute + * + * @expectedDeprecation \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. + * + * @group legacy */ public function testExtractIdFromRoute($expected, $value, array $defaults) { $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); @@ -104,6 +108,10 @@ public function providerTestExtractIdFromRoute() { * @covers ::getSectionListFromId * * @dataProvider providerTestGetSectionListFromId + * + * @expectedDeprecation \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. + * + * @group legacy */ public function testGetSectionListFromId($success, $expected_entity_type_id, $id) { $defaults['the_parameter_name'] = $id; @@ -160,6 +168,88 @@ public function providerTestGetSectionListFromId() { return $data; } + /** + * @covers ::extractEntityFromRoute + * + * @dataProvider providerTestExtractEntityFromRoute + * + * @param bool $success + * Whether a successful result is expected. + * @param string|null $expected_entity_type_id + * The expected entity type ID. + * @param string $value + * The value to pass to ::extractEntityFromRoute(). + * @param array $defaults + * The defaults to pass to ::extractEntityFromRoute(). + */ + 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(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE); + $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); + + $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); + $entity_with_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(TRUE); + $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(); + } + + $method = new \ReflectionMethod($this->plugin, 'extractEntityFromRoute'); + $method->setAccessible(TRUE); + $result = $method->invoke($this->plugin, $value, $defaults); + if ($success) { + $this->assertInstanceOf(FieldableEntityInterface::class, $result); + } + else { + $this->assertNull($result); + } + } + + /** + * Provides data for ::testExtractEntityFromRoute(). + */ + public function providerTestExtractEntityFromRoute() { + // Data provider values are: + // - whether a successful result is expected + // - the expected entity ID + // - the value to pass to ::extractEntityFromRoute() + // - the defaults to pass to ::extractEntityFromRoute(). + $data = []; + $data['with value, with layout'] = [ + TRUE, + 'my_entity_type', + 'my_entity_type.entity_with_layout', + [], + ]; + $data['with value, without layout'] = [ + FALSE, + '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, + '', + [], + ]; + return $data; + } + /** * @covers ::buildRoutes * @covers ::hasIntegerId diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php index 392cd8939e..4cbd788415 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php @@ -2,13 +2,20 @@ 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; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @coversDefaultClass \Drupal\layout_builder\SectionStorage\SectionStorageManager @@ -31,6 +38,27 @@ class SectionStorageManagerTest extends UnitTestCase { */ protected $plugin; + /** + * The plugin discovery. + * + * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + 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} */ @@ -39,15 +67,37 @@ protected function setUp() { $cache = $this->prophesize(CacheBackendInterface::class); $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); + $this->contextHandler = $this->prophesize(ContextHandlerInterface::class); + $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal(), $this->contextHandler->reveal()); + + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $reflection_property = new \ReflectionProperty($this->manager, 'discovery'); + $reflection_property->setAccessible(TRUE); + $reflection_property->setValue($this->manager, $this->discovery->reveal()); $this->plugin = $this->prophesize(SectionStorageInterface::class); - - $factory = $this->prophesize(FactoryInterface::class); - $factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal()); + $this->factory = $this->prophesize(FactoryInterface::class); + $this->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()); + $reflection_property->setValue($this->manager, $this->factory->reveal()); + } + + /** + * @covers ::__construct + * + * @expectedDeprecation 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. + * + * @group legacy + */ + public function testConstructNoContextHandler() { + $cache = $this->prophesize(CacheBackendInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $container = $this->prophesize(ContainerInterface::class); + $container->get('context.handler')->shouldBeCalled(); + \Drupal::setContainer($container->reveal()); + new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); } /** @@ -56,17 +106,18 @@ protected function setUp() { public function testLoadEmpty() { $result = $this->manager->loadEmpty('the_plugin_id'); $this->assertInstanceOf(SectionStorageInterface::class, $result); + $this->assertSame($this->plugin->reveal(), $result); } /** * @covers ::loadFromStorageId + * + * @expectedDeprecation \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::loadFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() should be used instead. See https://www.drupal.org/node/3012353. + * + * @group legacy */ 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()); + $this->plugin->deriveContextsFromRoute('the_storage_id', [], '', [])->willReturn([]); $result = $this->manager->loadFromStorageId('the_plugin_id', 'the_storage_id'); $this->assertInstanceOf(SectionStorageInterface::class, $result); @@ -74,27 +125,135 @@ public function testLoadFromStorageId() { /** * @covers ::loadFromRoute + * + * @expectedDeprecation \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::loadFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() and \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() should be used instead. See https://www.drupal.org/node/3012353. + * + * @group legacy */ 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; - }); - + $this->plugin->deriveContextsFromRoute('the_value', [], 'the_parameter_name', [])->willReturn([]); $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []); $this->assertInstanceOf(SectionStorageInterface::class, $result); } /** * @covers ::loadFromRoute + * + * @expectedDeprecation \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::loadFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() and \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::load() should be used instead. See https://www.drupal.org/node/3012353. + * + * @group legacy */ public function testLoadFromRouteNull() { - $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn(NULL); - + $this->plugin->deriveContextsFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn([]); $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::load + */ + public function testLoad() { + $contexts = [ + 'the_context' => $this->prophesize(ContextInterface::class)->reveal(), + ]; + + $this->contextHandler->applyContextMapping($this->plugin, $contexts)->shouldBeCalled(); + + $result = $this->manager->load('the_plugin_id', $contexts); + $this->assertSame($this->plugin->reveal(), $result); + } + + /** + * @covers ::load + */ + public function testLoadNull() { + $contexts = [ + 'the_context' => $this->prophesize(ContextInterface::class)->reveal(), + ]; + + $this->contextHandler->applyContextMapping($this->plugin, $contexts)->willThrow(new ContextException()); + + $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 + * + * @param bool $plugin_access + * The result for the plugin's access method to return. + */ + public function testFindByContext($plugin_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($plugin_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 ($plugin_access) { + $this->assertSame($provider_access->reveal(), $result); + } + else { + $this->assertNull($result); + } + } + + /** + * Provides test data for ::testFindByContext(). + */ + public function providerTestFindByContext() { + // Data provider values are: + // - the result for the plugin's access method to return. + $data = []; + $data['plugin access: true'] = [TRUE]; + $data['plugin access: false'] = [FALSE]; + return $data; + } + }