diff --git a/package_manager/package_manager.module b/package_manager/package_manager.module index c54269edfcc9b4ac1d298c38732cef7b48da97ef..338a3ce447f494d10981139827650e30a57f8f51 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 75ae35d2ece1b138391ece253096555ec0775c0a..3fa0bfa707d037d81d7bba4669f243fd53cd2bfd 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 f2e4e46157be32f80aa96d7cfe2833d91d014176..06c7104e222d7f068e4571e86a6ceaff47a79d9b 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 9b8236a786fd6b309a033deb601b51e24dca154b..004e37e5b0d1068b2f353dadbcd8fa4c52bd5a52 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); } }