From e71b1d399634bde945495685b040b23d23bc973e Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Thu, 13 Feb 2025 19:11:04 +0000
Subject: [PATCH] Issue #3506511: Use attributes for source plugin definitions

---
 .../project_browser.admin_settings.yml        |  1 +
 .../ProjectBrowserSource/RandomDataPlugin.php | 19 +++---
 .../ProjectBrowserSourceExample.php           | 13 ++--
 project_browser.install                       | 18 ------
 project_browser.module                        | 19 ------
 src/Annotation/ProjectBrowserSource.php       | 60 -------------------
 src/Attribute/ProjectBrowserSource.php        | 41 +++++++++++++
 .../ProjectBrowserSource/DrupalCore.php       | 21 +++----
 .../DrupalDotOrgJsonApi.php                   | 19 +++---
 src/Plugin/ProjectBrowserSource/Recipes.php   | 10 ++++
 src/Plugin/ProjectBrowserSourceManager.php    |  3 +-
 .../ProjectBrowserTestMock.php                | 19 +++---
 tests/src/Kernel/RecipesSourceTest.php        | 19 ------
 13 files changed, 100 insertions(+), 162 deletions(-)
 delete mode 100644 src/Annotation/ProjectBrowserSource.php
 create mode 100644 src/Attribute/ProjectBrowserSource.php

diff --git a/config/install/project_browser.admin_settings.yml b/config/install/project_browser.admin_settings.yml
index 1abc75ca3..e90d04658 100644
--- a/config/install/project_browser.admin_settings.yml
+++ b/config/install/project_browser.admin_settings.yml
@@ -1,5 +1,6 @@
 enabled_sources:
   - drupalorg_jsonapi
+  - recipes
 allow_ui_install: false
 allowed_projects: {}
 max_selections: null
diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
index 188a5ca34..2ca646992 100644
--- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
+++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
@@ -4,7 +4,9 @@ namespace Drupal\project_browser_devel\Plugin\ProjectBrowserSource;
 
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter;
 use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter;
@@ -14,18 +16,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Random data plugin. Used mostly for testing.
- *
- * To enable this source use the following drush command.
- * phpcs:ignore
- *   drush config:set project_browser.admin_settings enabled_source random_data
- *
- * @ProjectBrowserSource(
- *   id = "random_data",
- *   label = @Translation("Random data"),
- *   description = @Translation("Gets random project and filters information"),
- *   local_task = {}
- * )
  */
+#[ProjectBrowserSource(
+  id: 'random_data',
+  label: new TranslatableMarkup('Random data'),
+  description: new TranslatableMarkup('Gets random project and filters information'),
+  local_task: [],
+)]
 final class RandomDataPlugin extends ProjectBrowserSourceBase {
 
   /**
diff --git a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
index 6d5b207e5..375f6b633 100644
--- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
+++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\project_browser_source_example\Plugin\ProjectBrowserSource;
 
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
@@ -11,13 +13,12 @@ use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Project Browser Source Plugin example code.
- *
- * @ProjectBrowserSource(
- *   id = "project_browser_source_example",
- *   label = @Translation("Example source"),
- *   description = @Translation("Example source plugin for Project Browser."),
- * )
  */
+#[ProjectBrowserSource(
+  id: 'project_browser_source_example',
+  label: new TranslatableMarkup('Example source'),
+  description: new TranslatableMarkup('Example source plugin for Project Browser.'),
+)]
 final class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
 
   /**
diff --git a/project_browser.install b/project_browser.install
index 8c5e62d7a..6c142cadb 100644
--- a/project_browser.install
+++ b/project_browser.install
@@ -5,24 +5,6 @@
  * Contains install and update functions for Project Browser.
  */
 
-use Drupal\Core\Recipe\Recipe;
-
-/**
- * Implements hook_install().
- *
- * Populates the project_browser_projects using a fixture with PHP serialized
- * items.
- */
-function project_browser_install(): void {
-  if (class_exists(Recipe::class)) {
-    $config = \Drupal::configFactory()
-      ->getEditable('project_browser.admin_settings');
-    $enabled_sources = $config->get('enabled_sources');
-    $enabled_sources[] = 'recipes';
-    $config->set('enabled_sources', $enabled_sources)->save();
-  }
-}
-
 /**
  * Implements hook_update_last_removed().
  */
diff --git a/project_browser.module b/project_browser.module
index e168a2f1c..5ff0bb0b2 100644
--- a/project_browser.module
+++ b/project_browser.module
@@ -5,10 +5,8 @@
  * Provides hook implementations for the Project Browser module.
  */
 
