Issue #3420983 by sorlov, godotislate, alexpott, quietone, kim.pepper, larowlan, mstrelan: Convert Layout plugin discovery to attributes

merge-requests/7539/merge
Alex Pott 2024-05-03 09:01:25 +01:00
parent 74a6dce280
commit 091483c1f1
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
13 changed files with 319 additions and 113 deletions

View File

@ -0,0 +1,111 @@
<?php
namespace Drupal\Core\Layout\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Layout\LayoutDefinition;
/**
* Defines a Layout attribute object.
*
* Layouts are used to define a list of regions and then output render arrays
* in each of the regions, usually using a template.
*
* Plugin Namespace: Plugin\Layout
*
* @see \Drupal\Core\Layout\LayoutInterface
* @see \Drupal\Core\Layout\LayoutDefault
* @see \Drupal\Core\Layout\LayoutPluginManager
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class Layout extends Plugin {
/**
* Any additional properties and values.
*
* @see \Drupal\Core\Layout\LayoutDefinition::$additional
*
* @var array
*/
public readonly array $additional;
/**
* Constructs a Layout attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
* (optional) The human-readable name. @todo Deprecate optional label in
* https://www.drupal.org/project/drupal/issues/3392572.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $category
* (optional) The human-readable category. @todo Deprecate optional category
* in https://www.drupal.org/project/drupal/issues/3392572.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
* (optional) The description for advanced layouts.
* @param string|null $template
* (optional) The template file to render the layout.
* @param string $theme_hook
* (optional) The template hook to render the layout.
* @param string|null $path
* (optional) Path (relative to the module or theme) to resources like icon or template.
* @param string|null $library
* (optional) The asset library.
* @param string|null $icon
* (optional) The path to the preview image (relative to the 'path' given).
* @param string[][]|null $icon_map
* (optional) The icon map.
* @param array $regions
* (optional) An associative array of regions in this layout.
* @param string|null $default_region
* (optional) The default region.
* @param class-string $class
* (optional) The layout plugin class.
* @param \Drupal\Core\Plugin\Context\ContextDefinitionInterface[] $context_definitions
* (optional) The context definition.
* @param array $config_dependencies
* (optional) The config dependencies.
* @param class-string|null $deriver
* (optional) The deriver class.
* @param mixed $additional
* (optional) Additional properties passed in that can be used by a deriver.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $label = NULL,
public readonly ?TranslatableMarkup $category = NULL,
public readonly ?TranslatableMarkup $description = NULL,
public readonly ?string $template = NULL,
public readonly string $theme_hook = 'layout',
public readonly ?string $path = NULL,
public readonly ?string $library = NULL,
public readonly ?string $icon = NULL,
public readonly ?array $icon_map = NULL,
public readonly array $regions = [],
public readonly ?string $default_region = NULL,
public string $class = LayoutDefault::class,
public readonly array $context_definitions = [],
public readonly array $config_dependencies = [],
public readonly ?string $deriver = NULL,
...$additional,
) {
// Layout definitions support arbitrary properties being passed in, which
// are stored in the 'additional' property in LayoutDefinition. The variadic
// 'additional' parameter here saves arbitrary parameters passed into the
// 'additional' property in this attribute class. The 'additional' property
// gets passed to the LayoutDefinition constructor in ::get().
// @see \Drupal\Core\Layout\LayoutDefinition::$additional
// @see \Drupal\Core\Layout\LayoutDefinition::get()
$this->additional = $additional;
}
/**
* {@inheritdoc}
*/
public function get(): LayoutDefinition {
return new LayoutDefinition(parent::get());
}
}

View File

