Skip to content

Commit

Permalink
add plugin versions to plugin list command
Browse files Browse the repository at this point in the history
  • Loading branch information
LordSimal committed Jan 13, 2024
1 parent 2955123 commit 393fd43
Show file tree
Hide file tree
Showing 5 changed files with 565 additions and 6 deletions.
10 changes: 8 additions & 2 deletions src/Command/PluginListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ public static function defaultName(): string
public function execute(Arguments $args, ConsoleIo $io): ?int
{
$loadedPluginsCollection = Plugin::getCollection();
$config = PluginConfig::getAppConfig();
$path = (string)$args->getOption('composer-path');
$config = PluginConfig::getAppConfig($path ?: null);

$table = [
['Plugin', 'Is Loaded', 'Only Debug', 'Only CLI', 'Optional'],
['Plugin', 'Is Loaded', 'Only Debug', 'Only CLI', 'Optional', 'Version'],
];

if (empty($config)) {
Expand All @@ -63,12 +64,14 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$onlyDebug = $options['onlyDebug'] ?? false;
$onlyCli = $options['onlyCli'] ?? false;
$optional = $options['optional'] ?? false;
$version = $options['version'] ?? '';
$table[] = [
$pluginName,
$isLoaded ? 'X' : '',
$onlyDebug ? 'X' : '',
$onlyCli ? 'X' : '',
$optional ? 'X' : '',
$version,
];
}
$io->helper('Table')->output($table);
Expand All @@ -85,6 +88,9 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription('Displays all currently available plugins.');
$parser->addOption('composer-path', [
'help' => 'The absolute path to the composer.lock file to retrieve the versions from',
]);

return $parser;
}
Expand Down
89 changes: 85 additions & 4 deletions src/Core/PluginConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
namespace Cake\Core;

use Cake\Core\Exception\CakeException;
use Cake\Utility\Hash;

/**
Expand Down Expand Up @@ -57,9 +58,10 @@ public static function loadInstallerConfig(): void
/**
* Get the config how plugins should be loaded
*
* @param string|null $path The absolute path to the composer.lock file to retrieve the versions from
* @return array
*/
public static function getAppConfig(): array
public static function getAppConfig(?string $path = null): array
{
self::loadInstallerConfig();

Expand All @@ -71,11 +73,16 @@ public static function getAppConfig(): array
$pluginLoadConfig = [];
}

try {
$composerVersions = self::getVersions($path);
} catch (CakeException) {
$composerVersions = [];
}

$result = [];
$availablePlugins = Configure::read('plugins', []);
if ($availablePlugins && is_array($availablePlugins)) {
$availablePlugins = array_keys($availablePlugins);
foreach ($availablePlugins as $pluginName) {
foreach ($availablePlugins as $pluginName => $pluginPath) {
if ($pluginLoadConfig && array_key_exists($pluginName, $pluginLoadConfig)) {
$options = $pluginLoadConfig[$pluginName];
$hooks = PluginInterface::VALID_HOOKS;
Expand All @@ -92,15 +99,89 @@ public static function getAppConfig(): array
} else {
$result[$pluginName]['isLoaded'] = false;
}

try {
$packageName = self::getPackageNameFromPath($pluginPath);
$result[$pluginName]['packagePath'] = $pluginPath;
$result[$pluginName]['package'] = $packageName;
} catch (CakeException) {
$packageName = null;
}
if ($composerVersions && $packageName) {
if (array_key_exists($packageName, $composerVersions['packages'])) {
$result[$pluginName]['version'] = $composerVersions['packages'][$packageName];
$result[$pluginName]['isDevPackage'] = false;
} elseif (array_key_exists($packageName, $composerVersions['devPackages'])) {
$result[$pluginName]['version'] = $composerVersions['devPackages'][$packageName];
$result[$pluginName]['isDevPackage'] = true;
}
}
}
}

$diff = array_diff(array_keys($pluginLoadConfig), $availablePlugins);
$diff = array_diff(array_keys($pluginLoadConfig), array_keys($availablePlugins));
foreach ($diff as $unknownPlugin) {
$result[$unknownPlugin]['isLoaded'] = false;
$result[$unknownPlugin]['isUnknown'] = true;
}

return $result;
}

/**
* @param string|null $path The absolute path to the composer.lock file to retrieve the versions from
* @return array
*/
public static function getVersions(?string $path = null): array
{
$lockFilePath = $path ?? ROOT . DIRECTORY_SEPARATOR . 'composer.lock';
if (!file_exists($lockFilePath)) {
throw new CakeException(sprintf('composer.lock does not exist in %s', $lockFilePath));
}
$lockFile = file_get_contents($lockFilePath);
if ($lockFile === false) {
throw new CakeException(sprintf('Could not read composer.lock: %s', $lockFilePath));
}
$lockFileJson = json_decode($lockFile, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing composer.lock: %s',
json_last_error_msg()
));
}

$packages = Hash::combine($lockFileJson['packages'], '{n}.name', '{n}.version');
$devPackages = Hash::combine($lockFileJson['packages-dev'], '{n}.name', '{n}.version');

return [
'packages' => $packages,
'devPackages' => $devPackages,
];
}

