diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 5d7c60615c..0676365f70 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -202,3 +202,12 @@ 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) { + /** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $definitions */ + $definitions['overrides']->getContextDefinition('entity') + ->addConstraint('EntityHasField', 'layout_builder__layout'); +} diff --git a/core/modules/layout_builder/src/Annotation/SectionStorage.php b/core/modules/layout_builder/src/Annotation/SectionStorage.php index 42f4a47fe6..e84fbaad2e 100644 --- a/core/modules/layout_builder/src/Annotation/SectionStorage.php +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php @@ -22,6 +22,13 @@ class SectionStorage extends Plugin { */ public $id; + /** + * Any required context definitions. + * + * @var array + */ + public $context = []; + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 78ff430b00..8c1c2d8d1b 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; @@ -239,11 +241,35 @@ 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(); - } + if ($this->isOverridable()) { + $contexts = [ + 'entity' => new Context(ContextDefinition::create('entity'), $entity), + ]; + + /** @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $storage_manager */ + $storage_manager = \Drupal::service('plugin.manager.layout_builder.section_storage'); + + // Use the contexts as a filtering mechanism to find all applicable + // section storage plugin IDs. + $storage_types = array_keys($storage_manager->getDefinitionsForContexts($contexts)); - return $this->getSections(); + // Loop through every section storage plugin until we find one that + // derives a valid section list from the context values. + while (empty($section_list) && $storage_types) { + $storage_type = array_shift($storage_types); + + $storage = $storage_manager->loadEmpty($storage_type); + $storage->setContext('entity', $contexts['entity']); + $section_list = $storage->getSectionListFromContext(); + } + } + // 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. + if (empty($section_list) || count($section_list) === 0) { + $section_list = $this; + } + return $section_list->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..1ab12eebd1 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -275,6 +275,13 @@ public function getSectionListFromId($id) { return $display; } + /** + * {@inheritdoc} + */ + public function getSectionListFromContext() { + return NULL; + } + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index c623c5ede3..555636862c 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -23,6 +23,9 @@ * * @SectionStorage( * id = "overrides", + * context = { + * "entity" = @ContextDefinition("entity"), + * } * ) * * @internal @@ -134,6 +137,16 @@ 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 getSectionListFromContext() { + // The entity is guaranteed to have have the layout_builder__layout field + // because we are adding that as a constraint in an alter hook. + // @see layout_builder_layout_builder_section_storage_alter() + return $this->getContextValue('entity')->get('layout_builder__layout'); + } + /** * {@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..ae6569cb33 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,7 +16,7 @@ * 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; @@ -103,4 +103,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/SectionStorage/SectionStorageDefinition.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php index 61b975a471..4c3bac2c49 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,9 @@ * 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; /** * Any additional properties and values. @@ -28,6 +32,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); } diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php index 18147cd1d8..8f24b06cb0 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php @@ -4,6 +4,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\layout_builder\Annotation\SectionStorage; use Drupal\layout_builder\SectionStorageInterface; @@ -18,6 +19,8 @@ */ class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface { + use ContextAwarePluginManagerTrait; + /** * Constructs a new SectionStorageManager object. * diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php index 3b269fcbba..8cf53b620a 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php @@ -2,7 +2,7 @@ namespace Drupal\layout_builder\SectionStorage; -use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface; /** * Provides the interface for a plugin manager of section storage types. @@ -12,7 +12,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface SectionStorageManagerInterface extends DiscoveryInterface { +interface SectionStorageManagerInterface extends ContextAwarePluginManagerInterface { /** * Loads a section storage with no associated section list. diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index 90ce9072fd..9a1aa61934 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -2,8 +2,8 @@ namespace Drupal\layout_builder; -use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Access\AccessibleInterface; +use Drupal\Core\Plugin\ContextAwarePluginInterface; use Symfony\Component\Routing\RouteCollection; /** @@ -14,7 +14,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, ContextAwarePluginInterface, AccessibleInterface { /** * Returns an identifier for this storage. @@ -64,6 +64,17 @@ public function setSectionList(SectionListInterface $section_list); */ public function getSectionListFromId($id); + /** + * Derives the section list from the available context. + * + * @return \Drupal\layout_builder\SectionListInterface + * The section list. + * + * @internal + * This should only be called during section storage instantiation. + */ + public function getSectionListFromContext(); + /** * Provides the routes needed for Layout Builder UI. * 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..38b66f557d 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\OverridesSectionStorage; /** * Implements hook_plugin_filter_TYPE__CONSUMER_alter(). @@ -58,3 +59,11 @@ 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(OverridesSectionStorage::class); +} diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/OverridesSectionStorage.php new file mode 100644 index 0000000000..f6d492d5b8 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -0,0 +1,17 @@ +set('layout_builder_test_context_applied', TRUE); + return parent::getSectionListFromContext(); + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 1e4f240869..51730ea412 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\Entity\LayoutBuilderEntityViewDisplay; use Drupal\node\Entity\Node; use Drupal\Tests\BrowserTestBase; use Drupal\views\Entity\View; @@ -526,4 +527,25 @@ public function testBlockPlaceholder() { $assert_session->pageTextContains($block_content); } + /** + * Tests that section loading is delegated to plugins during rendering. + */ + public function testRenderByContextualPluginDelegate() { + $state_key = 'layout_builder_test_context_applied'; + /** @var \Drupal\Core\State\StateInterface $state */ + $state = $this->container->get('state'); + + $this->drupalGet('node/1'); + $this->assertEmpty($state->get($state_key)); + + entity_get_display('node', 'bundle_with_section_field', 'full') + ->enableLayoutBuilder() + ->setOverridable() + ->save(); + + $this->getSession()->reload(); + $state->resetCache(); + $this->assertTrue($state->get($state_key)); + } + }