@ -132,7 +132,7 @@ class LayoutDefinition extends PluginDefinition implements PluginDefinitionInter
* LayoutDefinition constructor.
*
* @param array $definition
* An array of values from the annotation.
* An array of values from the attribute.
*/
public function __construct(array $definition) {
// If there are context definitions in the plugin definition, they should

View File

@ -2,16 +2,16 @@
namespace Drupal\Core\Layout;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
use Drupal\Core\Layout\Annotation\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Plugin\FilteredPluginManagerTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
@ -43,7 +43,7 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
* The theme handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class);
parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class, 'Drupal\Core\Layout\Annotation\Layout');
$this->themeHandler = $theme_handler;
$type = $this->getType();
@ -70,13 +70,13 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
*/
protected function getDiscovery() {
if (!$this->discovery) {
$discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
$discovery = new AttributeDiscoveryWithAnnotations($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
$discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
$discovery
->addTranslatableProperty('label')
->addTranslatableProperty('description')
->addTranslatableProperty('category');
$discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
$discovery = new AttributeBridgeDecorator($discovery, $this->pluginDefinitionAttributeName);
$discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
$this->discovery = $discovery;
}

View File

@ -2,26 +2,27 @@
namespace Drupal\field_layout_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides an annotated layout plugin for field_layout tests.
*
* @Layout(
* id = "test_layout_content_and_footer",
* label = @Translation("Test plugin: Content and Footer"),
* category = @Translation("Layout test"),
* description = @Translation("Test layout"),
* regions = {
* "content" = {
* "label" = @Translation("Content Region")
* },
* "footer" = {
* "label" = @Translation("Footer Region")
* }
* },
* )
* Provides a Layout plugin for field_layout tests.
*/
#[Layout(
id: 'test_layout_content_and_footer',
label: new TranslatableMarkup('Test plugin: Content and Footer'),
category: new TranslatableMarkup('Layout test'),
description: new TranslatableMarkup('Test layout'),
regions: [
"content" => [
"label" => new TranslatableMarkup("Content Region"),
],
"footer" => [
"label" => new TranslatableMarkup("Footer Region"),
],
],
)]
class TestLayoutContentFooter extends LayoutDefault {
}

View File