-use Drupal\Core\Recipe\Recipe;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
-use Drupal\project_browser\Plugin\ProjectBrowserSource\Recipes;
 
 /**
  * Implements hook_help().
@@ -43,23 +41,6 @@ function project_browser_theme(): array {
   ];
 }
 
-/**
- * Implements hook_project_browser_source_info_alter().
- */
-function project_browser_project_browser_source_info_alter(array &$definitions): void {
-  if (class_exists(Recipe::class)) {
-    $definitions['recipes'] = [
-      'id' => 'recipes',
-      'label' => t('Recipes'),
-      'description' => t('Recipes available in the local code base'),
-      'local_task' => [
-        'weight' => ($definitions['drupalorg_jsonapi']['local_task']['weight'] ?? 5) + 2,
-      ],
-      'class' => Recipes::class,
-    ];
-  }
-}
-
 /**
  * Preprocess function for the project_browser_main_app theme hook.
  *
diff --git a/src/Annotation/ProjectBrowserSource.php b/src/Annotation/ProjectBrowserSource.php
deleted file mode 100644
index aab59a016..000000000
--- a/src/Annotation/ProjectBrowserSource.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-namespace Drupal\project_browser\Annotation;
-
-use Drupal\Component\Annotation\Plugin;
-
-/**
- * Defines a Project Browser source plugin annotation object.
- *
- * Project Browser sources are used to provide information about
- * available projects that can be installed on a Drupal site.
- * Typically, these come from Drupal.org, but may also come
- * from a private repository, etc.
- *
- * Plugin Namespace: Plugin\ProjectBrowserSource
- *
- * For a working example, see:
- * \Drupal\project_browser\Plugin\ProjectBrowserSource\MockDrupalDotOrg
- *
- * @see \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface
- * @see \Drupal\project_browser\Plugin\ProjectBrowserSourceManager
- * @see plugin_api
- *
- * @Annotation
- */
-class ProjectBrowserSource extends Plugin {
-
-  /**
-   * The plugin ID.
-   *
-   * @var string
-   */
-  public $id;
-
-  /**
-   * The human-readable name of the source.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   * @ingroup plugin_translatable
-   */
-  public $label;
-
-  /**
-   * A short description of the source.
-   *
-   * @var \Drupal\Core\Annotation\Translation
-   * @ingroup plugin_translatable
-   */
-  public $description;
-
-  /**
-   * The local task definition at which this source should be exposed.
-   *
-   * If NULL, the source will never be exposed as a local task.
-   *
-   * @var array|null
-   */
-  public ?array $local_task = NULL;
-
-}
diff --git a/src/Attribute/ProjectBrowserSource.php b/src/Attribute/ProjectBrowserSource.php
new file mode 100644
index 000000000..f7b738a73
--- /dev/null
+++ b/src/Attribute/ProjectBrowserSource.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\project_browser\Attribute;
+
+use Drupal\Component\Plugin\Attribute\Plugin;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+
+/**
+ * Defines an attribute to identify Project Browser source plugins.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class ProjectBrowserSource extends Plugin {
+
+  /**
+   * Constructs a ProjectBrowserSource attribute.
+   *
+   * @param string $id
+   *   The plugin ID.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup $label
+   *   The plugin's human-readable name.
+   * @param \Drupal\Core\StringTranslation\TranslatableMarkup $description
+   *   A brief description of the plugin and what it does.
+   * @param array|null $local_task
+   *   A local task definition at which this source should be exposed in the UI.
+   *   If NULL, the source will not be shown as a local task.
+   * @param class-string|null $deriver
+   *   The plugin's deriver class, if any.
+   */
+  public function __construct(
+    string $id,
+    public TranslatableMarkup $label,
+    public TranslatableMarkup $description,
+    public ?array $local_task = NULL,
+    ?string $deriver = NULL,
+  ) {
+    parent::__construct($id, $deriver);
+  }
+
+}
diff --git a/src/Plugin/ProjectBrowserSource/DrupalCore.php b/src/Plugin/ProjectBrowserSource/DrupalCore.php
index 48868d196..d71c36c67 100644
--- a/src/Plugin/ProjectBrowserSource/DrupalCore.php
+++ b/src/Plugin/ProjectBrowserSource/DrupalCore.php
@@ -6,24 +6,25 @@ use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * The source plugin to get Drupal core projects list.
- *
- * @ProjectBrowserSource(
- *   id = "drupal_core",
- *   label = @Translation("Core modules"),
- *   description = @Translation("Modules included in Drupal core"),
- *   local_task = {
- *     "title" = @Translation("Core modules"),
- *   }
- * )
+ * A source that lists Drupal core modules.
  */
+#[ProjectBrowserSource(
+  id: 'drupal_core',
+  label: new TranslatableMarkup('Core modules'),
+  description: new TranslatableMarkup('Modules included in Drupal core'),
+  local_task: [
+    'title' => new TranslatableMarkup('Core modules'),
+  ],
+)]
 final class DrupalCore extends ProjectBrowserSourceBase {
 
   public function __construct(
diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
index 8e53b48ee..69074fedd 100644
--- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
+++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
@@ -9,7 +9,9 @@ use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ExtensionVersion;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter;
 use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter;
@@ -23,16 +25,15 @@ use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Drupal.org JSON:API endpoint.
- *
- * @ProjectBrowserSource(
- *   id = "drupalorg_jsonapi",
- *   label = @Translation("Contrib modules"),
- *   description = @Translation("Modules on Drupal.org queried via the JSON:API endpoint"),
- *   local_task = {
- *     "title" = @Translation("Contrib modules"),
- *   }
- * )
  */
+#[ProjectBrowserSource(
+  id: 'drupalorg_jsonapi',
+  label: new TranslatableMarkup('Contrib modules'),
+  description: new TranslatableMarkup('Modules on Drupal.org queried via the JSON:API endpoint'),
+  local_task: [
+    'title' => new TranslatableMarkup('Contrib modules'),
+  ],
+)]
 final class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
 
   use StringTranslationTrait;
diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php
index b5bf79c62..f8283a8a3 100644
--- a/src/Plugin/ProjectBrowserSource/Recipes.php
+++ b/src/Plugin/ProjectBrowserSource/Recipes.php
@@ -13,7 +13,9 @@ use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Recipe\Recipe;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
@@ -24,6 +26,14 @@ use Symfony\Component\Finder\Finder;
 /**
  * A source plugin that exposes recipes installed locally.
  */
+#[ProjectBrowserSource(
+  id: 'recipes',
+  label: new TranslatableMarkup('Recipes'),
+  description: new TranslatableMarkup('Recipes available in the local code base'),
+  local_task: [
+    'weight' => 2,
+  ]
+)]
 final class Recipes extends ProjectBrowserSourceBase {
 
   public function __construct(
diff --git a/src/Plugin/ProjectBrowserSourceManager.php b/src/Plugin/ProjectBrowserSourceManager.php
index e7b10717f..96c93384d 100644
--- a/src/Plugin/ProjectBrowserSourceManager.php
+++ b/src/Plugin/ProjectBrowserSourceManager.php
@@ -5,6 +5,7 @@ namespace Drupal\project_browser\Plugin;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 
 /**
  * Provides a Project Browser Source Manager.
@@ -32,7 +33,7 @@ class ProjectBrowserSourceManager extends DefaultPluginManager {
       $namespaces,
       $module_handler,
       'Drupal\project_browser\Plugin\ProjectBrowserSourceInterface',
-      'Drupal\project_browser\Annotation\ProjectBrowserSource',
+      ProjectBrowserSource::class,
     );
 
     $this->alterInfo('project_browser_source_info');
diff --git a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
index 4894e234b..72a77815f 100644
--- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
+++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
@@ -9,7 +9,9 @@ use Drupal\Component\Utility\Html;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\project_browser\Attribute\ProjectBrowserSource;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter;
 use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter;
@@ -22,16 +24,15 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Database driven plugin.
- *
- * @ProjectBrowserSource(
- *   id = "project_browser_test_mock",
- *   label = @Translation("Project Browser Mock Plugin"),
- *   description = @Translation("Gets project and filters information from a database"),
- *   local_task = {
- *     "title" = @Translation("Browse"),
- *   }
- * )
  */
+#[ProjectBrowserSource(
+  id: 'project_browser_test_mock',
+  label: new TranslatableMarkup('Project Browser Mock Plugin'),
+  description: new TranslatableMarkup('Gets project and filters information from a database'),
+  local_task: [
+    'title' => new TranslatableMarkup('Browse'),
+  ],
+)]
 final class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
 
   /**
diff --git a/tests/src/Kernel/RecipesSourceTest.php b/tests/src/Kernel/RecipesSourceTest.php
index cd8818901..e167d58bc 100644
--- a/tests/src/Kernel/RecipesSourceTest.php
+++ b/tests/src/Kernel/RecipesSourceTest.php
@@ -5,10 +5,8 @@ declare(strict_types=1);
 namespace Drupal\Tests\project_browser\Kernel;
 
 use Drupal\Component\FileSystem\FileSystem;
-use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Recipe\Recipe;
 use Drupal\KernelTests\KernelTestBase;
-use Drupal\project_browser\EnabledSourceHandler;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceManager;
 use Drupal\project_browser\ProjectType;
 use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
@@ -44,23 +42,6 @@ class RecipesSourceTest extends KernelTestBase {
     $this->installConfig('project_browser');
   }
 
-  /**
-   * @covers \project_browser_install
-   * @covers \project_browser_project_browser_source_info_alter
-   */
-  public function testRecipeSourceIsEnabledAtInstallTime(): void {
-    $this->assertNotContains('recipes', $this->config('project_browser.admin_settings')->get('enabled_sources'));
-
-    $this->container->get(ModuleHandlerInterface::class)
-      ->loadInclude('project_browser', 'install');
-    project_browser_install();
-    $this->assertContains('recipes', $this->config('project_browser.admin_settings')->get('enabled_sources'));
-
-    $enabled_sources = $this->container->get(EnabledSourceHandler::class)
-      ->getCurrentSources();
-    $this->assertArrayHasKey('recipes', $enabled_sources);
-  }
-
   /**
    * Tests that recipes are discovered by the plugin.
    */
-- 
GitLab