diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md
index b4997d22d..bf72aa235 100644
--- a/doc/articles/plugins.md
+++ b/doc/articles/plugins.md
@@ -183,6 +183,84 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
}
```
+## Plugin capabilities
+
+Composer defines a standard set of capabilities which may be implemented by plugins.
+Their goal is to make the plugin ecosystem more stable as it reduces the need to mess
+with [`Composer\Composer`][4]'s internal state, by providing explicit extension points
+for common plugin requirements.
+
+Capable Plugins classes must implement the [`Composer\Plugin\Capable`][8] interface
+and declare their capabilities in the `getCapabilities()` method.
+This method must return an array, with the _key_ as a Composer Capability class name,
+and the _value_ as the Plugin's own implementation class name of said Capability:
+
+```php
+ 'My\Composer\CommandProvider',
+ );
+ }
+}
+```
+
+### Command provider
+
+The [`Composer\Plugin\Capability\CommandProvider`][9] capability allows to register
+additional commands for Composer :
+
+```php
+setName('custom-plugin-command');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Executing');
+ }
+}
+```
+
+Now the `custom-plugin-command` is available alongside Composer commands.
+
+> _Composer commands are based on the [Symfony Console Component][10]._
+
## Using Plugins
Plugin packages are automatically loaded as soon as they are installed and will
@@ -202,3 +280,6 @@ local project plugins are loaded.
[5]: https://github.com/composer/composer/blob/master/src/Composer/IO/IOInterface.php
[6]: https://github.com/composer/composer/blob/master/src/Composer/EventDispatcher/EventSubscriberInterface.php
[7]: ../01-basic-usage.md#package-versions
+[8]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/Capable.php
+[9]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/Capability/CommandProvider.php
+[10]: http://symfony.com/doc/current/components/console/introduction.html
diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php
index 2a70b584d..365733d85 100644
--- a/src/Composer/Command/BaseCommand.php
+++ b/src/Composer/Command/BaseCommand.php
@@ -79,6 +79,18 @@ abstract class BaseCommand extends Command
$this->getApplication()->resetComposer();
}
+ /**
+ * Whether or not this command is meant to call another command.
+ *
+ * This is mainly needed to avoid duplicated warnings messages.
+ *
+ * @return bool
+ */
+ public function isProxyCommand()
+ {
+ return false;
+ }
+
/**
* @return IOInterface
*/
diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php
index 9aa48706b..beeca5b77 100644
--- a/src/Composer/Command/GlobalCommand.php
+++ b/src/Composer/Command/GlobalCommand.php
@@ -80,4 +80,12 @@ EOT
return $this->getApplication()->run($input, $output);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isProxyCommand()
+ {
+ return true;
+ }
}
diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php
index d89434951..b3cf566df 100644
--- a/src/Composer/Command/OutdatedCommand.php
+++ b/src/Composer/Command/OutdatedCommand.php
@@ -71,4 +71,12 @@ EOT
return $this->getApplication()->run($input, $output);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isProxyCommand()
+ {
+ return true;
+ }
}
diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php
index 9ad0d1308..5ad94a59e 100644
--- a/src/Composer/Console/Application.php
+++ b/src/Composer/Console/Application.php
@@ -56,6 +56,8 @@ class Application extends BaseApplication
/_/
';
+ private $hasPluginCommands = false;
+
public function __construct()
{
static $shutdownRegistered = false;
@@ -108,7 +110,7 @@ class Application extends BaseApplication
$io = $this->io = new ConsoleIO($input, $output, $this->getHelperSet());
ErrorHandler::register($io);
- // determine command name to be executed
+ // determine command name to be executed without including plugin commands
$commandName = '';
if ($name = $this->getCommandName($input)) {
try {
@@ -117,7 +119,27 @@ class Application extends BaseApplication
}
}
- $isProxyCommand = $commandName === 'global' || $commandName === 'outdated';
+ if (!$input->hasParameterOption('--no-plugins') && !$this->hasPluginCommands && 'global' !== $commandName) {
+ foreach ($this->getPluginCommands() as $command) {
+ if ($this->has($command->getName())) {
+ $io->writeError('Plugin command '.$command->getName().' ('.get_class($command).') would override a Composer command and has been skipped');
+ } else {
+ $this->add($command);
+ }
+ }
+ $this->hasPluginCommands = true;
+ }
+
+ // determine command name to be executed incl plugin commands, and check if it's a proxy command
+ $isProxyCommand = false;
+ if ($name = $this->getCommandName($input)) {
+ try {
+ $command = $this->find($name);
+ $commandName = $command->getName();
+ $isProxyCommand = ($command instanceof Command\BaseCommand && $command->isProxyCommand());
+ } catch (\InvalidArgumentException $e) {
+ }
+ }
if (!$isProxyCommand) {
$io->writeError(sprintf(
@@ -197,16 +219,6 @@ class Application extends BaseApplication
$this->io->enableDebugging($startTime);
}
- if (!$input->hasParameterOption('--no-plugins') && !$isProxyCommand) {
- foreach ($this->getPluginCommands() as $command) {
- if ($this->has($command->getName())) {
- $io->writeError('Plugin command '.$command->getName().' ('.get_class($command).') would override a Composer command and has been skipped');
- } else {
- $this->add($command);
- }
- }
- }
-
$result = parent::doRun($input, $output);
if (isset($oldWorkingDir)) {
diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php
index 4e843bfa6..b977b78e0 100644
--- a/tests/Composer/Test/ApplicationTest.php
+++ b/tests/Composer/Test/ApplicationTest.php
@@ -25,7 +25,12 @@ class ApplicationTest extends TestCase
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
- $inputMock->expects($this->once())
+ $inputMock->expects($this->any())
+ ->method('hasParameterOption')
+ ->with($this->equalTo('--no-plugins'))
+ ->will($this->returnValue(true));
+
+ $inputMock->expects($this->any())
->method('getFirstArgument')
->will($this->returnValue('list'));
@@ -68,7 +73,7 @@ class ApplicationTest extends TestCase
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
- $inputMock->expects($this->once())
+ $inputMock->expects($this->any())
->method('getFirstArgument')
->will($this->returnValue($command));