From 091483c1f1ab020f42df9f892a7c2fda596dd263 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 3 May 2024 09:01:25 +0100
Subject: [PATCH] Issue #3420983 by sorlov, godotislate, alexpott, quietone,
 kim.pepper, larowlan, mstrelan: Convert Layout plugin discovery to attributes

---
 .../Drupal/Core/Layout/Attribute/Layout.php   | 111 ++++++++++++++++++
 .../Drupal/Core/Layout/LayoutDefinition.php   |   2 +-
 .../Core/Layout/LayoutPluginManager.php       |  12 +-
 .../Plugin/Layout/TestLayoutContentFooter.php |  33 +++---
 .../Plugin/Layout/TestLayoutMainFooter.php    |  43 +++----
 .../layout_builder/layout_builder.api.php     |   2 +-
 .../src/Plugin/Layout/BlankLayout.php         |  10 +-
 .../Plugin/Layout/LayoutBuilderTestPlugin.php |  21 ++--
 .../src/Plugin/Layout/LayoutWithoutLabel.php  |  21 ++--
 .../Plugin/Layout/TestContextAwareLayout.php  |  28 +++--
 .../Layout/LayoutTestDependenciesPlugin.php   |  25 ++--
 .../src/Plugin/Layout/LayoutTestPlugin.php    |  27 +++--
 .../Core/Layout/LayoutPluginManagerTest.php   |  97 +++++++++++++--
 13 files changed, 319 insertions(+), 113 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Layout/Attribute/Layout.php

diff --git a/core/lib/Drupal/Core/Layout/Attribute/Layout.php b/core/lib/Drupal/Core/Layout/Attribute/Layout.php
new file mode 100644
index 000000000000..11bf8a206cf6
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Attribute/Layout.php
@@ -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());
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutDefinition.php b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
index 9936fcaa0d9d..11cbf5143662 100644
--- a/core/lib/Drupal/Core/Layout/LayoutDefinition.php
+++ b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
@@ -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
diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
index 2265194bf30b..5e8f2f3b2ae6 100644
--- a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
+++ b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
@@ -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 @@ protected function providerExists($provider) {
    */
   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;
     }
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php
index c283c82d7875..75d7e384f5f9 100644
--- a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php
+++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php
@@ -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 {
 
 }
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php
index b01ec4518751..9a0df30325f4 100644
--- a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php
+++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php
@@ -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 {
 
   /**
diff --git a/core/modules/layout_builder/layout_builder.api.php b/core/modules/layout_builder/layout_builder.api.php
index 66afc1fb175a..d7e84c6a26b4 100644
--- a/core/modules/layout_builder/layout_builder.api.php
+++ b/core/modules/layout_builder/layout_builder.api.php
@@ -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.
  *
diff --git a/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php b/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php
index a3f71b0ef9c6..7d5b83e3c8a6 100644
--- a/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php
+++ b/core/modules/layout_builder/src/Plugin/Layout/BlankLayout.php
@@ -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 @@
  *
  * @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 {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutBuilderTestPlugin.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutBuilderTestPlugin.php
index 26aafb0d8ed7..c0b16b56b79e 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutBuilderTestPlugin.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutBuilderTestPlugin.php
@@ -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 {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutWithoutLabel.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutWithoutLabel.php
index 9b32bbb1b90e..6298ca51941c 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutWithoutLabel.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/LayoutWithoutLabel.php
@@ -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 {
 
   /**
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/TestContextAwareLayout.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/TestContextAwareLayout.php
index b53e954c8703..21becc380247 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/TestContextAwareLayout.php
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/Layout/TestContextAwareLayout.php
@@ -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 {
 
   /**
diff --git a/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestDependenciesPlugin.php b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestDependenciesPlugin.php
index 6ad62101dd96..c82d18f46dcb 100644
--- a/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestDependenciesPlugin.php
+++ b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestDependenciesPlugin.php
@@ -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 {
 
   /**
diff --git a/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
index 4598dccbf40a..7eb3450ec501 100644
--- a/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
+++ b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
@@ -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 {
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
index 7ce72081d8e3..7610bf16bf58 100644
--- a/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
@@ -4,6 +4,7 @@
 
 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 org\bovigo\vfs\vfsStream;
 use Prophecy\Argument;
 
+// cspell:ignore lorem, ipsum, consectetur, adipiscing
+
 /**
  * @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager
  * @group Layout
@@ -91,6 +94,9 @@ protected function setUp(): void {
     $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 @@ public function testGetDefinitions() {
       '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 @@ public function testGetDefinition() {
     $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 @@ public function testGetDefinition() {
     $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 @@ public function testGetThemeImplementations() {
         '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 @@ public function testGetCategories() {
    * @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 @@ public function testGetGroupedDefinitions() {
       ],
       'Columns: 2' => [
         'theme_a_provided_layout',
+        'plugin_provided_by_annotation_layout',
       ],
     ];
 
@@ -315,7 +364,9 @@ protected function setUpFilesystem() {
       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 @@ class: '\Drupal\Core\Layout\LayoutDefault'
     $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 @@ class TestLayout extends LayoutDefault {}
               'Plugin' => [
                 'Layout' => [
                   'TestLayout.php' => $plugin_provided_layout,
+                  'TestAnnotationLayout.php' => $plugin_provided_by_annotation_layout,
                 ],
               ],
             ],
-- 
GitLab