From d96c60094a280f6823a0bb85b18561f19dd700c5 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 8 Mar 2018 22:40:40 -0500 Subject: [PATCH] Add separate method to discover plugin commands. Add a method to discover plugin commands on a per-plugin basis. This will let Plugin classes auto-discover their own command names in a self-contained way. Refs #11137 --- src/Console/CommandCollection.php | 91 +++++++++++-------- src/Console/CommandScanner.php | 46 +++------- .../Console/CommandCollectionTest.php | 64 ++++++++----- 3 files changed, 107 insertions(+), 94 deletions(-) diff --git a/src/Console/CommandCollection.php b/src/Console/CommandCollection.php index c8cde7467d6..294ca73fd99 100644 --- a/src/Console/CommandCollection.php +++ b/src/Console/CommandCollection.php @@ -148,6 +148,54 @@ public function count() return count($this->commands); } + /** + * Auto-discover shell & commands from the named plugin. + * + * Discovered commands will have their names de-duplicated with + * existing commands in the collection. If a command is already + * defined in the collection and discovered in a plugin, only + * the long name (`plugin.command`) will be returned. + * + * @param string $plugin The plugin to scan. + * @return array Discovered plugin commands. + */ + public function discoverPlugin($plugin) + { + $scanner = new CommandScanner(); + $shells = $scanner->scanPlugin($plugin); + + return $this->resolveNames($shells); + } + + /** + * Resolve names based on existing commands + * + * @param array $input The results of a CommandScanner operation. + * @return array A flat map of command names => class names. + */ + protected function resolveNames(array $input) + { + $out = []; + foreach ($input as $info) { + $name = $info['name']; + $addLong = $name !== $info['fullName']; + + // If the short name has been used, use the full name. + // This allows app shells to have name preference. + // and app shells to overwrite core shells. + if ($this->has($name) && $addLong) { + $name = $info['fullName']; + } + + $out[$name] = $info['class']; + if ($addLong) { + $out[$info['fullName']] = $info['class']; + } + } + + return $out; + } + /** * Automatically discover shell commands in CakePHP, the application and all plugins. * @@ -156,52 +204,19 @@ public function count() * * - CakePHP provided commands * - Application commands - * - Plugin commands * - * Commands from plugins will be added based on the order plugins are loaded. - * Plugin shells will attempt to use a short name. If however, a plugin - * provides a shell that conflicts with CakePHP or the application shells, - * the full `plugin_name.shell` name will be used. Plugin shells are added - * in the order that plugins were loaded. + * Commands defined in the application will ovewrite commands with + * the same name provided by CakePHP. * * @return array An array of command names and their classes. */ public function autoDiscover() { $scanner = new CommandScanner(); - $shells = $scanner->scanAll(); - $adder = function ($out, $shells, $key) { - if (empty($shells[$key])) { - return $out; - } - - foreach ($shells[$key] as $info) { - $name = $info['name']; - $addLong = $name !== $info['fullName']; - - // If the short name has been used, use the full name. - // This allows app shells to have name preference. - // and app shells to overwrite core shells. - if (isset($out[$name]) && $addLong) { - $name = $info['fullName']; - } - - $out[$name] = $info['class']; - if ($addLong) { - $out[$info['fullName']] = $info['class']; - } - } - - return $out; - }; + $core = $this->resolveNames($scanner->scanCore()); + $app = $this->resolveNames($scanner->scanApp()); - $out = $adder([], $shells, 'CORE'); - $out = $adder($out, $shells, 'app'); - foreach (array_keys($shells['plugins']) as $key) { - $out = $adder($out, $shells['plugins'], $key); - } - - return $out; + return array_merge($core, $app); } } diff --git a/src/Console/CommandScanner.php b/src/Console/CommandScanner.php index e44e28e2d87..085692c7e6c 100644 --- a/src/Console/CommandScanner.php +++ b/src/Console/CommandScanner.php @@ -19,6 +19,7 @@ use Cake\Core\Plugin; use Cake\Filesystem\Folder; use Cake\Utility\Inflector; +use InvalidArgumentException; /** * Used by CommandCollection and CommandTask to scan the filesystem @@ -28,28 +29,12 @@ */ class CommandScanner { - /** - * Scan CakePHP core, the applications and plugins for shell classes - * - * @return array - */ - public function scanAll() - { - $shellList = [ - 'CORE' => $this->scanCore(), - 'app' => $this->scanApp(), - 'plugins' => $this->scanPlugins() - ]; - - return $shellList; - } - /** * Scan CakePHP internals for shells & commands. * * @return array A list of command metadata. */ - protected function scanCore() + public function scanCore() { $coreShells = $this->scanDir( dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Shell' . DIRECTORY_SEPARATOR, @@ -72,7 +57,7 @@ protected function scanCore() * * @return array A list of command metadata. */ - protected function scanApp() + public function scanApp() { $appNamespace = Configure::read('App.namespace'); $appShells = $this->scanDir( @@ -92,25 +77,24 @@ protected function scanApp() } /** - * Scan the plugins for shells & commands. + * Scan the named plugin for shells and commands * + * @param string $plugin The named plugin. * @return array A list of command metadata. */ - protected function scanPlugins() + public function scanPlugin($plugin) { - $plugins = []; - foreach (Plugin::loaded() as $plugin) { - $path = Plugin::classPath($plugin); - $namespace = str_replace('/', '\\', $plugin); - $prefix = Inflector::underscore($plugin) . '.'; - - $commands = $this->scanDir($path . 'Command', $namespace . '\Command\\', $prefix, []); - $shells = $this->scanDir($path . 'Shell', $namespace . '\Shell\\', $prefix, []); - - $plugins[$plugin] = array_merge($shells, $commands); + if (!Plugin::loaded($plugin)) { + return []; } + $path = Plugin::classPath($plugin); + $namespace = str_replace('/', '\\', $plugin); + $prefix = Inflector::underscore($plugin) . '.'; + + $commands = $this->scanDir($path . 'Command', $namespace . '\Command\\', $prefix, []); + $shells = $this->scanDir($path . 'Shell', $namespace . '\Shell\\', $prefix, []); - return $plugins; + return array_merge($shells, $commands); } /** diff --git a/tests/TestCase/Console/CommandCollectionTest.php b/tests/TestCase/Console/CommandCollectionTest.php index fd623926cc2..a52cc455065 100644 --- a/tests/TestCase/Console/CommandCollectionTest.php +++ b/tests/TestCase/Console/CommandCollectionTest.php @@ -21,6 +21,7 @@ use Cake\Shell\I18nShell; use Cake\Shell\RoutesShell; use Cake\TestSuite\TestCase; +use InvalidArgumentException; use stdClass; use TestApp\Command\DemoCommand; @@ -237,50 +238,63 @@ public function testAutoDiscoverCore() $this->assertSame('Cake\Command\VersionCommand', $collection->get('version')); } + /** + * test missing plugin discovery + * + * @return void + */ + public function testDiscoverPluginUnknown() + { + $this->assertSame([], $collection = new CommandCollection()); + } + /** * test autodiscovering plugin shells * * @return void */ - public function testAutoDiscoverPlugin() + public function testDiscoverPlugin() { Plugin::load('TestPlugin'); Plugin::load('Company/TestPluginThree'); + $collection = new CommandCollection(); - $collection->addMany($collection->autoDiscover()); + // Add a dupe to test de-duping + $collection->add('sample', DemoCommand::class); - $this->assertTrue( - $collection->has('example'), + $result = $collection->discoverPlugin('TestPlugin'); + + $this->assertArrayHasKey( + 'example', + $result, 'Used short name for unique plugin shell' ); - $this->assertTrue( - $collection->has('test_plugin.example'), + $this->assertArrayHasKey( + 'test_plugin.example', + $result, 'Long names are stored for unique shells' ); - $this->assertTrue( - $collection->has('sample'), - 'Has app shell' - ); - $this->assertTrue( - $collection->has('test_plugin.sample'), + $this->assertArrayNotHasKey('sample', $result, 'Existing command not output'); + $this->assertArrayHasKey( + 'test_plugin.sample', + $result, 'Duplicate shell was given a full alias' ); - $this->assertTrue( - $collection->has('company'), + $this->assertEquals('TestPlugin\Shell\ExampleShell', $result['example']); + $this->assertEquals($result['example'], $result['test_plugin.example']); + $this->assertEquals('TestPlugin\Shell\SampleShell', $result['test_plugin.sample']); + + $result = $collection->discoverPlugin('Company/TestPluginThree'); + $this->assertArrayHasKey( + 'company', + $result, 'Used short name for unique plugin shell' ); - $this->assertTrue( - $collection->has('company/test_plugin_three.company'), + $this->assertArrayHasKey( + 'company/test_plugin_three.company', + $result, 'Long names are stored as well' ); - - $this->assertEquals('TestPlugin\Shell\ExampleShell', $collection->get('example')); - $this->assertEquals($collection->get('example'), $collection->get('test_plugin.example')); - $this->assertEquals( - 'TestApp\Shell\SampleShell', - $collection->get('sample'), - 'Should prefer app shells over plugin ones' - ); - $this->assertEquals('TestPlugin\Shell\SampleShell', $collection->get('test_plugin.sample')); + $this->assertSame($result['company'], $result['company/test_plugin_three.company']); } }