class ComposerPatchesValidator

Validates the configuration of the cweagans/composer-patches plugin.

To ensure that applied patches remain consistent between the active and stage directories, the following rules are enforced if the patcher is installed:

  • It must be installed in both places, or in neither of them. It can't, for example, be installed in the active directory but not the stage directory (or vice versa).
  • It must be one of the project's direct runtime or dev dependencies.
  • It cannot be installed or removed by Package Manager. In other words, it must be added to the project at the command line by someone technical enough to install and configure it properly.

@internal This is an internal part of Package Manager and may be changed or removed at any time without warning. External code should not interact with this class.

Hierarchy

Expanded class hierarchy of ComposerPatchesValidator

File

core/modules/package_manager/src/Validator/ComposerPatchesValidator.php, line 40

Namespace

Drupal\package_manager\Validator
View source
final class ComposerPatchesValidator implements EventSubscriberInterface {
    use StringTranslationTrait;
    
    /**
     * The name of the plugin being analyzed.
     *
     * @var string
     */
    private const PLUGIN_NAME = 'cweagans/composer-patches';
    public function __construct(ModuleHandlerInterface $moduleHandler, ComposerInspector $composerInspector, PathLocator $pathLocator) {
    }
    
    /**
     * Validates the status of the patcher plugin.
     *
     * @param \Drupal\package_manager\Event\PreOperationStageEvent $event
     *   The event object.
     */
    public function validate(PreOperationStageEvent $event) : void {
        $messages = [];
        [
            $plugin_installed_in_active,
            $is_active_root_requirement,
            $active_configuration_ok,
        ] = $this->computePatcherStatus($this->pathLocator
            ->getProjectRoot());
        if ($event instanceof PreApplyEvent) {
            [
                $plugin_installed_in_stage,
                $is_stage_root_requirement,
                $stage_configuration_ok,
            ] = $this->computePatcherStatus($event->stage
                ->getStageDirectory());
            $has_staged_update = TRUE;
        }
        else {
            // No staged update exists.
            $has_staged_update = FALSE;
        }
        // If there's a staged update and the patcher has been installed or removed
        // in the stage directory, that's a problem.
        if ($has_staged_update && $plugin_installed_in_active !== $plugin_installed_in_stage) {
            if ($plugin_installed_in_stage) {
                $message = $this->t('It cannot be installed by Package Manager.');
            }
            else {
                $message = $this->t('It cannot be removed by Package Manager.');
            }
            $messages[] = $this->createErrorMessage($message, 'package-manager-faq-composer-patches-installed-or-removed');
        }
        // If the patcher is not listed in the runtime or dev dependencies, that's
        // an error as well.
        if ($plugin_installed_in_active && !$is_active_root_requirement || $has_staged_update && $plugin_installed_in_stage && !$is_stage_root_requirement) {
            $messages[] = $this->createErrorMessage($this->t('It must be a root dependency.'), 'package-manager-faq-composer-patches-not-a-root-dependency');
        }
        // If the plugin is misconfigured in either the active or stage directories,
        // flag an error.
        if ($plugin_installed_in_active && !$active_configuration_ok || $has_staged_update && $plugin_installed_in_stage && !$stage_configuration_ok) {
            $messages[] = $this->t('The <code>composer-exit-on-patch-failure</code> key is not set to <code>true</code> in the <code>extra</code> section of <code>composer.json</code>.');
        }
        if ($messages) {
            $summary = $this->t("Problems detected related to the Composer plugin <code>@plugin</code>.", [
                '@plugin' => static::PLUGIN_NAME,
            ]);
            $event->addError($messages, $summary);
        }
    }
    
    /**
     * Appends a link to online help to an error message.
     *
     * @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
     *   The error message.
     * @param string $fragment
     *   The fragment of the online help to link to.
     *
     * @return \Drupal\Core\StringTranslation\TranslatableMarkup
     *   The final, translated error message.
     */
    private function createErrorMessage(TranslatableMarkup $message, string $fragment) : TranslatableMarkup {
        if ($this->moduleHandler
            ->moduleExists('help')) {
            $url = Url::fromRoute('help.page', [
                'name' => 'package_manager',
            ])->setOption('fragment', $fragment)
                ->toString();
            return $this->t('@message See <a href=":url">the help page</a> for information on how to resolve the problem.', [
                '@message' => $message,
                ':url' => $url,
            ]);
        }
        return $message;
    }
    
    /**
     * Computes the status of the patcher plugin in a particular directory.
     *
     * @param string $working_dir
     *   The directory in which to run Composer.
     *
     * @return bool[]
     *   An indexed array containing three booleans, in order:
     *   - Whether the patcher plugin is installed.
     *   - Whether the patcher plugin is a root requirement in composer.json (in
     *     either the runtime or dev dependencies).
     *   - Whether the `composer-exit-on-patch-failure` flag is set in the `extra`
     *     section of composer.json.
     */
    private function computePatcherStatus(string $working_dir) : array {
        $list = $this->composerInspector
            ->getInstalledPackagesList($working_dir);
        $installed_version = $list[static::PLUGIN_NAME]?->version;
        $info = $this->composerInspector
            ->getRootPackageInfo($working_dir);
        $is_root_requirement = array_key_exists(static::PLUGIN_NAME, $info['requires'] ?? []) || array_key_exists(static::PLUGIN_NAME, $info['devRequires'] ?? []);
        // The 2.x version of the plugin always exits with an error if a patch can't
        // be applied.
        if ($installed_version && Semver::satisfies($installed_version, '^2')) {
            $exit_on_failure = TRUE;
        }
        else {
            $extra = Json::decode($this->composerInspector
                ->getConfig('extra', $working_dir));
            $exit_on_failure = $extra['composer-exit-on-patch-failure'] ?? FALSE;
        }
        return [
            is_string($installed_version),
            $is_root_requirement,
            $exit_on_failure,
        ];
    }
    
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents() : array {
        return [
            PreCreateEvent::class => 'validate',
            PreApplyEvent::class => 'validate',
            StatusCheckEvent::class => 'validate',
        ];
    }

}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.