diff --git a/core/lib/Drupal/Core/Plugin/Context/Context.php b/core/lib/Drupal/Core/Plugin/Context/Context.php index a33eca2a45..14407ea1ab 100644 --- a/core/lib/Drupal/Core/Plugin/Context/Context.php +++ b/core/lib/Drupal/Core/Plugin/Context/Context.php @@ -6,6 +6,7 @@ use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataTrait; @@ -14,6 +15,7 @@ */ class Context extends ComponentContext implements ContextInterface { + use DependencySerializationTrait; use TypedDataTrait; /** diff --git a/core/lib/Drupal/Core/Plugin/Context/EntityContext.php b/core/lib/Drupal/Core/Plugin/Context/EntityContext.php index 00ea81f759..131360df26 100644 --- a/core/lib/Drupal/Core/Plugin/Context/EntityContext.php +++ b/core/lib/Drupal/Core/Plugin/Context/EntityContext.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Plugin\Context; -use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -11,8 +10,6 @@ */ class EntityContext extends Context { - use DependencySerializationTrait; - /** * Gets a context from an entity type ID. * diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 0676365f70..9b908fea7c 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -207,6 +207,9 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation * Implements hook_layout_builder_section_storage_alter(). */ function layout_builder_layout_builder_section_storage_alter(array &$definitions) { + // The \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage + // plugin defines the entity context via its annotation but cannot specify the + // constraint inline. /** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $definitions */ $definitions['overrides']->getContextDefinition('entity') ->addConstraint('EntityHasField', 'layout_builder__layout'); diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php index 4c3bac2c49..267a499aff 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php @@ -18,6 +18,13 @@ class SectionStorageDefinition extends PluginDefinition implements ContextAwareP use ContextAwarePluginDefinitionTrait; + /** + * The plugin weight. + * + * @var int + */ + protected $weight = 0; + /** * Any additional properties and values. * @@ -86,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 933ebd29b6..f92929c336 100644 --- a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php @@ -2,7 +2,7 @@ namespace Drupal\layout_builder\SectionStorage; -use Drupal\Component\Utility\SortArray; +use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; @@ -47,15 +47,11 @@ protected function findDefinitions() { $definitions = parent::findDefinitions(); // Sort the definitions before they are cached. - uasort($definitions, function (SectionStorageDefinition $a, SectionStorageDefinition $b) { - $a = [ - 'weight' => $a->get('weight'), - ]; - $b = [ - 'weight' => $b->get('weight'), - ]; - return SortArray::sortByWeightElement($a, $b); - }); + $weights = array_map(function (SectionStorageDefinition $definition) { + return $definition->getWeight(); + }, $definitions); + $ids = array_keys($definitions); + array_multisort($weights, $ids, $definitions); return $definitions; } @@ -73,15 +69,8 @@ public function loadFromContext(array $contexts) { $storage_types = array_keys($this->getDefinitionsForContexts($contexts)); foreach ($storage_types as $type) { - /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ - $plugin = $this->createInstance($type); - - foreach ($contexts as $name => $context) { - $plugin->setContext($name, $context); - } - // Now that all contexts are set, the storage has the final say on whether - // it should be used or not. - if ($plugin->access('load')) { + $plugin = $this->loadWithContextsApplied($type, $contexts); + if ($plugin && $plugin->access('load')) { return $plugin; } } @@ -92,20 +81,30 @@ public function loadFromContext(array $contexts) { * {@inheritdoc} */ public function loadFromRoute($type, $value, $definition, $name, array $defaults) { - /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ - $plugin = $this->createInstance($type); + $contexts = $this->loadEmpty($type)->getContextsFromRoute($value, $definition, $name, $defaults); + return $this->loadWithContextsApplied($type, $contexts); + } + /** + * Loads a section storage with the provided contexts applied. + * + * @param string $type + * The section storage type. + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * The contexts available for this storage to use. + * + * @return \Drupal\layout_builder\SectionStorageInterface|null + * The section storage. + */ + protected function loadWithContextsApplied($type, array $contexts) { + $plugin = $this->loadEmpty($type); try { - $contexts = $plugin->getContextsFromRoute($value, $definition, $name, $defaults); + $this->contextHandler()->applyContextMapping($plugin, $contexts); } - catch (\InvalidArgumentException $e) { - $contexts = []; + catch (ContextException $e) { + return NULL; } - - foreach ($contexts as $name => $context) { - $plugin->setContext($name, $context); - } - return $contexts ? $plugin : NULL; + return $plugin; } } diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index a77c838620..bf078cac98 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -80,7 +80,7 @@ public function getLayoutBuilderUrl($rel = 'view'); * @param array $defaults * The route defaults array. * - * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * @return \Drupal\Core\Plugin\Context\ContextInterface[] * The required plugin contexts. */ public function getContextsFromRoute($value, $definition, $name, array $defaults); diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php index 10f41e937c..dce906aa6d 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php @@ -3,9 +3,16 @@ namespace Drupal\Tests\layout_builder\Unit; use Drupal\Component\Plugin\Context\ContextInterface; +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Component\Plugin\Factory\FactoryInterface; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\ContextHandlerInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; use Drupal\layout_builder\SectionStorage\SectionStorageManager; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; @@ -20,16 +27,30 @@ class SectionStorageManagerTest extends UnitTestCase { /** * The section storage manager. * - * @var \Drupal\layout_builder\SectionStorage\SectionStorageManager + * @var \Drupal\Tests\layout_builder\Unit\TestSectionStorageManager */ protected $manager; /** - * The plugin. + * The plugin discovery. * - * @var \Drupal\layout_builder\SectionStorageInterface + * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface */ - protected $plugin; + protected $discovery; + + /** + * The plugin factory. + * + * @var \Drupal\Component\Plugin\Factory\FactoryInterface + */ + protected $factory; + + /** + * The context handler. + * + * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected $contextHandler; /** * {@inheritdoc} @@ -37,49 +58,150 @@ class SectionStorageManagerTest extends UnitTestCase { protected function setUp() { parent::setUp(); + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $this->factory = $this->prophesize(FactoryInterface::class); $cache = $this->prophesize(CacheBackendInterface::class); $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); + $this->manager = new TestSectionStorageManager($this->discovery->reveal(), $this->factory->reveal(), $cache->reveal(), $module_handler->reveal()); - $this->plugin = $this->prophesize(SectionStorageInterface::class); + $this->contextHandler = $this->prophesize(ContextHandlerInterface::class); + $container = new ContainerBuilder(); + $container->set('context.handler', $this->contextHandler->reveal()); + \Drupal::setContainer($container); - $factory = $this->prophesize(FactoryInterface::class); - $factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal()); - $reflection_property = new \ReflectionProperty($this->manager, 'factory'); - $reflection_property->setAccessible(TRUE); - $reflection_property->setValue($this->manager, $factory->reveal()); } /** * @covers ::loadEmpty */ public function testLoadEmpty() { + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); + $result = $this->manager->loadEmpty('the_plugin_id'); - $this->assertInstanceOf(SectionStorageInterface::class, $result); + $this->assertSame($plugin->reveal(), $result); } /** * @covers ::loadFromRoute */ public function testLoadFromRoute() { + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); + $contexts = [ 'the_context' => $this->prophesize(ContextInterface::class)->reveal(), ]; - $this->plugin->getContextsFromRoute('the_value', [], 'the_parameter_name', [])->willReturn($contexts); - $this->plugin->setContext('the_context', $contexts['the_context'])->shouldBeCalled(); + $plugin->getContextsFromRoute('the_value', [], 'the_parameter_name', [])->willReturn($contexts); + $plugin->access('load')->willReturn(TRUE); $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []); - $this->assertInstanceOf(SectionStorageInterface::class, $result); + $this->assertSame($plugin->reveal(), $result); } /** * @covers ::loadFromRoute */ public function testLoadFromRouteNull() { - $this->plugin->getContextsFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn([]); + $plugin = $this->prophesize(SectionStorageInterface::class); + $this->factory->createInstance('the_plugin_id', [])->willReturn($plugin->reveal()); + + $plugin->getContextsFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn([]); + $this->contextHandler->applyContextMapping($plugin, [])->willThrow(new ContextException()); + $plugin->access('load')->shouldNotBeCalled(); $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']); $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 ::loadFromContext + * + * @dataProvider providerTestLoadFromContext + */ + public function testLoadFromContext($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('load')->willReturn($access); + + $no_access = $this->prophesize(SectionStorageInterface::class); + $no_access->access('load')->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->loadFromContext($contexts); + if ($access) { + $this->assertSame($provider_access->reveal(), $result); + } + else { + $this->assertNull($result); + } + } + + /** + * Provides test data for ::testLoadFromContext(). + */ + public function providerTestLoadFromContext() { + $data = []; + $data['true'] = [TRUE]; + $data['false'] = [FALSE]; + return $data; + } + +} + +/** + * Provides a test manager. + */ +class TestSectionStorageManager extends SectionStorageManager { + + /** + * {@inheritdoc} + */ + public function __construct(DiscoveryInterface $discovery, FactoryInterface $factory, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct(new \ArrayObject(), $cache_backend, $module_handler); + $this->discovery = $discovery; + $this->factory = $factory; + } + }