Added Capable plugins for a more future-proof Plugin API

Plugins can now present their capabilities to the PluginManager, through which it can act accordingly, thus making Plugin API more flexible, BC-friendly and decoupled.
main
nevvermind 9 years ago
parent 7d7b3ccb2a
commit 2051d74774

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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
{
}

@ -0,0 +1,48 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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();
}

@ -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

@ -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;
}
}

@ -0,0 +1,23 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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;
}
}

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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
{
}

@ -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));
}
}

Loading…
Cancel
Save