diff --git a/src/Composer/Plugin/Capability/Capability.php b/src/Composer/Plugin/Capability/Capability.php new file mode 100644 index 000000000..335d7344c --- /dev/null +++ b/src/Composer/Plugin/Capability/Capability.php @@ -0,0 +1,24 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin\Capability; + +/** + * Marker interface for Plugin capabilities. + * Every new Capability which is added to the Plugin API must implement this interface. + * + * @api + * @since Plugin API 1.1 + */ +interface Capability +{ +} diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php new file mode 100644 index 000000000..1dc8f5bad --- /dev/null +++ b/src/Composer/Plugin/Capable.php @@ -0,0 +1,48 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +/** + * Plugins which need to expose various implementations + * of the Composer Plugin Capabilities must have their + * declared Plugin class implementing this interface. + * + * @api + * @since Plugin API 1.1 + */ +interface Capable +{ + /** + * Method by which a Plugin announces its API implementations, through an array + * with a special structure. + * + * The key must be a string, representing a fully qualified class/interface name + * which Composer Plugin API exposes - named "API class". + * The value must be a string as well, representing the fully qualified class name + * of the API class - named "SPI class". + * + * Every SPI must implement their API class. + * + * Every SPI will be passed a single array parameter via their constructor. + * + * Example: + * // API as key, SPI as value + * return array( + * 'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider', + * 'Composer\Plugin\Capability\Validator' => 'My\Validator', + * ); + * + * @return string[] + */ + public function getCapabilities(); +} diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index dea5828c1..6eaca4e90 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -23,14 +23,14 @@ use Composer\IO\IOInterface; interface PluginInterface { /** - * Version number of the fake composer-plugin-api package + * Version number of the internal composer-plugin-api package * * @var string */ - const PLUGIN_API_VERSION = '1.0.0'; + const PLUGIN_API_VERSION = '1.1.0'; /** - * Apply plugin modifications to composer + * Apply plugin modifications to Composer * * @param Composer $composer * @param IOInterface $io diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 27680907d..a402b0b70 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -23,6 +23,7 @@ use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; use Composer\DependencyResolver\Pool; +use Composer\Plugin\Capability\Capability; /** * Plugin manager @@ -185,16 +186,6 @@ class PluginManager } } - /** - * Returns the version of the internal composer-plugin-api package. - * - * @return string - */ - protected function getPluginApiVersion() - { - return PluginInterface::PLUGIN_API_VERSION; - } - /** * Adds a plugin, activates it and registers it with the event dispatcher * @@ -299,4 +290,57 @@ class PluginManager return $this->globalComposer->getInstallationManager()->getInstallPath($package); } + + /** + * Returns the version of the internal composer-plugin-api package. + * + * @return string + */ + protected function getPluginApiVersion() + { + return PluginInterface::PLUGIN_API_VERSION; + } + + /** + * @param PluginInterface $plugin + * @param string $capability + * @return bool|string The fully qualified class of the implementation or false if none was provided + */ + protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) + { + if (!($plugin instanceof Capable)) { + return false; + } + + $capabilities = (array) $plugin->getCapabilities(); + + if (empty($capabilities[$capability]) || !is_string($capabilities[$capability])) { + return false; + } + + return trim($capabilities[$capability]); + } + + /** + * @param PluginInterface $plugin + * @param string $capability The fully qualified name of the API interface which the plugin may provide + * an implementation. + * @param array $ctorArgs Arguments passed to Capability's constructor. + * Keeping it an array will allow future values to be passed w\o changing the signature. + * @return Capability|boolean Bool false if the Plugin has no implementation of the requested Capability. + */ + public function getPluginCapability(PluginInterface $plugin, $capability, array $ctorArgs = array()) + { + if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capability)) { + if (class_exists($capabilityClass)) { + $capabilityObj = new $capabilityClass($ctorArgs); + if ($capabilityObj instanceof Capability && + $capabilityObj instanceof $capability + ) { + return $capabilityObj; + } + } + } + return false; + } } diff --git a/tests/Composer/Test/Plugin/Mock/Capability.php b/tests/Composer/Test/Plugin/Mock/Capability.php new file mode 100644 index 000000000..79635a314 --- /dev/null +++ b/tests/Composer/Test/Plugin/Mock/Capability.php @@ -0,0 +1,23 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Plugin\Mock; + +class Capability implements \Composer\Plugin\Capability\Capability +{ + public $args; + + public function __construct(array $args) + { + $this->args = $args; + } +} diff --git a/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php new file mode 100644 index 000000000..5e8d88c31 --- /dev/null +++ b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php @@ -0,0 +1,20 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Plugin\Mock; + +use Composer\Plugin\Capable; +use Composer\Plugin\PluginInterface; + +interface CapablePluginInterface extends PluginInterface, Capable +{ +} diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index b449d7e90..024561bd2 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -314,4 +314,67 @@ class PluginInstallerTest extends TestCase $this->setPluginApiVersionWithPlugins('5.5.0', $pluginWithApiConstraint); $this->assertCount(0, $this->pm->getPlugins()); } + + public function testIncapablePluginIsCorrectlyDetected() + { + $plugin = $this->getMockBuilder('Composer\Plugin\PluginInterface') + ->getMock(); + + $this->assertFalse($this->pm->getPluginCapability($plugin, 'Fake\Ability')); + } + + public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWithArgs() + { + $capabilityApi = 'Composer\Plugin\Capability\Capability'; + $capabilitySpi = 'Composer\Test\Plugin\Mock\Capability'; + + $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') + ->getMock(); + + $plugin->expects($this->once()) + ->method('getCapabilities') + ->will($this->returnCallback(function() use ($capabilitySpi, $capabilityApi) { + return array($capabilityApi => $capabilitySpi); + })); + + $capability = $this->pm->getPluginCapability($plugin, $capabilityApi, array('a' => 1, 'b' => 2)); + + $this->assertInstanceOf($capabilityApi, $capability); + $this->assertInstanceOf($capabilitySpi, $capability); + $this->assertSame(array('a' => 1, 'b' => 2), $capability->args); + } + + public function invalidSpiValues() + { + return array( + array(null), + array(""), + array(0), + array(1000), + array(" "), + array(array(1)), + array(array()), + array(new \stdClass()), + array("NonExistentClassLikeMiddleClass"), + ); + } + + /** + * @dataProvider invalidSpiValues + */ + public function testInvalidCapabilitySpiDeclarationsAreDisregarded($invalidSpi) + { + $capabilityApi = 'Composer\Plugin\Capability\Capability'; + + $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') + ->getMock(); + + $plugin->expects($this->once()) + ->method('getCapabilities') + ->will($this->returnCallback(function() use ($invalidSpi, $capabilityApi) { + return array($capabilityApi => $invalidSpi); + })); + + $this->assertFalse($this->pm->getPluginCapability($plugin, $capabilityApi)); + } }