Commit b1abfee8 authored by omkar podey's avatar omkar podey Committed by Tim Plunkett
Browse files

Issue #3249553 by omkar.podey, tim.plunkett, tedbow, phenaproxima,...

Issue #3249553 by omkar.podey, tim.plunkett, tedbow, phenaproxima, TravisCarden: Create package_manager validator to ensure the project to be installed is not already installed by Composer
parent 537b06b0
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -22,6 +22,12 @@ services:
      - '@string_translation'
    tags:
      - { name: 'event_subscriber' }
  project_browser.composer_validator.package_not_installed_validator:
    class: Drupal\project_browser\ComposerInstaller\Validator\PackageNotInstalledValidator
    arguments:
      - '@string_translation'
    tags:
      - { name: 'event_subscriber' }
  cache.project_browser:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
+70 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\project_browser\ComposerInstaller\Validator;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\project_browser\ComposerInstaller\Installer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Validates that packages to be installed are not already installed.
 *
 * @internal
 *   Tagged services are internal.
 */
final class PackageNotInstalledValidator implements EventSubscriberInterface {

  use StringTranslationTrait;

  /**
   * Constructs a PackageNotInstalledValidator object.
   *
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   The translation service.
   */
  public function __construct(TranslationInterface $translation) {
    $this->setStringTranslation($translation);
  }

  /**
   * Validates that packages are not already installed with composer.
   *
   * @param \Drupal\package_manager\Event\PreRequireEvent $event
   *   The event object.
   */
  public function validatePackagesNotAlreadyInstalled(PreRequireEvent $event): void {
    $stage = $event->getStage();
    if (!$stage instanceof Installer) {
      return;
    }

    $installed_packages = $stage->getActiveComposer()->getInstalledPackages();
    // Assuming project browser cannot install dev releases, since we are not
    // calling $event->getDevPackages() for now.
    $required_packages = $event->getRuntimePackages();
    $already_installed_packages = [];

    foreach (array_keys($required_packages) as $required_package) {
      if (array_key_exists($required_package, $installed_packages)) {
        $already_installed_packages[] = $required_package;
      }
    }

    if (!empty($already_installed_packages)) {
      $summary = $this->formatPlural(count($already_installed_packages), 'The following package is already installed:', 'The following packages are already installed:');
      $event->addError($already_installed_packages, $summary);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      PreRequireEvent::class => 'validatePackagesNotAlreadyInstalled',
    ];
  }

}
+34 −0
Original line number Diff line number Diff line
{
    "packages": [
        {
            "name": "drupal/core-recommended",
            "version": "9.8.0",
            "require": {
                "drupal/core": "9.8.0"
            }
        },
        {
            "name": "drupal/core",
            "version": "9.8.0"
        },
        {
            "name": "drupal/my_module",
            "version": "9.8.0",
            "type": "drupal-module"
        },
        {
            "name": "drupal/my_module_2",
            "version": "9.8.0",
            "type": "drupal-module"
        },
        {
            "name": "drupal/my_dev_module",
            "version": "9.8.1",
            "type": "drupal-module"
        }
    ],
    "dev": true,
    "dev-package-names": [
        "drupal/my_dev_module"
    ]
}
+91 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\project_browser\Kernel\Validator;

use Drupal\package_manager\ValidationResult;
use Drupal\project_browser\Exception\InstallException;
use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;

/**
 * @covers \Drupal\project_browser\ComposerInstaller\Validator\PackageNotInstalledValidator
 *
 * @group project_browser
 */
class PackageNotInstalledValidatorTest extends PackageManagerKernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'project_browser',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $active_dir = $this->container->get('package_manager.path_locator')
      ->getProjectRoot();

    $installed = __DIR__ . '/../../../fixtures/ComposerInstaller/PackageNotInstalledValidator/active.installed.json';
    $this->assertFileIsReadable($installed);
    copy($installed, $active_dir . '/vendor/composer/installed.json');
  }

  /**
   * Data provider for testPreRequireException().
   *
   * @return array[]
   *   The test cases.
   */
  public function providerPreRequireException(): array {
    $summary = t('The following package is already installed:');
    $summary_plural = t('The following packages are already installed:');

    return [
      'new package which is currently *not* installed' => [
        ['drupal/new_module'],
        NULL,
      ],
      'already installed package' => [
        ['drupal/my_module'],
        ValidationResult::createError(['drupal/my_module'], $summary),
      ],
      '2 packages sent, 1 is already installed' => [
        ['drupal/new_module', 'drupal/my_module'],
        ValidationResult::createError(['drupal/my_module'], $summary),
      ],
      '2 packages sent, both already installed' => [
        ['drupal/my_module', 'drupal/my_module_2'],
        ValidationResult::createError(['drupal/my_module', 'drupal/my_module_2'], $summary_plural),
      ],
    ];
  }

  /**
   * Tests the packages installed with Composer during pre-create.
   *
   * @param string[] $packages
   *   The packages to install.
   * @param \Drupal\package_manager\ValidationResult|null $expected_result
   *   The expected validation result if any, otherwise NULL.
   *
   * @dataProvider providerPreRequireException
   */
  public function testPreRequireException(array $packages, ?ValidationResult $expected_result): void {
    try {
      /** @var \Drupal\project_browser\ComposerInstaller\Installer $installer */
      $installer = $this->container->get('project_browser.installer');
      $installer->create();
      $installer->require($packages);
      // If we did not get an exception, ensure we didn't expect any results.
      $this->assertNull($expected_result);
    }
    catch (InstallException $e) {
      $this->assertNotNull($expected_result);
      $this->assertValidationResultsEqual([$expected_result], $e->getResults());
    }
  }

}