/**
* @param string $path
* @return string
*/
protected static function getPackageNameFromPath(string $path): string
{
$jsonPath = $path . DS . 'composer.json';
if (!file_exists($jsonPath)) {
throw new CakeException(sprintf('composer.json does not exist in %s', $jsonPath));
}
$jsonString = file_get_contents($jsonPath);
if ($jsonString === false) {
throw new CakeException(sprintf('Could not read composer.json: %s', $jsonPath));
}
$json = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new CakeException(sprintf(
'Error parsing %ss: %s',
$jsonPath,
json_last_error_msg()
));
}

return $json['name'];
}
}
33 changes: 33 additions & 0 deletions tests/TestCase/Command/PluginListCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,37 @@ public function testListUnknown(): void

$this->exec('plugin list');
}

/**
* Test listing vendor plugins with versions
*/
public function testListWithVersions(): void
{
$file = <<<PHP
<?php
declare(strict_types=1);
return [
'plugins' => [
'Chronos' => ROOT . '/vendor/cakephp/chronos',
'CodeSniffer' => ROOT . '/vendor/cakephp/cakephp-codesniffer'
]
];
PHP;
file_put_contents($this->pluginsListPath, $file);

$config = <<<PHP
<?php
declare(strict_types=1);
return [
'Chronos',
'CodeSniffer'
];
PHP;
file_put_contents($this->pluginsConfigPath, $config);

$path = ROOT . DS . 'tests' . DS . 'composer.lock';
$this->exec(sprintf('plugin list --composer-path="%s"', $path));
$this->assertOutputContains('| Chronos | X | | | | 3.0.4 |');
$this->assertOutputContains('| CodeSniffer | X | | | | 5.1.1 |');
}
}
125 changes: 125 additions & 0 deletions tests/TestCase/Core/PluginConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,129 @@ public function testNoPluginConfig(): void
],
], PluginConfig::getAppConfig());
}

public function testGetVersions(): void
{
$test = PluginConfig::getVersions(ROOT . DS . 'tests' . DS . 'composer.lock');
$expected = [
'packages' => [
'cakephp/chronos' => '3.0.4',
'psr/simple-cache' => '3.0.0',
],
'devPackages' => [
'cakephp/cakephp-codesniffer' => '5.1.1',
'squizlabs/php_codesniffer' => '3.8.1',
'theseer/tokenizer' => '1.2.2',
],
];
$this->assertEquals($expected, $test);
}

public function testSimpleConfiWithVersions(): void
{
$file = <<<PHP
<?php
return [
'plugins' => [
'Chronos' => ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'chronos',
'CodeSniffer' => ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'cakephp-codesniffer'
]
];
PHP;
file_put_contents($this->pluginsListPath, $file);

$config = <<<PHP
<?php
return [
'Chronos',
'CodeSniffer'
];
PHP;
file_put_contents($this->pluginsConfigPath, $config);

Configure::delete('plugins');
$pathToRootVendor = ROOT . DS . 'vendor' . DS;
$result = [
'Chronos' => [
'isLoaded' => true,
'onlyDebug' => false,
'onlyCli' => false,
'optional' => false,
'bootstrap' => true,
'console' => true,
'middleware' => true,
'routes' => true,
'services' => true,
'packagePath' => $pathToRootVendor . 'cakephp' . DS . 'chronos',
'package' => 'cakephp/chronos',
'version' => '3.0.4',
'isDevPackage' => false,
],
'CodeSniffer' => [
'isLoaded' => true,
'onlyDebug' => false,
'onlyCli' => false,
'optional' => false,
'bootstrap' => true,
'console' => true,
'middleware' => true,
'routes' => true,
'services' => true,
'packagePath' => $pathToRootVendor . 'cakephp' . DS . 'cakephp-codesniffer',
'package' => 'cakephp/cakephp-codesniffer',
'version' => '5.1.1',
'isDevPackage' => true,
],
];
$this->assertSame($result, PluginConfig::getAppConfig(ROOT . DS . 'tests' . DS . 'composer.lock'));
}

public function testInvalidComposerLock(): void
{
$path = ROOT . DS . 'tests' . DS . 'unknown_composer.lock';
$this->assertSame([], PluginConfig::getAppConfig($path));

file_put_contents($path, 'invalid-json');
$this->assertSame([], PluginConfig::getAppConfig($path));
unlink($path);
}

public function testInvalidComposerJson(): void
{
$pathToTestPlugin = ROOT . DS . 'tests' . DS . 'test_app' . DS . 'Plugin' . DS . 'TestPlugin' . DS;
$file = <<<PHP
<?php
return [
'plugins' => [
'TestPlugin' => ROOT . DS . 'tests' . DS . 'test_app' . DS . 'Plugin' . DS . 'TestPlugin' . DS,
]
];
PHP;
file_put_contents($this->pluginsListPath, $file);

$config = <<<PHP
<?php
return [
'TestPlugin',
];
PHP;
file_put_contents($this->pluginsConfigPath, $config);

file_put_contents($pathToTestPlugin . 'composer.json', 'invalid-json');

$this->assertSame([
'TestPlugin' => [
'isLoaded' => true,
'onlyDebug' => false,
'onlyCli' => false,
'optional' => false,
'bootstrap' => true,
'console' => true,
'middleware' => true,
'routes' => true,
'services' => true,
],
], PluginConfig::getAppConfig());
unlink($pathToTestPlugin . 'composer.json');
}
}
Loading

0 comments on commit 393fd43

Please sign in to comment.