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.pull/4124/head
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…
Reference in New Issue