Skip to content

Commit

Permalink
Add separate method to discover plugin commands.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
markstory committed Mar 9, 2018
1 parent 4332b35 commit d96c600
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 94 deletions.
91 changes: 53 additions & 38 deletions src/Console/CommandCollection.php
Expand Up @@ -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.
*
Expand All @@ -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);
}
}
46 changes: 15 additions & 31 deletions src/Console/CommandScanner.php
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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);
}

/**
Expand Down
64 changes: 39 additions & 25 deletions tests/TestCase/Console/CommandCollectionTest.php
Expand Up @@ -21,6 +21,7 @@
use Cake\Shell\I18nShell;
use Cake\Shell\RoutesShell;
use Cake\TestSuite\TestCase;
use InvalidArgumentException;
use stdClass;
use TestApp\Command\DemoCommand;

Expand Down Expand Up @@ -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']);
}
}

0 comments on commit d96c600

Please sign in to comment.