Przeglądaj źródła

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.
nevvermind 9 lat temu
rodzic
commit
2051d74774

+ 24 - 0
src/Composer/Plugin/Capability/Capability.php

@@ -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
+{
+}

+ 48 - 0
src/Composer/Plugin/Capable.php

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

+ 3 - 3
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

+ 54 - 10
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;
+    }
 }

+ 23 - 0
tests/Composer/Test/Plugin/Mock/Capability.php

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

+ 20 - 0
tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php

@@ -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
+{
+}

+ 63 - 0
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));
+    }
 }