diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index 9f2f1d9036fd0c5e2a5e73e4ede8e9f6e1fbf5b4..38de671385ec67c3dd96ccb7fd1294b9663032e2 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -12,21 +12,9 @@ services: arguments: - '@state' - '@string_translation' - - '@package_manager.beginner' - - '@package_manager.stager' - - '@package_manager.cleaner' - - '@package_manager.committer' - '@event_dispatcher' - - '@automatic_updates.path_locator' - automatic_updates.cleaner: - class: Drupal\automatic_updates\ComposerStager\Cleaner - decorates: package_manager.cleaner - public: false - arguments: - - '@automatic_updates.cleaner.inner' - - '%site.path%' - - '@automatic_updates.path_locator' - properties: { _serviceId: package_manager.cleaner } + - '@package_manager.path_locator' + - '@package_manager.stage' automatic_updates.update_refresh_subscriber: class: Drupal\automatic_updates\Event\UpdateRefreshSubscriber arguments: diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index 40bb06838dc43814e71619d87efb9ce195b4125d..41deacfbde761f9c6cb50b730007058019e8b93a 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -74,6 +74,23 @@ services: - '@package_manager.file_syncer' - '@package_manager.file_system' package_manager.cleaner: - class: PhpTuf\ComposerStager\Domain\Cleaner + class: Drupal\package_manager\Cleaner arguments: - '@package_manager.file_system' + - '%site.path%' + - '@package_manager.path_locator' + package_manager.path_locator: + class: Drupal\package_manager\PathLocator + arguments: + - '@config.factory' + - '%app.root%' + + # Public API. + package_manager.stage: + class: Drupal\package_manager\Stage + arguments: + - '@package_manager.path_locator' + - '@package_manager.beginner' + - '@package_manager.stager' + - '@package_manager.committer' + - '@package_manager.cleaner' diff --git a/src/ComposerStager/Cleaner.php b/package_manager/src/Cleaner.php similarity index 71% rename from src/ComposerStager/Cleaner.php rename to package_manager/src/Cleaner.php index 22ddac8e0dd214b400161907466cc111b56100f3..e920c259ae2b9df8080b99e1f2149dd8562fe813 100644 --- a/src/ComposerStager/Cleaner.php +++ b/package_manager/src/Cleaner.php @@ -1,10 +1,11 @@ <?php -namespace Drupal\automatic_updates\ComposerStager; +namespace Drupal\package_manager; -use Drupal\automatic_updates\PathLocator; +use PhpTuf\ComposerStager\Domain\Cleaner as StagerCleaner; use PhpTuf\ComposerStager\Domain\CleanerInterface; use PhpTuf\ComposerStager\Domain\Output\ProcessOutputCallbackInterface; +use PhpTuf\ComposerStager\Infrastructure\Filesystem\FilesystemInterface; use Symfony\Component\Filesystem\Filesystem; /** @@ -29,25 +30,25 @@ class Cleaner implements CleanerInterface { /** * The path locator service. * - * @var \Drupal\automatic_updates\PathLocator + * @var \Drupal\package_manager\PathLocator */ protected $pathLocator; /** * Constructs a Cleaner object. * - * @param \PhpTuf\ComposerStager\Domain\CleanerInterface $decorated - * The decorated cleaner service. + * @param \PhpTuf\ComposerStager\Infrastructure\Filesystem\FilesystemInterface $file_system + * The file system service from Composer Stager. * @param string $site_path * The current site path (e.g., 'sites/default'), without leading or * trailing slashes. - * @param \Drupal\automatic_updates\PathLocator $locator + * @param \Drupal\package_manager\PathLocator $path_locator * The path locator service. */ - public function __construct(CleanerInterface $decorated, string $site_path, PathLocator $locator) { - $this->decorated = $decorated; + public function __construct(FilesystemInterface $file_system, string $site_path, PathLocator $path_locator) { + $this->decorated = new StagerCleaner($file_system); $this->sitePath = $site_path; - $this->pathLocator = $locator; + $this->pathLocator = $path_locator; } /** diff --git a/package_manager/src/PathLocator.php b/package_manager/src/PathLocator.php new file mode 100644 index 0000000000000000000000000000000000000000..ae28ee005cd88a545b164a633e34633a843ec845 --- /dev/null +++ b/package_manager/src/PathLocator.php @@ -0,0 +1,105 @@ +<?php + +namespace Drupal\package_manager; + +use Composer\Autoload\ClassLoader; +use Drupal\Component\FileSystem\FileSystem; +use Drupal\Core\Config\ConfigFactoryInterface; + +/** + * Computes file system paths that are needed to stage code changes. + */ +class PathLocator { + + /** + * The config factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The absolute path of the running Drupal code base. + * + * @var string + */ + protected $appRoot; + + /** + * Constructs a PathLocator object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + * @param string $app_root + * The absolute path of the running Drupal code base. + */ + public function __construct(ConfigFactoryInterface $config_factory, string $app_root) { + $this->configFactory = $config_factory; + $this->appRoot = $app_root; + } + + /** + * Returns the path of the active code base. + * + * @return string + * The absolute path of the active, running code base. + */ + public function getActiveDirectory(): string { + return $this->getProjectRoot(); + } + + /** + * Returns the path of the directory where changes should be staged. + * + * This directory may be made world-writeable for clean-up, so it should be + * somewhere that doesn't put the Drupal installation at risk. + * + * @return string + * The absolute path of the directory where changes should be staged. + */ + public function getStageDirectory(): string { + // Append the site ID to the directory in order to support parallel test + // runs, or multiple sites hosted on the same server. + $site_id = $this->configFactory->get('system.site')->get('uuid'); + return FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . '.package_manager_' . $site_id; + } + + /** + * Returns the absolute path of the project root. + * + * This is where the project-level composer.json should normally be found, and + * may or may not be the same path as the Drupal code base. + * + * @return string + * The absolute path of the project root. + */ + public function getProjectRoot(): string { + // Assume that the vendor directory is immediately below the project root. + return realpath($this->getVendorDirectory() . DIRECTORY_SEPARATOR . '..'); + } + + /** + * Returns the absolute path of the vendor directory. + * + * @return string + * The absolute path of the vendor directory. + */ + public function getVendorDirectory(): string { + $reflector = new \ReflectionClass(ClassLoader::class); + return dirname($reflector->getFileName(), 2); + } + + /** + * Returns the path of the Drupal installation, relative to the project root. + * + * @return string + * The path of the Drupal installation, relative to the project root and + * without leading or trailing slashes. Will return an empty string if the + * project root and Drupal root are the same. + */ + public function getWebRoot(): string { + $web_root = str_replace($this->getProjectRoot(), NULL, $this->appRoot); + return trim($web_root, DIRECTORY_SEPARATOR); + } + +} diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php new file mode 100644 index 0000000000000000000000000000000000000000..d3a84009b5b16e023d88ccee9eee63ede43c9313 --- /dev/null +++ b/package_manager/src/Stage.php @@ -0,0 +1,127 @@ +<?php + +namespace Drupal\package_manager; + +use PhpTuf\ComposerStager\Domain\BeginnerInterface; +use PhpTuf\ComposerStager\Domain\CleanerInterface; +use PhpTuf\ComposerStager\Domain\CommitterInterface; +use PhpTuf\ComposerStager\Domain\StagerInterface; + +/** + * Creates and manages a staging area in which to install or update code. + * + * Allows calling code to copy the current Drupal site into a temporary staging + * directory, use Composer to require packages into it, sync changes from the + * staging directory back into the active code base, and then delete the + * staging directory. + */ +class Stage { + + /** + * The path locator service. + * + * @var \Drupal\package_manager\PathLocator + */ + protected $pathLocator; + + /** + * The beginner service from Composer Stager. + * + * @var \PhpTuf\ComposerStager\Domain\BeginnerInterface + */ + protected $beginner; + + /** + * The stager service from Composer Stager. + * + * @var \PhpTuf\ComposerStager\Domain\StagerInterface + */ + protected $stager; + + /** + * The committer service from Composer Stager. + * + * @var \PhpTuf\ComposerStager\Domain\CommitterInterface + */ + protected $committer; + + /** + * The cleaner service from Composer Stager. + * + * @var \PhpTuf\ComposerStager\Domain\CleanerInterface + */ + protected $cleaner; + + /** + * Constructs a new Stage object. + * + * @param \Drupal\package_manager\PathLocator $path_locator + * The path locator service. + * @param \PhpTuf\ComposerStager\Domain\BeginnerInterface $beginner + * The beginner service from Composer Stager. + * @param \PhpTuf\ComposerStager\Domain\StagerInterface $stager + * The stager service from Composer Stager. + * @param \PhpTuf\ComposerStager\Domain\CommitterInterface $committer + * The committer service from Composer Stager. + * @param \PhpTuf\ComposerStager\Domain\CleanerInterface $cleaner + * The cleaner service from Composer Stager. + */ + public function __construct(PathLocator $path_locator, BeginnerInterface $beginner, StagerInterface $stager, CommitterInterface $committer, CleanerInterface $cleaner) { + $this->pathLocator = $path_locator; + $this->beginner = $beginner; + $this->stager = $stager; + $this->committer = $committer; + $this->cleaner = $cleaner; + } + + /** + * Copies the active code base into the staging area. + * + * @param array|null $exclusions + * Paths to exclude from being copied into the staging area. + * + * @todo Remove the $exclusions parameter when this method fires events. + */ + public function create(?array $exclusions = []): void { + $active_dir = $this->pathLocator->getActiveDirectory(); + $stage_dir = $this->pathLocator->getStageDirectory(); + $this->beginner->begin($active_dir, $stage_dir, $exclusions); + } + + /** + * Requires packages in the staging area. + * + * @param string[] $constraints + * The packages to require, in the form 'vendor/name:version'. + */ + public function require(array $constraints): void { + $command = array_merge(['require'], $constraints); + $command[] = '--update-with-all-dependencies'; + $this->stager->stage($command, $this->pathLocator->getStageDirectory()); + } + + /** + * Applies staged changes to the active directory. + * + * @param array|null $exclusions + * Paths to exclude from being copied into the active directory. + * + * @todo Remove the $exclusions parameter when this method fires events. + */ + public function apply(?array $exclusions = []): void { + $active_dir = $this->pathLocator->getActiveDirectory(); + $stage_dir = $this->pathLocator->getStageDirectory(); + $this->committer->commit($stage_dir, $active_dir, $exclusions); + } + + /** + * Deletes the staging area. + */ + public function destroy(): void { + $stage_dir = $this->pathLocator->getStageDirectory(); + if (is_dir($stage_dir)) { + $this->cleaner->clean($stage_dir); + } + } + +} diff --git a/src/ComposerStager/README.txt b/src/ComposerStager/README.txt deleted file mode 100644 index f17637771ad20b171109c56ba1ed6c69d48e1d91..0000000000000000000000000000000000000000 --- a/src/ComposerStager/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -In a perfect world we would not need these classes and we would options in composer_stager not need the overrides. -@todo Make issues in composer_stager. diff --git a/src/PathLocator.php b/src/PathLocator.php index dfc56c412ef86d4e9f475f3b24313da0f7a4a02b..3ba735edadf0106dcf26d01a8d9e2227d35820e5 100644 --- a/src/PathLocator.php +++ b/src/PathLocator.php @@ -2,106 +2,10 @@ namespace Drupal\automatic_updates; -use Composer\Autoload\ClassLoader; -use Drupal\Component\FileSystem\FileSystem; -use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\package_manager\PathLocator as PackageManagerPathLocator; /** * Computes file system paths that are needed for automatic updates. */ -class PathLocator { - - /** - * The config factory service. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * The absolute path of the running Drupal code base. - * - * @var string - */ - protected $appRoot; - - /** - * Constructs a PathLocator object. - * - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory service. - * @param string $app_root - * The absolute path of the running Drupal code base. - */ - public function __construct(ConfigFactoryInterface $config_factory, string $app_root) { - $this->configFactory = $config_factory; - $this->appRoot = $app_root; - } - - /** - * Returns the path of the active directory, which should be updated. - * - * @return string - * The absolute path which should be updated. - */ - public function getActiveDirectory(): string { - return $this->getProjectRoot(); - } - - /** - * Returns the path of the directory where updates should be staged. - * - * This directory may be made world-writeable for clean-up, so it should be - * somewhere that doesn't put the Drupal installation at risk. - * - * @return string - * The absolute path of the directory where updates should be staged. - * - * @see \Drupal\automatic_updates\ComposerStager\Cleaner::clean() - */ - public function getStageDirectory(): string { - // Append the site ID to the directory in order to support parallel test - // runs, or multiple sites hosted on the same server. - $site_id = $this->configFactory->get('system.site')->get('uuid'); - return FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . '.automatic_updates_stage_' . $site_id; - } - - /** - * Returns the absolute path of the project root. - * - * This is where the project-level composer.json should normally be found, and - * may or may not be the same path as the Drupal code base. - * - * @return string - * The absolute path of the project root. - */ - public function getProjectRoot(): string { - // Assume that the vendor directory is immediately below the project root. - return realpath($this->getVendorDirectory() . DIRECTORY_SEPARATOR . '..'); - } - - /** - * Returns the absolute path of the vendor directory. - * - * @return string - * The absolute path of the vendor directory. - */ - public function getVendorDirectory(): string { - $reflector = new \ReflectionClass(ClassLoader::class); - return dirname($reflector->getFileName(), 2); - } - - /** - * Returns the path of the Drupal installation, relative to the project root. - * - * @return string - * The path of the Drupal installation, relative to the project root and - * without leading or trailing slashes. Will return an empty string if the - * project root and Drupal root are the same. - */ - public function getWebRoot(): string { - $web_root = str_replace($this->getProjectRoot(), NULL, $this->appRoot); - return trim($web_root, DIRECTORY_SEPARATOR); - } - +class PathLocator extends PackageManagerPathLocator { } diff --git a/src/Updater.php b/src/Updater.php index 4cb0b90a644156660ff6df0249978c207446d5c5..66158618d1f90aea100134011affea7676661c1b 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -10,11 +10,9 @@ use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\package_manager\ComposerUtility; +use Drupal\package_manager\PathLocator as PackageManagerPathLocator; +use Drupal\package_manager\Stage; use Drupal\system\SystemManager; -use PhpTuf\ComposerStager\Domain\BeginnerInterface; -use PhpTuf\ComposerStager\Domain\CleanerInterface; -use PhpTuf\ComposerStager\Domain\CommitterInterface; -use PhpTuf\ComposerStager\Domain\StagerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -31,34 +29,6 @@ class Updater { */ public const STATE_KEY = 'AUTOMATIC_UPDATES_CURRENT'; - /** - * The composer_stager beginner service. - * - * @var \Drupal\automatic_updates\ComposerStager\Beginner - */ - protected $beginner; - - /** - * The composer_stager stager service. - * - * @var \PhpTuf\ComposerStager\Domain\StagerInterface - */ - protected $stager; - - /** - * The composer_stager cleaner service. - * - * @var \PhpTuf\ComposerStager\Domain\CleanerInterface - */ - protected $cleaner; - - /** - * The composer_stager committer service. - * - * @var \PhpTuf\ComposerStager\Domain\CommitterInterface - */ - protected $committer; - /** * The state service. * @@ -76,10 +46,17 @@ class Updater { /** * The path locator service. * - * @var \Drupal\automatic_updates\PathLocator + * @var \Drupal\package_manager\PathLocator */ protected $pathLocator; + /** + * The stage service. + * + * @var \Drupal\package_manager\Stage + */ + protected $stage; + /** * Constructs an Updater object. * @@ -87,28 +64,19 @@ class Updater { * The state service. * @param \Drupal\Core\StringTranslation\TranslationInterface $translation * The string translation service. - * @param \PhpTuf\ComposerStager\Domain\BeginnerInterface $beginner - * The Composer Stager's beginner service. - * @param \PhpTuf\ComposerStager\Domain\StagerInterface $stager - * The Composer Stager's stager service. - * @param \PhpTuf\ComposerStager\Domain\CleanerInterface $cleaner - * The Composer Stager's cleaner service. - * @param \PhpTuf\ComposerStager\Domain\CommitterInterface $committer - * The Composer Stager's committer service. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher service. - * @param \Drupal\automatic_updates\PathLocator $path_locator + * @param \Drupal\package_manager\PathLocator $path_locator * The path locator service. + * @param \Drupal\package_manager\Stage $stage + * The stage service. */ - public function __construct(StateInterface $state, TranslationInterface $translation, BeginnerInterface $beginner, StagerInterface $stager, CleanerInterface $cleaner, CommitterInterface $committer, EventDispatcherInterface $event_dispatcher, PathLocator $path_locator) { + public function __construct(StateInterface $state, TranslationInterface $translation, EventDispatcherInterface $event_dispatcher, PackageManagerPathLocator $path_locator, Stage $stage) { $this->state = $state; - $this->beginner = $beginner; - $this->stager = $stager; - $this->cleaner = $cleaner; - $this->committer = $committer; $this->setStringTranslation($translation); $this->eventDispatcher = $event_dispatcher; $this->pathLocator = $path_locator; + $this->stage = $stage; } /** @@ -150,7 +118,7 @@ class Updater { $stage_key = $this->createActiveStage($packages); /** @var \Drupal\automatic_updates\Event\PreStartEvent $event */ $event = $this->dispatchUpdateEvent(new PreStartEvent($composer, $packages), AutomaticUpdatesEvents::PRE_START); - $this->beginner->begin($this->pathLocator->getActiveDirectory(), $this->pathLocator->getStageDirectory(), $this->getExclusions($event)); + $this->stage->create($this->getExclusions($event)); return $stage_key; } @@ -175,19 +143,7 @@ class Updater { */ public function stage(): void { $current = $this->state->get(static::STATE_KEY); - $this->stagePackages($current['package_versions']); - } - - /** - * Installs Composer packages in the staging area. - * - * @param string[] $packages - * The versions of the packages to stage, keyed by package name. - */ - protected function stagePackages(array $packages): void { - $command = array_merge(['require'], $packages); - $command[] = '--update-with-all-dependencies'; - $this->stageCommand($command); + $this->stage->require($current['package_versions']); } /** @@ -202,7 +158,7 @@ class Updater { /** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */ $event = $this->dispatchUpdateEvent(new PreCommitEvent($active_composer, $stage_composer), AutomaticUpdatesEvents::PRE_COMMIT); - $this->committer->commit($stage_dir, $active_dir, $this->getExclusions($event)); + $this->stage->apply($this->getExclusions($event)); $this->dispatchUpdateEvent(new UpdateEvent($active_composer), AutomaticUpdatesEvents::POST_COMMIT); } @@ -210,26 +166,10 @@ class Updater { * Cleans the current update. */ public function clean(): void { - $stage_dir = $this->pathLocator->getStageDirectory(); - if (is_dir($stage_dir)) { - $this->cleaner->clean($stage_dir); - } + $this->stage->destroy(); $this->state->delete(static::STATE_KEY); } - /** - * Stages a Composer command. - * - * @param string[] $command - * The command array as expected by - * \PhpTuf\ComposerStager\Domain\StagerInterface::stage(). - * - * @see \PhpTuf\ComposerStager\Domain\StagerInterface::stage() - */ - protected function stageCommand(array $command): void { - $this->stager->stage($command, $this->pathLocator->getStageDirectory()); - } - /** * Initializes an active update and returns its ID. * diff --git a/tests/src/Functional/FileSystemOperationsTest.php b/tests/src/Functional/FileSystemOperationsTest.php index 5fce64a3862f21abb0e99a9cef50306d05a7a6d6..1e821229b44d4edf52e32c327b609c9670710ebb 100644 --- a/tests/src/Functional/FileSystemOperationsTest.php +++ b/tests/src/Functional/FileSystemOperationsTest.php @@ -2,10 +2,11 @@ namespace Drupal\Tests\automatic_updates\Functional; -use Drupal\automatic_updates\ComposerStager\Cleaner; -use Drupal\automatic_updates\PathLocator; +use Drupal\package_manager\Cleaner; use Drupal\automatic_updates\Updater; use Drupal\Core\Site\Settings; +use Drupal\package_manager\PathLocator; +use Drupal\package_manager\Stage; /** * Tests handling of files and directories during an update. @@ -64,20 +65,25 @@ class FileSystemOperationsTest extends AutomaticUpdatesFunctionalTestBase { // will otherwise default to the site path being used for the test site, // which doesn't exist in the fake site fixture. $cleaner = new Cleaner( - $this->container->get('package_manager.cleaner'), + $this->container->get('package_manager.file_system'), 'sites/default', $locator->reveal() ); - $this->updater = new Updater( - $this->container->get('state'), - $this->container->get('string_translation'), + $stage = new Stage( + $locator->reveal(), $this->container->get('package_manager.beginner'), $this->container->get('package_manager.stager'), - $cleaner, $this->container->get('package_manager.committer'), + $cleaner + ); + + $this->updater = new Updater( + $this->container->get('state'), + $this->container->get('string_translation'), $this->container->get('event_dispatcher'), - $locator->reveal() + $locator->reveal(), + $stage ); // Use the public and private files directories in the fake site fixture. diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php index d6bd48448f9443d875f26c360cd25a7e3ae26a26..ed9eeb51df89f98756f3b9df7e7ea36708f05b52 100644 --- a/tests/src/Kernel/UpdaterTest.php +++ b/tests/src/Kernel/UpdaterTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\automatic_updates\Kernel; use Drupal\automatic_updates\PathLocator; -use Prophecy\Argument; /** * @coversDefaultClass \Drupal\automatic_updates\Updater @@ -34,7 +33,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase { $locator = $this->prophesize(PathLocator::class); $locator->getActiveDirectory()->willReturn(__DIR__ . '/../../fixtures/fake-site'); $locator->getStageDirectory()->willReturn('/tmp'); - $this->container->set('automatic_updates.path_locator', $locator->reveal()); + $this->container->set('package_manager.path_locator', $locator->reveal()); $this->container->get('automatic_updates.updater')->begin([ 'drupal' => '9.8.1', @@ -44,14 +43,9 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase { $kernel = $this->container->get('kernel'); $kernel->rebuildContainer(); $this->container = $kernel->getContainer(); - $stager = $this->prophesize('\PhpTuf\ComposerStager\Domain\StagerInterface'); - $command = [ - 'require', - 'drupal/core-recommended:9.8.1', - '--update-with-all-dependencies', - ]; - $stager->stage($command, Argument::cetera())->shouldBeCalled(); - $this->container->set('package_manager.stager', $stager->reveal()); + $stage = $this->prophesize('\Drupal\package_manager\Stage'); + $stage->require(['drupal/core-recommended:9.8.1'])->shouldBeCalled(); + $this->container->set('package_manager.stage', $stage->reveal()); $this->container->get('automatic_updates.updater')->stage(); }