From 52307f2933002fad88c148e319a5287e8086f5dd Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 9 Apr 2024 11:53:15 +0800 Subject: [PATCH] Convert commands to use constructors to handle initial command creation. Removes the "handle" method which prevented the use of promoted constructor properties. Also converted some enumerated values to PHP enums. --- README.md | 13 +++- src/Commands/BaseCommand.php | 42 ++++++++---- src/Commands/Command.php | 21 ------ src/Commands/Install.php | 2 +- src/Commands/Search.php | 54 +++++---------- src/Commands/Show.php | 92 +++++++------------------- src/Commands/Update.php | 69 ++++--------------- src/Commands/Version.php | 24 +++---- src/Composer.php | 28 ++++---- src/Enums/ShowMode.php | 50 ++++++++++++++ src/Package/Package.php | 13 +++- tests/Cases/Commands/SearchTest.php | 7 +- tests/Cases/Commands/ShowTest.php | 7 +- tests/Cases/Commands/VersionTest.php | 9 ++- tests/Cases/Package/ConstraintTest.php | 2 +- tests/ComposerTestCase.php | 5 +- 16 files changed, 197 insertions(+), 241 deletions(-) create mode 100644 src/Enums/ShowMode.php diff --git a/README.md b/README.md index 1303983..ef9c536 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ composer require winter/packager This library currently provides support for the following Composer commands: -- `version` **(--version)** - `install` -- `update` +- `search` - `show` +- `update` +- `version` **(--version)** You can create a `Composer` instance in your PHP script and run these commands like so, defining a working directory which contains a `composer.json` file, and a home directory in which cached packages are stored. @@ -42,12 +43,18 @@ $composer // Get the Composer version $version = $composer->version(); -// Run an install or update +// Run an install or update on the entire project, including dev dependencies $install = $composer->install(); $update = $composer->update(); +// Install project without dev dependencies +$install = $composer->install(false); + // Show installed packages $show = $composer->show(); + +// Search packages +$results = ``` Documentation on each command will be forthcoming soon. diff --git a/src/Commands/BaseCommand.php b/src/Commands/BaseCommand.php index 43768eb..8ed8494 100644 --- a/src/Commands/BaseCommand.php +++ b/src/Commands/BaseCommand.php @@ -38,13 +38,25 @@ abstract class BaseCommand implements Command * * Defines the Composer instance that will run the command. * - * @param Composer $composer + * @param Composer $composer Composer instance */ public function __construct(Composer $composer) { $this->composer = $composer; } + /** + * Make a new instance of the command. + * + * @param \Winter\Packager\Composer $composer + * @param mixed[] $args + */ + public static function make(Composer $composer, mixed ...$args): static + { + /* @phpstan-ignore-next-line */ + return new static($composer, ...$args); + } + /** * Returns the instance of Composer that is running the command. * @@ -56,20 +68,25 @@ public function getComposer(): Composer } /** - * @inheritDoc + * Provides the command name for Composer. + * + * @return string */ - public function getCommandName(): string - { - return ''; - } + abstract protected function getCommandName(): string; /** - * @inheritDoc + * Provides if the given command requires the working directory to be available. + * + * @return bool True if it does, false if it does not. */ - public function requiresWorkDir(): bool - { - return false; - } + abstract protected function requiresWorkDir(): bool; + + /** + * Provides the arguments for the wrapped Composer command. + * + * @return array An array of arguments to provide the Composer application. + */ + abstract protected function arguments(): array; /** * Sets up the environment and creates the Composer application. @@ -158,9 +175,6 @@ protected function runComposerCommand(): array 'output' => preg_split('/(\n|\r\n)/', $e->getMessage()), 'exception' => $e, ]; - } finally { - // Restores the error handler away from Composer's in-built error handler - restore_error_handler(); } $this->tearDownComposerApp(); diff --git a/src/Commands/Command.php b/src/Commands/Command.php index 795b656..5c7ef0e 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -18,25 +18,4 @@ interface Command * @return mixed The output of the command. */ public function execute(); - - /** - * Provides the command name for Composer. - * - * @return string - */ - public function getCommandName(): string; - - /** - * Provides if the given command requires the working directory to be available. - * - * @return bool True if it does, false if it does not. - */ - public function requiresWorkDir(): bool; - - /** - * Provides the arguments for the wrapped Composer command. - * - * @return array An array of arguments to provide the Composer application. - */ - public function arguments(): array; } diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 0fc4c99..00fa37e 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -15,7 +15,7 @@ class Install extends Update /** * @inheritDoc */ - public function getCommandName(): string + protected function getCommandName(): string { return 'install'; } diff --git a/src/Commands/Search.php b/src/Commands/Search.php index ea95e3d..4f84412 100644 --- a/src/Commands/Search.php +++ b/src/Commands/Search.php @@ -16,42 +16,22 @@ class Search extends BaseCommand { /** - * The search query to find packages. - */ - public string $query; - - /** - * The type of package to search for. - */ - public ?string $type = null; - - /** - * Limit the search parameters. This can be one of the following: + * Command constructor. * - * - `name`: Search and return package names only - * - `vendor`: Search and return vendors only - * - * @var string|null + * @param Composer $composer + * @param string $query The search query to find packages. + * @param string|null $type The type of package to search for. + * @param string|null $limitTo Limit the search parameters. This can be one of the following: + * - `name`: Search and return package names only + * - `vendor`: Search and return vendors only */ - public ?string $limitTo = null; - - /** - * Command handler. - */ - public function handle( - string $query, - ?string $type = null, - bool $onlyNames = false, - bool $onlyVendors = false - ): void { - $this->query = $query; - $this->type = $type; - - if ($onlyNames) { - $this->limitTo = 'name'; - } elseif ($onlyVendors) { - $this->limitTo = 'vendor'; - } + final public function __construct( + Composer $composer, + public string $query, + public ?string $type = null, + public ?string $limitTo = null + ) { + parent::__construct($composer); } /** @@ -84,7 +64,7 @@ public function execute() /** * @inheritDoc */ - public function getCommandName(): string + protected function getCommandName(): string { return 'search'; } @@ -92,7 +72,7 @@ public function getCommandName(): string /** * @inheritDoc */ - public function requiresWorkDir(): bool + protected function requiresWorkDir(): bool { return false; } @@ -100,7 +80,7 @@ public function requiresWorkDir(): bool /** * @inheritDoc */ - public function arguments(): array + protected function arguments(): array { $arguments = []; diff --git a/src/Commands/Show.php b/src/Commands/Show.php index 699a645..f4ac7d5 100644 --- a/src/Commands/Show.php +++ b/src/Commands/Show.php @@ -3,6 +3,7 @@ namespace Winter\Packager\Commands; use Winter\Packager\Composer; +use Winter\Packager\Enums\ShowMode; use Winter\Packager\Enums\VersionStatus; use Winter\Packager\Exceptions\CommandException; @@ -17,67 +18,21 @@ class Show extends BaseCommand { /** - * Mode to run the command against - */ - public string $mode = 'installed'; - - /** - * Individual package to search - */ - public ?string $package; - - /** - * Exclude dev dependencies from search - */ - public bool $noDev = false; - - /** - * Command handler. + * Command constructor. * - * The mode can be one of the following: - * - `installed`: Show installed packages - * - `locked`: Show locked packages - * - `platform`: Show platform requirements - * - `available`: Show all available packages - * - `self`: Show the current package - * - `path`: Show the package path - * - `tree`: Show packages in a dependency tree - * - `outdated`: Show only outdated packages - * - `direct`: Show only direct dependencies - * - * @param string|null $mode - * @param string|null $package - * @param boolean $noDev + * @param Composer $composer Composer instance + * @param ShowMode $mode Mode to run the command against + * @param string|null $package Individual package to search + * @param boolean $noDev Exclude dev dependencies from search * @return void */ - public function handle(?string $mode = 'installed', string $package = null, bool $noDev = false): void - { - $mode = $mode ?? 'installed'; - - $validModes = [ - 'installed', - 'locked', - 'platform', - 'available', - 'self', - 'path', - 'tree', - 'outdated', - 'direct' - ]; - - if (!in_array(strtolower($mode), $validModes)) { - throw new CommandException( - sprintf( - 'Invalid mode, must be one of the following: %s', - implode(', ', $validModes) - ) - ); - } - - $this->mode = $mode; - $this->package = $package; - $this->noDev = $noDev; + final public function __construct( + Composer $composer, + public ShowMode $mode = ShowMode::INSTALLED, + public ?string $package = null, + public bool $noDev = false + ) { + parent::__construct($composer); } /** @@ -103,10 +58,9 @@ public function execute() $results = json_decode(implode(PHP_EOL, $output['output']), true); $packages = []; - if (is_null($this->package) && in_array($this->mode, ['installed', 'locked', 'platform', 'path', 'outdated', 'direct'])) { + if (is_null($this->package) && $this->mode->isCollectible()) { // Convert packages in simple lists to a package collection - $key = (!in_array($this->mode, ['locked', 'platform'])) ? 'installed' : $this->mode; - $results = $results[$key]; + $results = $results[$this->mode->getComposerArrayKeyName()]; foreach ($results as $result) { [$namespace, $name] = $this->nameSplit($result['name']); @@ -130,9 +84,9 @@ public function execute() } return Composer::newCollection($packages); - } elseif (is_null($this->package) && $this->mode === 'available') { + } elseif (is_null($this->package) && $this->mode === ShowMode::AVAILABLE) { // Convert entire available package list into a package collection - foreach ($results['available'] as $result) { + foreach ($results[$this->mode->getComposerArrayKeyName()] as $result) { [$namespace, $name] = $this->nameSplit($result['name']); $packages[] = Composer::newPackage( @@ -143,7 +97,7 @@ public function execute() } return Composer::newCollection($packages); - } elseif ($this->mode === 'self') { + } elseif ($this->mode === ShowMode::SELF) { $result = $results; [$namespace, $name] = $this->nameSplit($result['name']); @@ -190,7 +144,7 @@ public function execute() /** * @inheritDoc */ - public function getCommandName(): string + protected function getCommandName(): string { return 'show'; } @@ -198,7 +152,7 @@ public function getCommandName(): string /** * @inheritDoc */ - public function requiresWorkDir(): bool + protected function requiresWorkDir(): bool { return true; } @@ -206,7 +160,7 @@ public function requiresWorkDir(): bool /** * @inheritDoc */ - public function arguments(): array + protected function arguments(): array { $arguments = []; @@ -214,8 +168,8 @@ public function arguments(): array $arguments['package'] = $this->package; } - if ($this->mode !== 'installed') { - $arguments['--' . $this->mode] = true; + if ($this->mode !== ShowMode::INSTALLED) { + $arguments['--' . $this->mode->value] = true; } if ($this->noDev) { diff --git a/src/Commands/Update.php b/src/Commands/Update.php index f81c641..7e6e1bc 100644 --- a/src/Commands/Update.php +++ b/src/Commands/Update.php @@ -2,8 +2,8 @@ namespace Winter\Packager\Commands; +use Winter\Packager\Composer; use Winter\Packager\Exceptions\ComposerExceptionHandler; -use Winter\Packager\Exceptions\ComposerJsonException; use Winter\Packager\Parser\InstallOutputParser; /** @@ -23,41 +23,6 @@ class Update extends BaseCommand const PREFER_DIST = 'dist'; const PREFER_SOURCE = 'source'; - /** - * Whether to do a lockfile-only update - */ - protected bool $lockFileOnly = false; - - /** - * Include "require-dev" dependencies in the update. - */ - protected bool $includeDev = true; - - /** - * Ignore platform requirements when updating. - */ - protected bool $ignorePlatformReqs = false; - - /** - * Prefer dist releases of packages - */ - protected string $installPreference = 'none'; - - /** - * Ignore scripts that run after Composer events. - */ - protected bool $ignoreScripts = false; - - /** - * Use dry-run mode - */ - protected bool $dryRun = false; - - /** - * Whether this command has already been executed - */ - protected bool $executed = false; - /** * @var array Raw output from Composer */ @@ -93,7 +58,7 @@ class Update extends BaseCommand protected $problems = []; /** - * Handle options before execution. + * Command constructor. * * @param boolean $includeDev Include "require-dev" dependencies in the update. * @param boolean $lockFileOnly Do a lockfile update only, do not install dependencies. @@ -102,17 +67,16 @@ class Update extends BaseCommand * @param boolean $ignoreScripts Ignores scripts that run after Composer events. * @return void */ - public function handle( - bool $includeDev = true, - bool $lockFileOnly = false, - bool $ignorePlatformReqs = false, - string $installPreference = 'none', - bool $ignoreScripts = false, - bool $dryRun = false + final public function __construct( + Composer $composer, + protected bool $includeDev = true, + protected bool $lockFileOnly = false, + protected bool $ignorePlatformReqs = false, + protected string $installPreference = 'none', + protected bool $ignoreScripts = false, + protected bool $dryRun = false ) { - if ($this->executed) { - return; - } + parent::__construct($composer); $this->includeDev = $includeDev; $this->lockFileOnly = $lockFileOnly; @@ -132,11 +96,6 @@ public function handle( */ public function execute() { - if ($this->executed) { - return $this; - } - - $this->executed = true; $output = $this->runComposerCommand(); if ($output['code'] !== 0) { @@ -332,7 +291,7 @@ public function getProblems(): array /** * @inheritDoc */ - public function getCommandName(): string + protected function getCommandName(): string { return 'update'; } @@ -340,7 +299,7 @@ public function getCommandName(): string /** * @inheritDoc */ - public function requiresWorkDir(): bool + protected function requiresWorkDir(): bool { return true; } @@ -348,7 +307,7 @@ public function requiresWorkDir(): bool /** * @inheritDoc */ - public function arguments(): array + protected function arguments(): array { $arguments = []; diff --git a/src/Commands/Version.php b/src/Commands/Version.php index ea522bd..f3f14af 100644 --- a/src/Commands/Version.php +++ b/src/Commands/Version.php @@ -2,6 +2,7 @@ namespace Winter\Packager\Commands; +use Winter\Packager\Composer; use Winter\Packager\Exceptions\CommandException; use Winter\Packager\Parser\VersionOutputParser; @@ -16,12 +17,7 @@ class Version extends BaseCommand { /** - * @var string The detail to return. Valid values: "version", "date", "dateTime", "all" - */ - protected $detail = 'version'; - - /** - * Command handler. + * Command constructor. * * Prepares the details of the Composer version. * @@ -31,11 +27,11 @@ class Version extends BaseCommand * - `date`: Get the build date * - `dateTime`: Get the build date and time */ - public function handle(string $detail = 'version'): void - { - $this->detail = (in_array($detail, ['version', 'date', 'dateTime', 'all'])) - ? $detail - : 'version'; + final public function __construct( + Composer $composer, + protected string $detail = 'version' + ) { + parent::__construct($composer); } /** @@ -73,7 +69,7 @@ public function execute() /** * @inheritDoc */ - public function getCommandName(): string + protected function getCommandName(): string { return ''; } @@ -81,7 +77,7 @@ public function getCommandName(): string /** * @inheritDoc */ - public function requiresWorkDir(): bool + protected function requiresWorkDir(): bool { return false; } @@ -89,7 +85,7 @@ public function requiresWorkDir(): bool /** * @inheritDoc */ - public function arguments(): array + protected function arguments(): array { return [ '--version' diff --git a/src/Composer.php b/src/Composer.php index 549191d..da9dafb 100644 --- a/src/Composer.php +++ b/src/Composer.php @@ -2,8 +2,8 @@ namespace Winter\Packager; -use Winter\Packager\Commands\Command; use Throwable; +use Winter\Packager\Commands\Command; use Winter\Packager\Package\Collection; use Winter\Packager\Package\Constraint; use Winter\Packager\Package\DetailedPackage; @@ -122,17 +122,9 @@ public function __call($name, $arguments) // Create a command instance. if (is_string($this->commands[$name])) { - $command = new $this->commands[$name]($this); - } elseif (is_object($this->commands[$name]) && $this->commands[$name] instanceof Command) { + $command = new $this->commands[$name]($this, ...$arguments); + } elseif ($this->commands[$name] instanceof Command) { $command = $this->commands[$name]; - } else { - throw new \Winter\Packager\Exceptions\CommandException( - sprintf( - 'The handler for command "%s" is not an instance of "%s"', - $name, - Command::class - ) - ); } // Allow for command handling @@ -331,8 +323,20 @@ public function getCommands(): array /** * Sets a command. */ - public function setCommand(string $command, Command $commandClass): static + public function setCommand(string $command, string|Command $commandClass): static { + // Check that command class is a valid Command class + $reflection = new \ReflectionClass($commandClass); + if (!$reflection->isSubclassOf(Command::class)) { + throw new \Exception( + sprintf( + 'Invalid command class "%s" - the class must extend "%s"', + $commandClass, + Command::class + ) + ); + } + $this->commands[$command] = $commandClass; return $this; } diff --git a/src/Enums/ShowMode.php b/src/Enums/ShowMode.php new file mode 100644 index 0000000..2345699 --- /dev/null +++ b/src/Enums/ShowMode.php @@ -0,0 +1,50 @@ + 'locked', + static::PLATFORM => 'platform', + static::AVAILABLE => 'available', + default => 'installed', + }; + } +} diff --git a/src/Package/Package.php b/src/Package/Package.php index c8c0c85..e81fe30 100644 --- a/src/Package/Package.php +++ b/src/Package/Package.php @@ -11,8 +11,8 @@ * The package may have a description. * * This is generally returned by searches and lists, as we may or may not have the full package information available - * in the output of these commands. You can convert this to a detailed package by using the - * `Package::getDetailedPackage()` method. + * in the output of these commands. You can convert this to a detailed package by using the `Package::toDetailed()` + * method, which will extract information about the package direct from Packagist API. * * @author Ben Thomson * @since 0.3.0 @@ -60,4 +60,13 @@ public function getDescription(): string { return $this->description; } + + public function toDetailed(): DetailedPackage + { + return new DetailedPackage( + $this->namespace, + $this->name, + $this->description + ); + } } diff --git a/tests/Cases/Commands/SearchTest.php b/tests/Cases/Commands/SearchTest.php index 5aead52..f698501 100644 --- a/tests/Cases/Commands/SearchTest.php +++ b/tests/Cases/Commands/SearchTest.php @@ -4,7 +4,6 @@ namespace Winter\Packager\Tests\Cases; -use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use Winter\Packager\Commands\Search; use Winter\Packager\Tests\ComposerTestCase; @@ -14,8 +13,6 @@ */ final class SearchTest extends ComposerTestCase { - use ArraySubsetAsserts; - /** * @test * @testdox can run a (mocked) search and show a few results. @@ -49,7 +46,9 @@ public function itCanRunAMockedSearch(): void 'url' => 'https://packagist.org/packages/winter/wn-backend-module', 'repository' => 'https://github.com/wintercms/wn-backend-module', ], - ], JSON_PRETTY_PRINT) + ], JSON_PRETTY_PRINT), + 'winter', + 'winter-module', ); $results = $this->composer->search('winter', 'winter-module'); diff --git a/tests/Cases/Commands/ShowTest.php b/tests/Cases/Commands/ShowTest.php index 20e70cf..5fdbf79 100644 --- a/tests/Cases/Commands/ShowTest.php +++ b/tests/Cases/Commands/ShowTest.php @@ -4,6 +4,7 @@ namespace Winter\Packager\Tests\Cases; +use Winter\Packager\Enums\ShowMode; use Winter\Packager\Enums\VersionStatus; use Winter\Packager\Exceptions\CommandException; use Winter\Packager\Tests\ComposerTestCase; @@ -63,7 +64,7 @@ public function itCanShowOnePackage() $this->composer->update(); /** @var \Winter\Packager\Package\DetailedPackage */ - $result = $this->composer->show(null, 'composer/ca-bundle'); + $result = $this->composer->show(ShowMode::INSTALLED, 'composer/ca-bundle'); $this->assertInstanceOf(\Winter\Packager\Package\Package::class, $result); $this->assertEquals('composer', $result->getNamespace()); @@ -85,7 +86,7 @@ public function itCanSafelyHandleAMissingPackage() $this->copyToWorkDir($this->testBasePath() . '/fixtures/valid/simple/composer.json'); $this->composer->update(); - $this->composer->show(null, 'missing/package'); + $this->composer->show(ShowMode::INSTALLED, 'missing/package'); } /** @@ -99,7 +100,7 @@ public function itCanShowOutdatedPackages() $this->copyToWorkDir($this->testBasePath() . '/fixtures/valid/simple/composer.json'); $this->composer->update(); - $results = $this->composer->show('outdated'); + $results = $this->composer->show(ShowMode::OUTDATED); $this->assertCount(2, $results); $this->assertContainsOnlyInstancesOf(\Winter\Packager\Package\Package::class, $results); diff --git a/tests/Cases/Commands/VersionTest.php b/tests/Cases/Commands/VersionTest.php index e9dea2b..7f92201 100644 --- a/tests/Cases/Commands/VersionTest.php +++ b/tests/Cases/Commands/VersionTest.php @@ -43,7 +43,8 @@ public function itCanGetTheReleaseDateOfComposer(): void 'version', Version::class, 0, - 'Composer version 2.0.12 2021-04-01 10:14:59' + 'Composer version 2.0.12 2021-04-01 10:14:59', + 'date', ); $this->assertEquals('2021-04-01', $this->composer->version('date')); @@ -61,7 +62,8 @@ public function itCanGetTheReleaseDateTimeOfComposer(): void 'version', Version::class, 0, - 'Composer version 2.0.12 2021-04-01 10:14:59' + 'Composer version 2.0.12 2021-04-01 10:14:59', + 'dateTime', ); $this->assertEquals('2021-04-01 10:14:59', $this->composer->version('dateTime')); @@ -79,7 +81,8 @@ public function itCanGetAllReleaseInfoOfComposer(): void 'version', Version::class, 0, - 'Composer version 2.0.12 2021-04-01 10:14:59' + 'Composer version 2.0.12 2021-04-01 10:14:59', + 'all', ); $this->assertEquals([ diff --git a/tests/Cases/Package/ConstraintTest.php b/tests/Cases/Package/ConstraintTest.php index 583fc69..f434242 100644 --- a/tests/Cases/Package/ConstraintTest.php +++ b/tests/Cases/Package/ConstraintTest.php @@ -71,7 +71,7 @@ public function itThrowsOnInvalidPackageAndExtensionsNames(string $method, strin $constraint = Constraint::$method($packageName); } - public function invalidPackageNames(): array + public static function invalidPackageNames(): array { return [ ['package', 'invalid-package-name'], diff --git a/tests/ComposerTestCase.php b/tests/ComposerTestCase.php index d39729c..b78d992 100644 --- a/tests/ComposerTestCase.php +++ b/tests/ComposerTestCase.php @@ -57,12 +57,13 @@ public function createComposer(): void * @param string $output * @return void */ - protected function mockCommandOutput(string $command, string $commandClass, int $code = 0, string $output = ''): void + protected function mockCommandOutput(string $command, string $commandClass, int $code = 0, string $output = '', mixed ...$constructorArgs): void { // Mock the command and replace the "runCommand" method $mockCommand = $this->getMockBuilder($commandClass) ->setConstructorArgs([ - $this->composer + $this->composer, + ...$constructorArgs, ]) ->onlyMethods(['runComposerCommand']) ->getMock();