From 15b871ae20dc49f20fa74feee5161df809d385db Mon Sep 17 00:00:00 2001
From: "kunal.sachdev" <kunal.sachdev@3685163.no-reply.drupal.org>
Date: Tue, 5 Apr 2022 18:16:44 +0000
Subject: [PATCH] Issue #3253858 by kunal.sachdev, phenaproxima, tedbow: In
 ComposerExecutableValidator provide in explanation about how you can set the
 path to the Composer executable

---
 package_manager/package_manager.module        |  2 +-
 package_manager/package_manager.services.yml  |  1 +
 .../Validator/ComposerExecutableValidator.php | 54 +++++++++++++++----
 .../ComposerExecutableValidatorTest.php       | 35 ++++++++++++
 4 files changed, 80 insertions(+), 12 deletions(-)

diff --git a/package_manager/package_manager.module b/package_manager/package_manager.module
index c54269edfc..338a3ce447 100644
--- a/package_manager/package_manager.module
+++ b/package_manager/package_manager.module
@@ -17,7 +17,7 @@ function package_manager_help($route_name, RouteMatchInterface $route_match) {
       $output = '<h3>' . t('About') . '</h3>';
       $output .= '<p>' . t('Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.') . '</p>';
       $output .= '<p>' . t('Package Manager dispatches events before and after various operations, and external code can integrate with it by subscribing to those events. For more information, see <code>package_manager.api.php</code>.') . '</p>';
-      $output .= '<h3>' . t('Requirements') . '</h3>';
+      $output .= '<h3 id="package-manager-requirements">' . t('Requirements') . '</h3>';
       $output .= '<p>' . t('Package Manager requires Composer @version or later available as an executable, and PHP must have permission to run it. The path to the executable may be set in the <code>package_manager.settings:executables.composer</code> config setting, or it will be automatically detected.', ['@version' => ComposerExecutableValidator::MINIMUM_COMPOSER_VERSION]) . '</p>';
       $output .= '<h3>' . t('Limitations') . '</h3>';
       $output .= '<p>' . t("Because Package Manager modifies the current site's code base, it is intentionally limited in certain ways to prevent unexpected changes from being made to the live site:") . '</p>';
diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index 75ae35d2ec..3fa0bfa707 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -80,6 +80,7 @@ services:
     class: Drupal\package_manager\Validator\ComposerExecutableValidator
     arguments:
       - '@package_manager.composer_runner'
+      - '@module_handler'
       - '@string_translation'
     tags:
       - { name: event_subscriber }
diff --git a/package_manager/src/Validator/ComposerExecutableValidator.php b/package_manager/src/Validator/ComposerExecutableValidator.php
index f2e4e46157..06c7104e22 100644
--- a/package_manager/src/Validator/ComposerExecutableValidator.php
+++ b/package_manager/src/Validator/ComposerExecutableValidator.php
@@ -3,6 +3,8 @@
 namespace Drupal\package_manager\Validator;
 
 use Composer\Semver\Comparator;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Url;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Event\PreOperationStageEvent;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -32,6 +34,13 @@ class ComposerExecutableValidator implements PreOperationStageValidatorInterface
    */
   protected $composer;
 
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * The detected version of Composer.
    *
@@ -44,11 +53,14 @@ class ComposerExecutableValidator implements PreOperationStageValidatorInterface
    *
    * @param \PhpTuf\ComposerStager\Domain\Process\Runner\ComposerRunnerInterface $composer
    *   The Composer runner.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
    *   The translation service.
    */
-  public function __construct(ComposerRunnerInterface $composer, TranslationInterface $translation) {
+  public function __construct(ComposerRunnerInterface $composer, ModuleHandlerInterface $module_handler, TranslationInterface $translation) {
     $this->composer = $composer;
+    $this->moduleHandler = $module_handler;
     $this->setStringTranslation($translation);
   }
 
@@ -60,27 +72,47 @@ class ComposerExecutableValidator implements PreOperationStageValidatorInterface
       $this->composer->run(['--version'], $this);
     }
     catch (ExceptionInterface $e) {
-      $event->addError([
-        $e->getMessage(),
-      ]);
+      $this->setError($e->getMessage(), $event);
       return;
     }
 
     if ($this->version) {
       if (Comparator::lessThan($this->version, static::MINIMUM_COMPOSER_VERSION)) {
-        $event->addError([
-          $this->t('Composer @minimum_version or later is required, but version @detected_version was detected.', [
-            '@minimum_version' => static::MINIMUM_COMPOSER_VERSION,
-            '@detected_version' => $this->version,
-          ]),
+        $message = $this->t('Composer @minimum_version or later is required, but version @detected_version was detected.', [
+          '@minimum_version' => static::MINIMUM_COMPOSER_VERSION,
+          '@detected_version' => $this->version,
         ]);
+        $this->setError($message, $event);
       }
     }
     else {
-      $event->addError([
-        $this->t('The Composer version could not be detected.'),
+      $this->setError($this->t('The Composer version could not be detected.'), $event);
+    }
+  }
+
+  /**
+   * Flags a validation error.
+   *
+   * @param string $message
+   *   The error message. If the Help module is enabled, a link to Package
+   *   Manager's online documentation will be appended.
+   * @param \Drupal\package_manager\Event\PreOperationStageEvent $event
+   *   The event object.
+   *
+   * @see package_manager_help()
+   */
+  protected function setError(string $message, PreOperationStageEvent $event): void {
+    if ($this->moduleHandler->moduleExists('help')) {
+      $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
+        ->setOption('fragment', 'package-manager-requirements')
+        ->toString();
+
+      $message .= ' ';
+      $message .= $this->t('See <a href=":package-manager-help">the help page</a> for information on how to configure the path to Composer.', [
+        ':package-manager-help' => $url,
       ]);
     }
+    $event->addError([$message]);
   }
 
   /**
diff --git a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
index 9b8236a786..004e37e5b0 100644
--- a/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
+++ b/package_manager/tests/src/Kernel/ComposerExecutableValidatorTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\package_manager\Kernel;
 
+use Drupal\Core\Url;
 use Drupal\package_manager\Event\PreCreateEvent;
 use Drupal\package_manager\Validator\ComposerExecutableValidator;
 use Drupal\package_manager\ValidationResult;
@@ -35,6 +36,10 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
       $exception->getMessage(),
     ]);
     $this->assertResults([$error], PreCreateEvent::class);
+
+    $this->enableModules(['help']);
+    $this->container->set('package_manager.executable_finder', $exec_finder->reveal());
+    $this->assertResultsWithHelp([$error], PreCreateEvent::class);
   }
 
   /**
@@ -137,6 +142,36 @@ class ComposerExecutableValidatorTest extends PackageManagerKernelTestBase {
     // If the validator can't find a recognized, supported version of Composer,
     // it should produce errors.
     $this->assertResults($expected_results, PreCreateEvent::class);
+
+    $this->enableModules(['help']);
+    $this->container->set('package_manager.composer_runner', $runner->reveal());
+    $this->assertResultsWithHelp($expected_results, PreCreateEvent::class);
+  }
+
+  /**
+   * Asserts that a set of validation results link to the Package Manager help.
+   *
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   The expected validation results.
+   * @param string|null $event_class
+   *   (optional) The class of the event which should return the results. Must
+   *   be passed if $expected_results is not empty.
+   */
+  private function assertResultsWithHelp(array $expected_results, string $event_class = NULL): void {
+    $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
+      ->setOption('fragment', 'package-manager-requirements')
+      ->toString();
+
+    // Reformat the provided results so that they all have the link to the
+    // online documentation appended to them.
+    $map = function (string $message) use ($url): string {
+      return $message . ' See <a href="' . $url . '">the help page</a> for information on how to configure the path to Composer.';
+    };
+    foreach ($expected_results as $index => $result) {
+      $messages = array_map($map, $result->getMessages());
+      $expected_results[$index] = ValidationResult::createError($messages);
+    }
+    $this->assertResults($expected_results, $event_class);
   }
 
 }
-- 
GitLab