@ -2,31 +2,32 @@
namespace Drupal\field_layout_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides an annotated layout plugin for field_layout tests.
*
* @Layout(
* id = "test_layout_main_and_footer",
* label = @Translation("Test plugin: Main and Footer"),
* category = @Translation("Layout test"),
* description = @Translation("Test layout"),
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* },
* "footer" = {
* "label" = @Translation("Footer Region")
* }
* },
* config_dependencies = {
* "module" = {
* "layout_discovery",
* },
* },
* )
* Provides an attributed layout plugin for field_layout tests.
*/
#[Layout(
id: 'test_layout_main_and_footer',
label: new TranslatableMarkup('Test plugin: Main and Footer'),
category: new TranslatableMarkup('Layout test'),
description: new TranslatableMarkup('Test layout'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
"footer" => [
"label" => new TranslatableMarkup("Footer Region"),
],
],
config_dependencies: [
"module" => [
"layout_discovery",
],
],
)]
class TestLayoutMainFooter extends LayoutDefault {
/**

View File

@ -15,7 +15,7 @@
*
* By default, the Layout Builder access check requires the 'configure any
* layout' permission. Individual section storage plugins may override this by
* setting the 'handles_permission_check' annotation key to TRUE. Any section
* setting the 'handles_permission_check' attribute key to TRUE. Any section
* storage plugin that uses 'handles_permission_check' must provide its own
* complete routing access checking to avoid any access bypasses.
*

View File

@ -2,7 +2,9 @@
namespace Drupal\layout_builder\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a layout plugin that produces no output.
@ -13,11 +15,11 @@ use Drupal\Core\Layout\LayoutDefault;
*
* @internal
* This layout plugin is intended for internal use by Layout Builder only.
*
* @Layout(
* id = "layout_builder_blank",
* )
*/
#[Layout(
id: 'layout_builder_blank',
label: new TranslatableMarkup('Blank'),
)]
class BlankLayout extends LayoutDefault {
/**

View File

@ -2,19 +2,22 @@
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @Layout(
* id = "layout_builder_test_plugin",
* label = @Translation("Layout Builder Test Plugin"),
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* }
* },
* )
* The Layout Builder Test Plugin.
*/
#[Layout(
id: 'layout_builder_test_plugin',
label: new TranslatableMarkup('Layout Builder Test Plugin'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutBuilderTestPlugin extends LayoutDefault {
/**

View File

@ -3,21 +3,22 @@
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Layout plugin without a label configuration.
*
* @Layout(
* id = "layout_without_label",
* label = @Translation("Layout Without Label"),
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* }
* },
* )
*/
#[Layout(
id: 'layout_without_label',
label: new TranslatableMarkup('Layout Without Label'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutWithoutLabel extends LayoutDefault {
/**

View File

@ -2,22 +2,26 @@
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* @Layout(
* id = "layout_builder_test_context_aware",
* label = @Translation("Layout Builder Test: Context Aware"),
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* }
* },
* context_definitions = {
* "user" = @ContextDefinition("entity:user")
* }
* )
* The TestContextAwareLayout Class.
*/
#[Layout(
id: 'layout_builder_test_context_aware',
label: new TranslatableMarkup('Layout Builder Test: Context Aware'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
context_definitions: [
"user" => new EntityContextDefinition("entity:user"),
],
)]
class TestContextAwareLayout extends LayoutDefault {
/**

View File

@ -3,23 +3,24 @@
namespace Drupal\layout_test\Plugin\Layout;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a plugin that contains config dependencies.
*
* @Layout(
* id = "layout_test_dependencies_plugin",
* label = @Translation("Layout plugin (with dependencies)"),
* category = @Translation("Layout test"),
* description = @Translation("Test layout"),
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* }
* }
* )
*/
#[Layout(
id: 'layout_test_dependencies_plugin',
label: new TranslatableMarkup('Layout plugin (with dependencies)'),
category: new TranslatableMarkup('Layout test'),
description: new TranslatableMarkup('Test layout'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutTestDependenciesPlugin extends LayoutDefault implements DependentPluginInterface {
/**

View File

@ -3,25 +3,26 @@
namespace Drupal\layout_test\Plugin\Layout;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* The plugin that handles the default layout template.
*
* @Layout(
* id = "layout_test_plugin",
* label = @Translation("Layout plugin (with settings)"),
* category = @Translation("Layout test"),
* description = @Translation("Test layout"),
* template = "templates/layout-test-plugin",
* regions = {
* "main" = {
* "label" = @Translation("Main Region")
* }
* }
* )
*/
#[Layout(
id: 'layout_test_plugin',
label: new TranslatableMarkup('Layout plugin (with settings)'),
category: new TranslatableMarkup('Layout test'),
description: new TranslatableMarkup('Test layout'),
template: "templates/layout-test-plugin",
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutTestPlugin extends LayoutDefault implements PluginFormInterface {
/**

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Layout;
use Composer\Autoload\ClassLoader;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Cache\CacheBackendInterface;
@ -20,6 +21,8 @@ use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
// cspell:ignore lorem, ipsum, consectetur, adipiscing
/**
* @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager
* @group Layout
@ -91,6 +94,9 @@ class LayoutPluginManagerTest extends UnitTestCase {
$this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
$namespaces = new \ArrayObject(['Drupal\Core' => vfsStream::url('root/core/lib/Drupal/Core')]);
$class_loader = new ClassLoader();
$class_loader->addPsr4("Drupal\\Core\\", vfsStream::url("root/core/lib/Drupal/Core"));
$class_loader->register(TRUE);
$this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend->reveal(), $this->moduleHandler->reveal(), $this->themeHandler->reveal());
}
@ -103,6 +109,7 @@ class LayoutPluginManagerTest extends UnitTestCase {
'module_a_provided_layout',
'theme_a_provided_layout',
'plugin_provided_layout',
'plugin_provided_by_annotation_layout',
];
$layout_definitions = $this->layoutPluginManager->getDefinitions();
@ -172,6 +179,8 @@ class LayoutPluginManagerTest extends UnitTestCase {
$this->assertEquals($expected_regions, $regions);
$this->assertInstanceOf(TranslatableMarkup::class, $regions['top']['label']);
$this->assertInstanceOf(TranslatableMarkup::class, $regions['bottom']['label']);
// Check that arbitrary property value gets set correctly.
$this->assertSame('ipsum', $layout_definition->get('lorem'));
$core_path = '/core/lib/Drupal/Core';
$layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
@ -198,6 +207,37 @@ class LayoutPluginManagerTest extends UnitTestCase {
$regions = $layout_definition->getRegions();
$this->assertEquals($expected_regions, $regions);
$this->assertInstanceOf(TranslatableMarkup::class, $regions['main']['label']);
// Check that arbitrary property value gets set correctly.
$this->assertSame('adipiscing', $layout_definition->get('consectetur'));
$layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_by_annotation_layout');
$this->assertSame('plugin_provided_by_annotation_layout', $layout_definition->id());
$this->assertEquals('Layout by annotation plugin', $layout_definition->getLabel());
$this->assertEquals('Columns: 2', $layout_definition->getCategory());
$this->assertEquals('Test layout provided by annotated plugin', $layout_definition->getDescription());
$this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getLabel());
$this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getCategory());
$this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getDescription());
$this->assertSame('plugin-provided-annotation-layout', $layout_definition->getTemplate());
$this->assertSame($core_path, $layout_definition->getPath());
$this->assertNull($layout_definition->getLibrary());
$this->assertSame('plugin_provided_annotation_layout', $layout_definition->getThemeHook());
$this->assertSame("$core_path/templates", $layout_definition->getTemplatePath());
$this->assertSame('core', $layout_definition->getProvider());
$this->assertSame('left', $layout_definition->getDefaultRegion());
$this->assertSame('Drupal\Core\Plugin\Layout\TestAnnotationLayout', $layout_definition->getClass());
$expected_regions = [
'left' => [
'label' => new TranslatableMarkup('Left Region', [], ['context' => 'layout_region']),
],
'right' => [
'label' => new TranslatableMarkup('Right Region', [], ['context' => 'layout_region']),
],
];
$regions = $layout_definition->getRegions();
$this->assertEquals($expected_regions, $regions);
$this->assertInstanceOf(TranslatableMarkup::class, $regions['left']['label']);
$this->assertInstanceOf(TranslatableMarkup::class, $regions['right']['label']);
}
/**
@ -243,6 +283,12 @@ EOS;
'template' => 'plugin-provided-layout',
'path' => "$core_path/templates",
],
'plugin_provided_annotation_layout' => [
'render element' => 'content',
'base hook' => 'layout',
'template' => 'plugin-provided-annotation-layout',
'path' => "$core_path/templates",
],
];
$theme_implementations = $this->layoutPluginManager->getThemeImplementations();
$this->assertEquals($expected, $theme_implementations);
@ -264,10 +310,12 @@ EOS;
* @covers ::getSortedDefinitions
*/
public function testGetSortedDefinitions() {
// Sorted by category first, then label.
$expected = [
'module_a_provided_layout',
'plugin_provided_layout',
'theme_a_provided_layout',
'plugin_provided_by_annotation_layout',
];
$layout_definitions = $this->layoutPluginManager->getSortedDefinitions();
@ -286,6 +334,7 @@ EOS;
],
'Columns: 2' => [
'theme_a_provided_layout',
'plugin_provided_by_annotation_layout',
],
];
@ -315,7 +364,9 @@ module_a_provided_layout:
label: Top region
bottom:
label: Bottom region
lorem: ipsum
module_a_derived_layout:
label: 'Invalid provider derived layout'
deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
invalid_provider: true
EOS;
@ -338,23 +389,52 @@ EOS;
$plugin_provided_layout = <<<'EOS'
<?php
namespace Drupal\Core\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* The TestLayout Class.
*/
#[Layout(
id: 'plugin_provided_layout',
label: new TranslatableMarkup('Layout plugin'),
category: new TranslatableMarkup('Columns: 1'),
description: new TranslatableMarkup('Test layout'),
path: "core/lib/Drupal/Core",
template: "templates/plugin-provided-layout",
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region", [], ["context" => "layout_region"]),
],
],
consectetur: 'adipiscing',
)]
class TestLayout extends LayoutDefault {}
EOS;
$plugin_provided_by_annotation_layout = <<<'EOS'
<?php
namespace Drupal\Core\Plugin\Layout;
use Drupal\Core\Layout\LayoutDefault;
/**
* @Layout(
* id = "plugin_provided_layout",
* label = @Translation("Layout plugin"),
* category = @Translation("Columns: 1"),
* description = @Translation("Test layout"),
* id = "plugin_provided_by_annotation_layout",
* label = @Translation("Layout by annotation plugin"),
* category = @Translation("Columns: 2"),
* description = @Translation("Test layout provided by annotated plugin"),
* path = "core/lib/Drupal/Core",
* template = "templates/plugin-provided-layout",
* template = "templates/plugin-provided-annotation-layout",
* default_region = "left",
* regions = {
* "main" = {
* "label" = @Translation("Main Region", context = "layout_region")
* "left" = {
* "label" = @Translation("Left Region", context = "layout_region")
* },
* "right" = {
* "label" = @Translation("Right Region", context = "layout_region")
* }
* }
* )
*/
class TestLayout extends LayoutDefault {}
class TestAnnotationLayout extends LayoutDefault {}
EOS;
vfsStream::setup('root');
vfsStream::create([
@ -379,6 +459,7 @@ EOS;
'Plugin' => [
'Layout' => [
'TestLayout.php' => $plugin_provided_layout,
'TestAnnotationLayout.php' => $plugin_provided_by_annotation_layout,
],
],
],