Bläddra i källkod

Merge pull request #2179 from naderman/plugins

Plugins
Nils Adermann 11 år sedan
förälder
incheckning
242c58c789
64 ändrade filer med 1433 tillägg och 463 borttagningar
  1. 1 1
      doc/04-schema.md
  2. 46 22
      doc/articles/custom-installers.md
  3. 147 0
      doc/articles/plugins.md
  4. 2 2
      res/composer-schema.json
  5. 3 3
      src/Composer/Autoload/AutoloadGenerator.php
  6. 3 2
      src/Composer/Command/Command.php
  7. 17 10
      src/Composer/Command/CreateProjectCommand.php
  8. 8 1
      src/Composer/Command/DependsCommand.php
  9. 5 0
      src/Composer/Command/DiagnoseCommand.php
  10. 6 0
      src/Composer/Command/DumpAutoloadCommand.php
  11. 17 4
      src/Composer/Command/InstallCommand.php
  12. 6 0
      src/Composer/Command/LicensesCommand.php
  13. 6 0
      src/Composer/Command/RequireCommand.php
  14. 7 0
      src/Composer/Command/SearchCommand.php
  15. 7 0
      src/Composer/Command/ShowCommand.php
  16. 6 0
      src/Composer/Command/StatusCommand.php
  17. 17 4
      src/Composer/Command/UpdateCommand.php
  18. 27 4
      src/Composer/Composer.php
  19. 3 2
      src/Composer/Console/Application.php
  20. 20 5
      src/Composer/Downloader/FileDownloader.php
  21. 3 2
      src/Composer/Downloader/ZipDownloader.php
  22. 69 0
      src/Composer/EventDispatcher/Event.php
  23. 87 5
      src/Composer/EventDispatcher/EventDispatcher.php
  24. 48 0
      src/Composer/EventDispatcher/EventSubscriberInterface.php
  25. 54 14
      src/Composer/Factory.php
  26. 9 8
      src/Composer/Installer.php
  27. 6 4
      src/Composer/Installer/InstallationManager.php
  28. 0 104
      src/Composer/Installer/InstallerInstaller.php
  29. 81 0
      src/Composer/Installer/PluginInstaller.php
  30. 87 0
      src/Composer/Plugin/CommandEvent.php
  31. 41 0
      src/Composer/Plugin/PluginEvents.php
  32. 39 0
      src/Composer/Plugin/PluginInterface.php
  33. 259 0
      src/Composer/Plugin/PluginManager.php
  34. 80 0
      src/Composer/Plugin/PreFileDownloadEvent.php
  35. 8 0
      src/Composer/Repository/PlatformRepository.php
  36. 4 18
      src/Composer/Script/Event.php
  37. 11 0
      src/Composer/Util/RemoteFilesystem.php
  38. 3 3
      tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
  39. 1 1
      tests/Composer/Test/Downloader/FileDownloaderTest.php
  40. 13 12
      tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
  41. 2 2
      tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test
  42. 0 19
      tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php
  43. 0 7
      tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Exception.php
  44. 0 9
      tests/Composer/Test/Installer/Fixtures/installer-v1/composer.json
  45. 0 19
      tests/Composer/Test/Installer/Fixtures/installer-v2/Installer/Custom2.php
  46. 0 7
      tests/Composer/Test/Installer/Fixtures/installer-v2/Installer/Exception.php
  47. 0 9
      tests/Composer/Test/Installer/Fixtures/installer-v2/composer.json
  48. 0 19
      tests/Composer/Test/Installer/Fixtures/installer-v3/Installer/Custom2.php
  49. 0 7
      tests/Composer/Test/Installer/Fixtures/installer-v3/Installer/Exception.php
  50. 0 9
      tests/Composer/Test/Installer/Fixtures/installer-v3/composer.json
  51. 0 20
      tests/Composer/Test/Installer/Fixtures/installer-v4/Installer/Custom1.php
  52. 0 20
      tests/Composer/Test/Installer/Fixtures/installer-v4/Installer/Custom2.php
  53. 0 12
      tests/Composer/Test/Installer/Fixtures/installer-v4/composer.json
  54. 2 2
      tests/Composer/Test/InstallerTest.php
  55. 16 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php
  56. 12 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json
  57. 16 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php
  58. 12 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json
  59. 16 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php
  60. 12 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json
  61. 17 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php
  62. 17 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php
  63. 15 0
      tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json
  64. 39 71
      tests/Composer/Test/Plugin/PluginInstallerTest.php

+ 1 - 1
doc/04-schema.md

@@ -99,7 +99,7 @@ Out of the box, composer supports three types:
   their installation, but contains no files and will not write anything to the
   filesystem. As such, it does not require a dist or source key to be
   installable.
-- **composer-installer:** A package of type `composer-installer` provides an
+- **composer-plugin:** A package of type `composer-plugin` may provide an
   installer for other packages that have a custom type. Read more in the
   [dedicated article](articles/custom-installers.md).
 

+ 46 - 22
doc/articles/custom-installers.md

@@ -29,8 +29,8 @@ An example use-case would be:
 
 > phpDocumentor features Templates that need to be installed outside of the
 > default /vendor folder structure. As such they have chosen to adopt the
-> `phpdocumentor-template` [type][1] and create a Custom Installer to send
-> these templates to the correct folder.
+> `phpdocumentor-template` [type][1] and create a plugin providing the Custom
+> Installer to send these templates to the correct folder.
 
 An example composer.json of such a template package would be:
 
@@ -38,23 +38,24 @@ An example composer.json of such a template package would be:
         "name": "phpdocumentor/template-responsive",
         "type": "phpdocumentor-template",
         "require": {
-            "phpdocumentor/template-installer": "*"
+            "phpdocumentor/template-installer-plugin": "*"
         }
     }
 
 > **IMPORTANT**: to make sure that the template installer is present at the
 > time the template package is installed, template packages should require
-> the installer package.
+> the plugin package.
 
 ## Creating an Installer
 
 A Custom Installer is defined as a class that implements the
-[`Composer\Installer\InstallerInterface`][3] and is contained in a Composer
-package that has the [type][1] `composer-installer`.
+[`Composer\Installer\InstallerInterface`][3] and is usually distributed in a
+Composer Plugin.
 
-A basic Installer would thus compose of two files:
+A basic Installer Plugin would thus compose of three files:
 
 1. the package file: composer.json
+2. The Plugin class, e.g.: `My\Project\Composer\Plugin.php`, containing a class that implements `Composer\Plugin\PluginInterface`.
 2. The Installer class, e.g.: `My\Project\Composer\Installer.php`, containing a class that implements `Composer\Installer\InstallerInterface`.
 
 ### composer.json
@@ -62,35 +63,57 @@ A basic Installer would thus compose of two files:
 The package file is the same as any other package file but with the following
 requirements:
 
-1. the [type][1] attribute must be `composer-installer`.
+1. the [type][1] attribute must be `composer-plugin`.
 2. the [extra][2] attribute must contain an element `class` defining the
-   class name of the installer (including namespace). If a package contains
-   multiple installers this can be array of class names.
+   class name of the plugin (including namespace). If a package contains
+   multiple plugins this can be array of class names.
 
 Example:
 
     {
-        "name": "phpdocumentor/template-installer",
-        "type": "composer-installer",
+        "name": "phpdocumentor/template-installer-plugin",
+        "type": "composer-installer-plugin",
         "license": "MIT",
         "autoload": {
             "psr-0": {"phpDocumentor\\Composer": "src/"}
         },
         "extra": {
-            "class": "phpDocumentor\\Composer\\TemplateInstaller"
+            "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin"
         }
     }
 
-### The Custom Installer class
+### The Plugin class
 
-The class that executes the custom installation should implement the
-[`Composer\Installer\InstallerInterface`][3] (or extend another installer that
-implements that interface).
+The class defining the Composer plugin must implement the
+[`Composer\Plugin\PluginInterface`][3]. It can then register the Custom
+Installer in its `activate()` method.
 
 The class may be placed in any location and have any name, as long as it is
 autoloadable and matches the `extra.class` element in the package definition.
-It will also define the [type][1] string as it will be recognized by packages
-that will use this installer in the `supports()` method.
+
+Example:
+
+    namespace phpDocumentor\Composer;
+
+    use Composer\Composer;
+    use Composer\IO\IOInterface;
+    use Composer\Plugin\PluginInterface
+
+    class TemplateInstallerPlugin implements PluginInterface
+    {
+        public function activate(Composer $composer, IOInterface $io)
+        {
+            $installer = new TemplateInstaller($io, $composer);
+            $composer->getInstallationManager()->addInstaller($installer);
+        }
+    }
+
+### The Custom Installer class
+
+The class that executes the custom installation should implement the
+[`Composer\Installer\InstallerInterface`][4] (or extend another installer that
+implements that interface). It defines the [type][1] string as it will be
+recognized by packages that will use this installer in the `supports()` method.
 
 > **NOTE**: _choose your [type][1] name carefully, it is recommended to follow
 > the format: `vendor-type`_. For example: `phpdocumentor-template`.
@@ -146,7 +169,7 @@ Example:
     }
 
 The example demonstrates that it is quite simple to extend the
-[`Composer\Installer\LibraryInstaller`][4] class to strip a prefix
+[`Composer\Installer\LibraryInstaller`][5] class to strip a prefix
 (`phpdocumentor/template-`) and use the remaining part to assemble a completely
 different installation path.
 
@@ -155,5 +178,6 @@ different installation path.
 
 [1]: ../04-schema.md#type
 [2]: ../04-schema.md#extra
-[3]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php
-[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php
+[3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php
+[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php
+[5]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php

+ 147 - 0
doc/articles/plugins.md

@@ -0,0 +1,147 @@
+<!--
+    tagline: Modify and extend Composer's functionality
+-->
+
+# Setting up and using plugins
+
+## Synopsis
+
+You may wish to alter or expand Composer's functionality with your own. For
+example if your environment poses special requirements on the behaviour of
+Composer which do not apply to the majority of its users or if you wish to
+accomplish something with composer in a way that is not desired by most users.
+
+In these cases you could consider creating a plugin to handle your
+specific logic.
+
+## Creating a Plugin
+
+A plugin is a regular composer package which ships its code as part of the
+package and may also depend on further packages.
+
+### Plugin Package
+
+The package file is the same as any other package file but with the following
+requirements:
+
+1. the [type][1] attribute must be `composer-plugin`.
+2. the [extra][2] attribute must contain an element `class` defining the
+   class name of the plugin (including namespace). If a package contains
+   multiple plugins this can be array of class names.
+
+Additionally you must require the special package called `composer-plugin-api`
+to define which composer API versions your plugin is compatible with. The
+current composer plugin API version is 1.0.0.
+
+For example
+
+    {
+        "name": "my/plugin-package",
+        "type": "composer-plugin",
+        "require": {
+            "composer-plugin-api": "1.0.0"
+        }
+    }
+
+### Plugin Class
+
+Every plugin has to supply a class which implements the
+[`Composer\Plugin\PluginInterface`][3]. The `activate()` method of the plugin
+is called after the plugin is loaded and receives an instance of
+[`Composer\Composer`][4] as well as an instance of
+[`Composer\IO\IOInterface`][5]. Using these two objects all configuration can
+be read and all internal objects and state can be manipulated as desired.
+
+Example:
+
+    namespace phpDocumentor\Composer;
+
+    use Composer\Composer;
+    use Composer\IO\IOInterface;
+    use Composer\Plugin\PluginInterface
+
+    class TemplateInstallerPlugin implements PluginInterface
+    {
+        public function activate(Composer $composer, IOInterface $io)
+        {
+            $installer = new TemplateInstaller($io, $composer);
+            $composer->getInstallationManager()->addInstaller($installer);
+        }
+    }
+
+## Event Handler
+
+Furthermore plugins may implement the
+[`Composer\EventDispatcher\EventSubscriberInterface`][6] in order to have its
+event handlers automatically registered with the `EventDispatcher` when the
+plugin is loaded.
+
+The events available for plugins are:
+
+* **COMMAND**, is called at the beginning of all commands that load plugins.
+  It provides you with access to the input and output objects of the program.
+* **PRE_FILE_DOWNLOAD**, is triggered before files are downloaded and allows
+  you to manipulate the `RemoteFilesystem` object prior to downloading files
+  based on the URL to be downloaded.
+
+Example:
+
+    namespace Naderman\Composer\AWS;
+
+    use Composer\Composer;
+    use Composer\EventDispatcher\EventSubscriberInterface;
+    use Composer\IO\IOInterface;
+    use Composer\Plugin\PluginInterface;
+    use Composer\Plugin\PluginEvents;
+    use Composer\Plugin\PreFileDownloadEvent;
+
+    class AwsPlugin implements PluginInterface, EventSubscriberInterface
+    {
+        protected $composer;
+        protected $io;
+
+        public function activate(Composer $composer, IOInterface $io)
+        {
+            $this->composer = $composer;
+            $this->io = $io;
+        }
+
+        public static function getSubscribedEvents()
+        {
+            return array(
+                PluginEvents::PRE_FILE_DOWNLOAD => array(
+                    array('onPreFileDownload', 0)
+                ),
+            );
+        }
+
+        public function onPreFileDownload(PreFileDownloadEvent $event)
+        {
+            $protocol = parse_url($event->getProcessedUrl(), PHP_URL_SCHEME);
+
+            if ($protocol === 's3') {
+                $awsClient = new AwsClient($this->io, $this->composer->getConfig());
+                $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient);
+                $event->setRemoteFilesystem($s3RemoteFilesystem);
+            }
+        }
+    }
+
+## Using Plugins
+
+Plugin packages are automatically loaded as soon as they are installed and will
+be loaded when composer starts up if they are found in the current project's
+list of installed packages. Additionally all plugin packages installed in the
+`COMPOSER_HOME` directory using the composer global command are loaded before
+local project plugins are loaded.
+
+> You may pass the `--no-plugins` option to composer commands to disable all
+> installed commands. This may be particularly helpful if any of the plugins
+> causes errors and you wish to update or uninstall it.
+
+[1]: ../04-schema.md#type
+[2]: ../04-schema.md#extra
+[3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php
+[4]: https://github.com/composer/composer/blob/master/src/Composer/Composer.php
+[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

+ 2 - 2
res/composer-schema.json

@@ -9,7 +9,7 @@
             "required": true
         },
         "type": {
-            "description": "Package type, either 'library' for common packages, 'composer-installer' for custom installers, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.",
+            "description": "Package type, either 'library' for common packages, 'composer-plugin' for plugins, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.",
             "type": "string"
         },
         "target-dir": {
@@ -180,7 +180,7 @@
         },
         "extra": {
             "type": ["object", "array"],
-            "description": "Arbitrary extra data that can be used by custom installers, for example, package of type composer-installer must have a 'class' key defining the installer class name.",
+            "description": "Arbitrary extra data that can be used by plugins, for example, package of type composer-plugin may have a 'class' key defining an installer class name.",
             "additionalProperties": true
         },
         "autoload": {

+ 3 - 3
src/Composer/Autoload/AutoloadGenerator.php

@@ -13,12 +13,12 @@
 namespace Composer\Autoload;
 
 use Composer\Config;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Installer\InstallationManager;
 use Composer\Package\AliasPackage;
 use Composer\Package\PackageInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Util\Filesystem;
-use Composer\Script\EventDispatcher;
 use Composer\Script\ScriptEvents;
 
 /**
@@ -39,7 +39,7 @@ class AutoloadGenerator
 
     public function dump(Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsr0Packages = false, $suffix = '')
     {
-        $this->eventDispatcher->dispatch(ScriptEvents::PRE_AUTOLOAD_DUMP);
+        $this->eventDispatcher->dispatchScript(ScriptEvents::PRE_AUTOLOAD_DUMP);
 
         $filesystem = new Filesystem();
         $filesystem->ensureDirectoryExists($config->get('vendor-dir'));
@@ -191,7 +191,7 @@ EOF;
         fclose($targetLoader);
         unset($sourceLoader, $targetLoader);
 
-        $this->eventDispatcher->dispatch(ScriptEvents::POST_AUTOLOAD_DUMP);
+        $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP);
     }
 
     public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages)

+ 3 - 2
src/Composer/Command/Command.php

@@ -38,16 +38,17 @@ abstract class Command extends BaseCommand
 
     /**
      * @param  bool              $required
+     * @param  bool              $disablePlugins
      * @throws \RuntimeException
      * @return Composer
      */
-    public function getComposer($required = true)
+    public function getComposer($required = true, $disablePlugins = false)
     {
         if (null === $this->composer) {
             $application = $this->getApplication();
             if ($application instanceof Application) {
                 /* @var $application    Application */
-                $this->composer = $application->getComposer($required);
+                $this->composer = $application->getComposer($required, $disablePlugins);
             } elseif ($required) {
                 throw new \RuntimeException(
                     'Could not create a Composer\Composer instance, you must inject '.

+ 17 - 10
src/Composer/Command/CreateProjectCommand.php

@@ -44,6 +44,7 @@ use Composer\Package\Version\VersionParser;
  * @author Benjamin Eberlei <kontakt@beberlei.de>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Tobias Munk <schmunk@usrbin.de>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class CreateProjectCommand extends Command
 {
@@ -62,7 +63,8 @@ class CreateProjectCommand extends Command
                 new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'Pick a different repository url to look for the package.'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
-                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Whether to disable custom installers.'),
+                new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.'),
+                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Whether to prevent execution of all defined scripts in the root package.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deletion vcs folder.'),
@@ -116,6 +118,11 @@ EOT
             $preferDist = $input->getOption('prefer-dist');
         }
 
+        if ($input->getOption('no-custom-installers')) {
+            $output->writeln('<warning>You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.</warning>');
+            $input->setOption('no-plugins', true);
+        }
+
         return $this->installProject(
             $this->getIO(),
             $config,
@@ -127,24 +134,24 @@ EOT
             $preferDist,
             !$input->getOption('no-dev'),
             $input->getOption('repository-url'),
-            $input->getOption('no-custom-installers'),
+            $input->getOption('no-plugins'),
             $input->getOption('no-scripts'),
             $input->getOption('keep-vcs'),
             $input->getOption('no-progress')
         );
     }
 
-    public function installProject(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disableCustomInstallers = false, $noScripts = false, $keepVcs = false, $noProgress = false)
+    public function installProject(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false)
     {
         $oldCwd = getcwd();
 
         if ($packageName !== null) {
-            $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disableCustomInstallers, $noScripts, $keepVcs, $noProgress);
+            $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disablePlugins, $noScripts, $keepVcs, $noProgress);
         } else {
             $installedFromVcs = false;
         }
 
-        $composer = Factory::create($io);
+        $composer = Factory::create($io, null, $disablePlugins);
 
         if ($noScripts === false) {
             // dispatch event
@@ -158,8 +165,8 @@ EOT
             ->setDevMode($installDevPackages)
             ->setRunScripts( ! $noScripts);
 
-        if ($disableCustomInstallers) {
-            $installer->disableCustomInstallers();
+        if ($disablePlugins) {
+            $installer->disablePlugins();
         }
 
         if (!$installer->run()) {
@@ -226,7 +233,7 @@ EOT
         return 0;
     }
 
-    protected function installRootPackage(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disableCustomInstallers = false, $noScripts = false, $keepVcs = false, $noProgress = false)
+    protected function installRootPackage(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false)
     {
         $stability = strtolower($stability);
         if ($stability === 'rc') {
@@ -285,8 +292,8 @@ EOT
 
         $io->write('<info>Installing ' . $package->getName() . ' (' . VersionParser::formatVersion($package, false) . ')</info>');
 
-        if ($disableCustomInstallers) {
-            $io->write('<info>Custom installers have been disabled.</info>');
+        if ($disablePlugins) {
+            $io->write('<info>Plugins have been disabled.</info>');
         }
 
         if (0 === strpos($package->getPrettyVersion(), 'dev-') && in_array($package->getSourceType(), array('git', 'hg'))) {

+ 8 - 1
src/Composer/Command/DependsCommand.php

@@ -13,6 +13,8 @@
 namespace Composer\Command;
 
 use Composer\DependencyResolver\Pool;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -50,7 +52,12 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $repo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
+        $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'depends', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
+        $repo = $composer->getRepositoryManager()->getLocalRepository();
         $needle = $input->getArgument('package');
 
         $pool = new Pool();

+ 5 - 0
src/Composer/Command/DiagnoseCommand.php

@@ -15,6 +15,8 @@ namespace Composer\Command;
 use Composer\Composer;
 use Composer\Factory;
 use Composer\Downloader\TransportException;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Composer\Util\ConfigValidator;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\StreamContextFactory;
@@ -64,6 +66,9 @@ EOT
 
         $composer = $this->getComposer(false);
         if ($composer) {
+            $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'diagnose', $input, $output);
+            $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
             $output->write('Checking composer.json: ');
             $this->outputResult($output, $this->checkComposerSchema());
         }

+ 6 - 0
src/Composer/Command/DumpAutoloadCommand.php

@@ -12,6 +12,8 @@
 
 namespace Composer\Command;
 
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -42,6 +44,10 @@ EOT
         $output->writeln('<info>Generating autoload files</info>');
 
         $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'dump-autoload', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $installationManager = $composer->getInstallationManager();
         $localRepo = $composer->getRepositoryManager()->getLocalRepository();
         $package = $composer->getPackage();

+ 17 - 4
src/Composer/Command/InstallCommand.php

@@ -13,6 +13,8 @@
 namespace Composer\Command;
 
 use Composer\Installer;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -21,6 +23,7 @@ use Symfony\Component\Console\Output\OutputInterface;
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class InstallCommand extends Command
 {
@@ -35,7 +38,8 @@ class InstallCommand extends Command
                 new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
-                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'),
+                new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
+                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
@@ -56,9 +60,18 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $composer = $this->getComposer();
+        if ($input->getOption('no-custom-installers')) {
+            $output->writeln('<warning>You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.</warning>');
+            $input->setOption('no-plugins', true);
+        }
+
+        $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
 
         $preferSource = false;
@@ -90,8 +103,8 @@ EOT
             ->setOptimizeAutoloader($input->getOption('optimize-autoloader'))
         ;
 
-        if ($input->getOption('no-custom-installers')) {
-            $install->disableCustomInstallers();
+        if ($input->getOption('no-plugins')) {
+            $install->disablePlugins();
         }
 
         return $install->run() ? 0 : 1;

+ 6 - 0
src/Composer/Command/LicensesCommand.php

@@ -15,6 +15,8 @@ namespace Composer\Command;
 use Composer\Package\PackageInterface;
 use Composer\Json\JsonFile;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Helper\TableHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
@@ -46,6 +48,10 @@ EOT
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'licenses', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $root = $composer->getPackage();
         $repo = $composer->getRepositoryManager()->getLocalRepository();
 

+ 6 - 0
src/Composer/Command/RequireCommand.php

@@ -21,6 +21,8 @@ use Composer\Installer;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 
 /**
  * @author Jérémy Romey <jeremy@free-agent.fr>
@@ -106,6 +108,10 @@ EOT
         $composer = $this->getComposer();
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
 
         $install

+ 7 - 0
src/Composer/Command/SearchCommand.php

@@ -20,6 +20,8 @@ use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryInterface;
 use Composer\Factory;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 
 /**
  * @author Robert Schönthal <seroscho@googlemail.com>
@@ -65,6 +67,11 @@ EOT
             $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
         }
 
+        if ($composer) {
+            $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'search', $input, $output);
+            $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+        }
+
         $onlyName = $input->getOption('only-name');
 
         $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT;

+ 7 - 0
src/Composer/Command/ShowCommand.php

@@ -18,6 +18,8 @@ use Composer\DependencyResolver\DefaultPolicy;
 use Composer\Factory;
 use Composer\Package\CompletePackageInterface;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -94,6 +96,11 @@ EOT
             $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
         }
 
+        if ($composer) {
+            $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'show', $input, $output);
+            $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+        }
+
         // show single package or single version
         if ($input->getArgument('package') || !empty($package)) {
             $versions = array();

+ 6 - 0
src/Composer/Command/StatusCommand.php

@@ -17,6 +17,8 @@ use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Downloader\ChangeReportInterface;
 use Composer\Downloader\VcsDownloader;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Composer\Script\ScriptEvents;
 
 /**
@@ -46,6 +48,10 @@ EOT
     {
         // init repos
         $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'status', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
 
         $dm = $composer->getDownloadManager();

+ 17 - 4
src/Composer/Command/UpdateCommand.php

@@ -13,6 +13,8 @@
 namespace Composer\Command;
 
 use Composer\Installer;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputArgument;
@@ -20,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class UpdateCommand extends Command
 {
@@ -36,7 +39,8 @@ class UpdateCommand extends Command
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'),
                 new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
                 new InputOption('lock', null, InputOption::VALUE_NONE, 'Only updates the lock file hash to suppress warning about the lock file being out of date.'),
-                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'),
+                new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
+                new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'),
                 new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
@@ -60,9 +64,18 @@ EOT
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $composer = $this->getComposer();
+        if ($input->getOption('no-custom-installers')) {
+            $output->writeln('<warning>You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.</warning>');
+            $input->setOption('no-plugins', true);
+        }
+
+        $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
 
         $preferSource = false;
@@ -96,8 +109,8 @@ EOT
             ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages'))
         ;
 
-        if ($input->getOption('no-custom-installers')) {
-            $install->disableCustomInstallers();
+        if ($input->getOption('no-plugins')) {
+            $install->disablePlugins();
         }
 
         return $install->run() ? 0 : 1;

+ 27 - 4
src/Composer/Composer.php

@@ -16,13 +16,15 @@ use Composer\Package\RootPackageInterface;
 use Composer\Package\Locker;
 use Composer\Repository\RepositoryManager;
 use Composer\Installer\InstallationManager;
+use Composer\Plugin\PluginManager;
 use Composer\Downloader\DownloadManager;
-use Composer\Script\EventDispatcher;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class Composer
 {
@@ -53,13 +55,18 @@ class Composer
      */
     private $installationManager;
 
+    /**
+     * @var Plugin\PluginManager
+     */
+    private $pluginManager;
+
     /**
      * @var Config
      */
     private $config;
 
     /**
-     * @var Script\EventDispatcher
+     * @var EventDispatcher\EventDispatcher
      */
     private $eventDispatcher;
 
@@ -166,7 +173,23 @@ class Composer
     }
 
     /**
-     * @param Script\EventDispatcher $eventDispatcher
+     * @param Plugin\PluginManager $manager
+     */
+    public function setPluginManager(PluginManager $manager)
+    {
+        $this->pluginManager = $manager;
+    }
+
+    /**
+     * @return Plugin\PluginManager
+     */
+    public function getPluginManager()
+    {
+        return $this->pluginManager;
+    }
+
+    /**
+     * @param EventDispatcher\EventDispatcher $eventDispatcher
      */
     public function setEventDispatcher(EventDispatcher $eventDispatcher)
     {
@@ -174,7 +197,7 @@ class Composer
     }
 
     /**
-     * @return Script\EventDispatcher
+     * @return EventDispatcher\EventDispatcher
      */
     public function getEventDispatcher()
     {

+ 3 - 2
src/Composer/Console/Application.php

@@ -165,14 +165,15 @@ class Application extends BaseApplication
 
     /**
      * @param  bool                    $required
+     * @param  bool                    $disablePlugins
      * @throws JsonValidationException
      * @return \Composer\Composer
      */
-    public function getComposer($required = true)
+    public function getComposer($required = true, $disablePlugins = false)
     {
         if (null === $this->composer) {
             try {
-                $this->composer = Factory::create($this->io);
+                $this->composer = Factory::create($this->io, null, $disablePlugins);
             } catch (\InvalidArgumentException $e) {
                 if ($required) {
                     $this->io->write($e->getMessage());

+ 20 - 5
src/Composer/Downloader/FileDownloader.php

@@ -17,6 +17,9 @@ use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\PluginEvents;
+use Composer\Plugin\PreFileDownloadEvent;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
 use Composer\Util\GitHub;
 use Composer\Util\RemoteFilesystem;
@@ -27,6 +30,7 @@ use Composer\Util\RemoteFilesystem;
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class FileDownloader implements DownloaderInterface
 {
@@ -43,14 +47,16 @@ class FileDownloader implements DownloaderInterface
      *
      * @param IOInterface      $io         The IO instance
      * @param Config           $config     The config
+     * @param EventDispatcher  $eventDispatcher The event dispatcher
      * @param Cache            $cache      Optional cache instance
      * @param RemoteFilesystem $rfs        The remote filesystem
      * @param Filesystem       $filesystem The filesystem
      */
-    public function __construct(IOInterface $io, Config $config, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
+    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
     {
         $this->io = $io;
         $this->config = $config;
+        $this->eventDispatcher = $eventDispatcher;
         $this->rfs = $rfs ?: new RemoteFilesystem($io);
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->cache = $cache;
@@ -89,6 +95,12 @@ class FileDownloader implements DownloaderInterface
         $processedUrl = $this->processUrl($package, $url);
         $hostname = parse_url($processedUrl, PHP_URL_HOST);
 
+        $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl);
+        if ($this->eventDispatcher) {
+            $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
+        }
+        $rfs = $preFileDownloadEvent->getRemoteFilesystem();
+
         if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) {
             $hostname = 'github.com';
         }
@@ -104,7 +116,7 @@ class FileDownloader implements DownloaderInterface
                     $retries = 3;
                     while ($retries--) {
                         try {
-                            $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
+                            $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
                             break;
                         } catch (TransportException $e) {
                             // if we got an http response with a proper code, then requesting again will probably not help, abort
@@ -125,15 +137,18 @@ class FileDownloader implements DownloaderInterface
                     $this->io->write('    Loading from cache');
                 }
             } catch (TransportException $e) {
-                if (in_array($e->getCode(), array(404, 403)) && 'github.com' === $hostname && !$this->io->hasAuthentication($hostname)) {
+                if (!in_array($e->getCode(), array(404, 403, 412))) {
+                    throw $e;
+                }
+                if ('github.com' === $hostname && !$this->io->hasAuthentication($hostname)) {
                     $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode() === 404 ? 'to access private repos' : 'to go over the API rate limit');
-                    $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs);
+                    $gitHubUtil = new GitHub($this->io, $this->config, null, $rfs);
                     if (!$gitHubUtil->authorizeOAuth($hostname)
                         && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
                     ) {
                         throw $e;
                     }
-                    $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
+                    $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
                 } else {
                     throw $e;
                 }

+ 3 - 2
src/Composer/Downloader/ZipDownloader.php

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 use Composer\Config;
 use Composer\Cache;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use ZipArchive;
@@ -25,10 +26,10 @@ class ZipDownloader extends ArchiveDownloader
 {
     protected $process;
 
-    public function __construct(IOInterface $io, Config $config, Cache $cache = null, ProcessExecutor $process = null)
+    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $cache);
+        parent::__construct($io, $config, $eventDispatcher, $cache);
     }
 
     protected function extract($file, $path)

+ 69 - 0
src/Composer/EventDispatcher/Event.php

@@ -0,0 +1,69 @@
+<?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\EventDispatcher;
+
+/**
+ * The base event class
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class Event
+{
+    /**
+     * @var string This event's name
+     */
+    protected $name;
+
+    /**
+     * @var boolean Whether the event should not be passed to more listeners
+     */
+    private $propagationStopped = false;
+
+    /**
+     * Constructor.
+     *
+     * @param string      $name     The event name
+     */
+    public function __construct($name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Returns the event's name.
+     *
+     * @return string The event name
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Checks if stopPropagation has been called
+     *
+     * @return boolean Whether propagation has been stopped
+     */
+    public function isPropagationStopped()
+    {
+        return $this->propagationStopped;
+    }
+
+    /**
+     * Prevents the event from being passed to further listeners
+     */
+    public function stopPropagation()
+    {
+        $this->propagationStopped = true;
+    }
+}

+ 87 - 5
src/Composer/Script/EventDispatcher.php → src/Composer/EventDispatcher/EventDispatcher.php

@@ -10,11 +10,14 @@
  * file that was distributed with this source code.
  */
 
-namespace Composer\Script;
+namespace Composer\EventDispatcher;
 
 use Composer\IO\IOInterface;
 use Composer\Composer;
 use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\Script;
+use Composer\Script\CommandEvent;
+use Composer\Script\PackageEvent;
 use Composer\Util\ProcessExecutor;
 
 /**
@@ -28,6 +31,7 @@ use Composer\Util\ProcessExecutor;
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class EventDispatcher
 {
@@ -50,16 +54,31 @@ class EventDispatcher
         $this->process = $process ?: new ProcessExecutor($io);
     }
 
+    /**
+     * Dispatch an event
+     *
+     * @param string $eventName An event name
+     * @param Event  $event
+     */
+    public function dispatch($eventName, Event $event = null)
+    {
+        if (null == $event) {
+            $event = new Event($eventName);
+        }
+
+        $this->doDispatch($event);
+    }
+
     /**
      * Dispatch a script event.
      *
      * @param string $eventName The constant in ScriptEvents
      * @param Event  $event
      */
-    public function dispatch($eventName, Event $event = null)
+    public function dispatchScript($eventName, Script\Event $event = null)
     {
         if (null == $event) {
-            $event = new Event($eventName, $this->composer, $this->io);
+            $event = new Script\Event($eventName, $this->composer, $this->io);
         }
 
         $this->doDispatch($event);
@@ -100,7 +119,9 @@ class EventDispatcher
         $listeners = $this->getListeners($event);
 
         foreach ($listeners as $callable) {
-            if ($this->isPhpScript($callable)) {
+            if (!is_string($callable) && is_callable($callable)) {
+                call_user_func($callable, $event);
+            } elseif ($this->isPhpScript($callable)) {
                 $className = substr($callable, 0, strpos($callable, '::'));
                 $methodName = substr($callable, strpos($callable, '::') + 2);
 
@@ -127,6 +148,10 @@ class EventDispatcher
                     throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
                 }
             }
+
+            if ($event->isPropagationStopped()) {
+                break;
+            }
         }
     }
 
@@ -141,10 +166,67 @@ class EventDispatcher
     }
 
     /**
+     * Add a listener for a particular event
+     *
+     * @param string   $eventName The event name - typically a constant
+     * @param Callable $listener  A callable expecting an event argument
+     * @param integer  $priority  A higher value represents a higher priority
+     */
+    protected function addListener($eventName, $listener, $priority = 0)
+    {
+        $this->listeners[$eventName][$priority][] = $listener;
+    }
+
+    /**
+     * Adds object methods as listeners for the events in getSubscribedEvents
+     *
+     * @see EventSubscriberInterface
+     *
+     * @param EventSubscriberInterface $subscriber
+     */
+    public function addSubscriber(EventSubscriberInterface $subscriber)
+    {
+        foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
+            if (is_string($params)) {
+                $this->addListener($eventName, array($subscriber, $params));
+            } elseif (is_string($params[0])) {
+                $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
+            } else {
+                foreach ($params as $listener) {
+                    $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
+                }
+            }
+        }
+    }
+
+    /**
+     * Retrieves all listeners for a given event
+     *
+     * @param Event $event
+     * @return array All listeners: callables and scripts
+     */
+    protected function getListeners(Event $event)
+    {
+        $scriptListeners = $this->getScriptListeners($event);
+
+        if (!isset($this->listeners[$event->getName()][0])) {
+            $this->listeners[$event->getName()][0] = array();
+        }
+        krsort($this->listeners[$event->getName()]);
+
+        $listeners = $this->listeners;
+        $listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners);
+
+        return call_user_func_array('array_merge', $listeners[$event->getName()]);
+    }
+
+    /**
+     * Finds all listeners defined as scripts in the package
+     *
      * @param  Event $event Event object
      * @return array Listeners
      */
-    protected function getListeners(Event $event)
+    protected function getScriptListeners(Event $event)
     {
         $package = $this->composer->getPackage();
         $scripts = $package->getScripts();

+ 48 - 0
src/Composer/EventDispatcher/EventSubscriberInterface.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\EventDispatcher;
+
+/**
+ * An EventSubscriber knows which events it is interested in.
+ *
+ * If an EventSubscriber is added to an EventDispatcher, the manager invokes
+ * {@link getSubscribedEvents} and registers the subscriber as a listener for all
+ * returned events.
+ *
+ * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
+ * @author Jonathan Wage <jonwage@gmail.com>
+ * @author Roman Borschel <roman@code-factory.org>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface EventSubscriberInterface
+{
+    /**
+     * Returns an array of event names this subscriber wants to listen to.
+     *
+     * The array keys are event names and the value can be:
+     *
+     * * The method name to call (priority defaults to 0)
+     * * An array composed of the method name to call and the priority
+     * * An array of arrays composed of the method names to call and respective
+     *   priorities, or 0 if unset
+     *
+     * For instance:
+     *
+     * * array('eventName' => 'methodName')
+     * * array('eventName' => array('methodName', $priority))
+     * * array('eventName' => array(array('methodName1', $priority), array('methodName2'))
+     *
+     * @return array The event names to listen to
+     */
+    public static function getSubscribedEvents();
+}

+ 54 - 14
src/Composer/Factory.php

@@ -18,10 +18,11 @@ use Composer\IO\IOInterface;
 use Composer\Package\Archiver;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\RepositoryManager;
+use Composer\Repository\RepositoryInterface;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
-use Composer\Script\EventDispatcher;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Package\Version\VersionParser;
 
@@ -31,6 +32,7 @@ use Composer\Package\Version\VersionParser;
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Igor Wiedler <igor@wiedler.ch>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class Factory
 {
@@ -176,11 +178,12 @@ class Factory
      * @param IOInterface       $io          IO instance
      * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will
      *                                       read from the default filename
+     * @param bool        $disablePlugins Whether plugins should not be loaded
      * @throws \InvalidArgumentException
      * @throws \UnexpectedValueException
      * @return Composer
      */
-    public function createComposer(IOInterface $io, $localConfig = null)
+    public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false)
     {
         // load Composer configuration
         if (null === $localConfig) {
@@ -227,9 +230,6 @@ class Factory
         $loader  = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io));
         $package = $loader->load($localConfig);
 
-        // initialize download manager
-        $dm = $this->createDownloadManager($io, $config);
-
         // initialize installation manager
         $im = $this->createInstallationManager();
 
@@ -238,20 +238,32 @@ class Factory
         $composer->setConfig($config);
         $composer->setPackage($package);
         $composer->setRepositoryManager($rm);
-        $composer->setDownloadManager($dm);
         $composer->setInstallationManager($im);
 
         // initialize event dispatcher
         $dispatcher = new EventDispatcher($composer, $io);
+
+        // initialize download manager
+        $dm = $this->createDownloadManager($io, $config, $dispatcher);
+
+        $composer->setDownloadManager($dm);
         $composer->setEventDispatcher($dispatcher);
 
         // initialize autoload generator
         $generator = new AutoloadGenerator($dispatcher);
         $composer->setAutoloadGenerator($generator);
 
+        $globalRepository = $this->createGlobalRepository($config, $vendorDir);
+        $pm = $this->createPluginManager($composer, $io, $globalRepository);
+        $composer->setPluginManager($pm);
+
         // add installers to the manager
         $this->createDefaultInstallers($im, $composer, $io);
 
+        if (!$disablePlugins) {
+            $pm->loadInstalledPlugins();
+        }
+
         // purge packages if they have been deleted on the filesystem
         $this->purgePackages($rm, $im);
 
@@ -296,12 +308,31 @@ class Factory
         $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json')));
     }
 
+     /**
+     * @param Config $config
+     * @param string $vendorDir
+     */
+    protected function createGlobalRepository(Config $config, $vendorDir)
+    {
+        if ($config->get('home') == $vendorDir) {
+            return null;
+        }
+
+        $path = $config->get('home').'/vendor/composer/installed.json';
+        if (!file_exists($path)) {
+            return null;
+        }
+
+        return new Repository\InstalledFilesystemRepository(new JsonFile($path));
+    }
+
     /**
      * @param  IO\IOInterface             $io
      * @param  Config                     $config
+     * @param  EventDispatcher            $eventDispatcher
      * @return Downloader\DownloadManager
      */
-    public function createDownloadManager(IOInterface $io, Config $config)
+    public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null)
     {
         $cache = null;
         if ($config->get('cache-files-ttl') > 0) {
@@ -325,10 +356,10 @@ class Factory
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config));
-        $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $cache));
-        $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $cache));
-        $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $cache));
-        $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $cache));
+        $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache));
+        $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache));
+        $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache));
+        $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache));
 
         return $dm;
     }
@@ -353,6 +384,14 @@ class Factory
         return $am;
     }
 
+    /**
+     * @return Plugin\PluginManager
+     */
+    protected function createPluginManager(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null)
+    {
+        return new Plugin\PluginManager($composer, $io, $globalRepository);
+    }
+
     /**
      * @return Installer\InstallationManager
      */
@@ -370,7 +409,7 @@ class Factory
     {
         $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null));
         $im->addInstaller(new Installer\PearInstaller($io, $composer, 'pear-library'));
-        $im->addInstaller(new Installer\InstallerInstaller($io, $composer));
+        $im->addInstaller(new Installer\PluginInstaller($io, $composer));
         $im->addInstaller(new Installer\MetapackageInstaller($io));
     }
 
@@ -392,12 +431,13 @@ class Factory
      * @param IOInterface $io     IO instance
      * @param mixed       $config either a configuration array or a filename to read from, if null it will read from
      *                             the default filename
+     * @param bool        $disablePlugins Whether plugins should not be loaded
      * @return Composer
      */
-    public static function create(IOInterface $io, $config = null)
+    public static function create(IOInterface $io, $config = null, $disablePlugins = false)
     {
         $factory = new static();
 
-        return $factory->createComposer($io, $config);
+        return $factory->createComposer($io, $config, $disablePlugins);
     }
 }

+ 9 - 8
src/Composer/Installer.php

@@ -24,6 +24,7 @@ use Composer\DependencyResolver\Rule;
 use Composer\DependencyResolver\Solver;
 use Composer\DependencyResolver\SolverProblemsException;
 use Composer\Downloader\DownloadManager;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Installer\InstallationManager;
 use Composer\Config;
 use Composer\Installer\NoopInstaller;
@@ -41,13 +42,13 @@ use Composer\Repository\InstalledFilesystemRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositoryManager;
-use Composer\Script\EventDispatcher;
 use Composer\Script\ScriptEvents;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Beau Simensen <beau@dflydev.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class Installer
 {
@@ -461,7 +462,7 @@ class Installer
             $this->io->write('Nothing to install or update');
         }
 
-        $operations = $this->moveCustomInstallersToFront($operations);
+        $operations = $this->movePluginsToFront($operations);
 
         foreach ($operations as $operation) {
             // collect suggestions
@@ -540,7 +541,7 @@ class Installer
 
 
     /**
-     * Workaround: if your packages depend on custom installers, we must be sure
+     * Workaround: if your packages depend on plugins, we must be sure
      * that those are installed / updated first; else it would lead to packages
      * being installed multiple times in different folders, when running Composer
      * twice.
@@ -552,7 +553,7 @@ class Installer
      * @param OperationInterface[] $operations
      * @return OperationInterface[] reordered operation list
      */
-    private function moveCustomInstallersToFront(array $operations)
+    private function movePluginsToFront(array $operations)
     {
         $installerOps = array();
         foreach ($operations as $idx => $op) {
@@ -564,7 +565,7 @@ class Installer
                 continue;
             }
 
-            if ($package->getRequires() === array() && $package->getType() === 'composer-installer') {
+            if ($package->getRequires() === array() && ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer')) {
                 $installerOps[] = $op;
                 unset($operations[$idx]);
             }
@@ -1055,7 +1056,7 @@ class Installer
     }
 
     /**
-     * Disables custom installers.
+     * Disables plugins.
      *
      * Call this if you want to ensure that third-party code never gets
      * executed. The default is to automatically install, and execute
@@ -1063,9 +1064,9 @@ class Installer
      *
      * @return Installer
      */
-    public function disableCustomInstallers()
+    public function disablePlugins()
     {
-        $this->installationManager->disableCustomInstallers();
+        $this->installationManager->disablePlugins();
 
         return $this;
     }

+ 6 - 4
src/Composer/Installer/InstallationManager.php

@@ -14,6 +14,7 @@ namespace Composer\Installer;
 
 use Composer\Package\PackageInterface;
 use Composer\Package\AliasPackage;
+use Composer\Plugin\PluginInstaller;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
@@ -29,6 +30,7 @@ use Composer\Util\StreamContextFactory;
  *
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class InstallationManager
 {
@@ -66,16 +68,16 @@ class InstallationManager
     }
 
     /**
-     * Disables custom installers.
+     * Disables plugins.
      *
-     * We prevent any custom installers from being instantiated by simply
+     * We prevent any plugins from being instantiated by simply
      * deactivating the installer for them. This ensure that no third-party
      * code is ever executed.
      */
-    public function disableCustomInstallers()
+    public function disablePlugins()
     {
         foreach ($this->installers as $i => $installer) {
-            if (!$installer instanceof InstallerInstaller) {
+            if (!$installer instanceof PluginInstaller) {
                 continue;
             }
 

+ 0 - 104
src/Composer/Installer/InstallerInstaller.php

@@ -1,104 +0,0 @@
-<?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\Installer;
-
-use Composer\Composer;
-use Composer\Package\Package;
-use Composer\IO\IOInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-use Composer\Package\PackageInterface;
-
-/**
- * Installer installation manager.
- *
- * @author Jordi Boggiano <j.boggiano@seld.be>
- */
-class InstallerInstaller extends LibraryInstaller
-{
-    private $installationManager;
-    private static $classCounter = 0;
-
-    /**
-     * Initializes Installer installer.
-     *
-     * @param IOInterface $io
-     * @param Composer    $composer
-     * @param string      $type
-     */
-    public function __construct(IOInterface $io, Composer $composer, $type = 'library')
-    {
-        parent::__construct($io, $composer, 'composer-installer');
-        $this->installationManager = $composer->getInstallationManager();
-
-        $repo = $composer->getRepositoryManager()->getLocalRepository();
-        foreach ($repo->getPackages() as $package) {
-            if ('composer-installer' === $package->getType()) {
-                $this->registerInstaller($package);
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
-    {
-        $extra = $package->getExtra();
-        if (empty($extra['class'])) {
-            throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-installer packages should have a class defined in their extra key to be usable.');
-        }
-
-        parent::install($repo, $package);
-        $this->registerInstaller($package);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
-    {
-        $extra = $target->getExtra();
-        if (empty($extra['class'])) {
-            throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-installer packages should have a class defined in their extra key to be usable.');
-        }
-
-        parent::update($repo, $initial, $target);
-        $this->registerInstaller($target);
-    }
-
-    private function registerInstaller(PackageInterface $package)
-    {
-        $downloadPath = $this->getInstallPath($package);
-
-        $extra = $package->getExtra();
-        $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
-
-        $generator = $this->composer->getAutoloadGenerator();
-        $map = $generator->parseAutoloads(array(array($package, $downloadPath)), new Package('dummy', '1.0.0.0', '1.0.0'));
-        $classLoader = $generator->createLoader($map);
-        $classLoader->register();
-
-        foreach ($classes as $class) {
-            if (class_exists($class, false)) {
-                $code = file_get_contents($classLoader->findFile($class));
-                $code = preg_replace('{^(\s*)class\s+(\S+)}mi', '$1class $2_composer_tmp'.self::$classCounter, $code);
-                eval('?>'.$code);
-                $class .= '_composer_tmp'.self::$classCounter;
-                self::$classCounter++;
-            }
-
-            $installer = new $class($this->io, $this->composer);
-            $this->installationManager->addInstaller($installer);
-        }
-    }
-}

+ 81 - 0
src/Composer/Installer/PluginInstaller.php

@@ -0,0 +1,81 @@
+<?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\Installer;
+
+use Composer\Composer;
+use Composer\Package\Package;
+use Composer\IO\IOInterface;
+use Composer\Repository\InstalledRepositoryInterface;
+use Composer\Package\PackageInterface;
+
+/**
+ * Installer for plugin packages
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class PluginInstaller extends LibraryInstaller
+{
+    private $installationManager;
+    private static $classCounter = 0;
+
+    /**
+     * Initializes Plugin installer.
+     *
+     * @param IOInterface $io
+     * @param Composer    $composer
+     * @param string      $type
+     */
+    public function __construct(IOInterface $io, Composer $composer, $type = 'library')
+    {
+        parent::__construct($io, $composer, 'composer-plugin');
+        $this->installationManager = $composer->getInstallationManager();
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function supports($packageType)
+    {
+        return $packageType === 'composer-plugin' || $packageType === 'composer-installer';
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
+    {
+        $extra = $package->getExtra();
+        if (empty($extra['class'])) {
+            throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
+        }
+
+        parent::install($repo, $package);
+        $this->composer->getPluginManager()->registerPackage($package);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
+    {
+        $extra = $target->getExtra();
+        if (empty($extra['class'])) {
+            throw new \UnexpectedValueException('Error while installing '.$target->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
+        }
+
+        parent::update($repo, $initial, $target);
+        $this->composer->getPluginManager()->registerPackage($target);
+    }
+}

+ 87 - 0
src/Composer/Plugin/CommandEvent.php

@@ -0,0 +1,87 @@
+<?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;
+
+use Composer\IO\IOInterface;
+use Composer\EventDispatcher\Event;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * An event for all commands.
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class CommandEvent extends Event
+{
+    /**
+     * @var string
+     */
+    private $commandName;
+
+    /**
+     * @var InputInterface
+     */
+    private $input;
+
+    /**
+     * @var OutputInterface
+     */
+    private $output;
+
+    /**
+     * Constructor.
+     *
+     * @param string          $name        The event name
+     * @param string          $commandName The command name
+     * @param InputInterface  $input
+     * @param OutputInterface $output
+     */
+    public function __construct($name, $commandName, $input, $output)
+    {
+        parent::__construct($name);
+        $this->commandName = $commandName;
+        $this->input = $input;
+        $this->output = $output;
+    }
+
+    /**
+     * Returns the command input interface
+     *
+     * @return InputInterface
+     */
+    public function getInput()
+    {
+        return $this->input;
+    }
+
+    /**
+     * Retrieves the command output interface
+     *
+     * @return OutputInterface
+     */
+    public function getOutput()
+    {
+        return $this->output;
+    }
+
+    /**
+     * Retrieves the name of the command being run
+     *
+     * @return string
+     */
+    public function getCommandName()
+    {
+        return $this->commandName;
+    }
+}

+ 41 - 0
src/Composer/Plugin/PluginEvents.php

@@ -0,0 +1,41 @@
+<?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;
+
+/**
+ * The Plugin Events.
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class PluginEvents
+{
+    /**
+     * The COMMAND event occurs as a command begins
+     *
+     * The event listener method receives a
+     * Composer\Plugin\CommandEvent instance.
+     *
+     * @var string
+     */
+    const COMMAND = 'command';
+
+    /**
+     * The PRE_FILE_DOWNLOAD event occurs before downloading a file
+     *
+     * The event listener method receives a
+     * Composer\Plugin\PreFileDownloadEvent instance.
+     *
+     * @var string
+     */
+    const PRE_FILE_DOWNLOAD = 'pre-file-download';
+}

+ 39 - 0
src/Composer/Plugin/PluginInterface.php

@@ -0,0 +1,39 @@
+<?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;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+
+/**
+ * Plugin interface
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+interface PluginInterface
+{
+    /**
+     * Version number of the fake composer-plugin-api package
+     *
+     * @var string
+     */
+    const PLUGIN_API_VERSION = '1.0.0';
+
+    /**
+     * Apply plugin modifications to composer
+     *
+     * @param Composer $composer
+     * @param IOInterface $io
+     */
+    public function activate(Composer $composer, IOInterface $io);
+}

+ 259 - 0
src/Composer/Plugin/PluginManager.php

@@ -0,0 +1,259 @@
+<?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;
+
+use Composer\Composer;
+use Composer\EventDispatcher\EventSubscriberInterface;
+use Composer\IO\IOInterface;
+use Composer\Package\Package;
+use Composer\Package\Version\VersionParser;
+use Composer\Repository\RepositoryInterface;
+use Composer\Package\PackageInterface;
+use Composer\Package\Link;
+use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\DependencyResolver\Pool;
+
+/**
+ * Plugin manager
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class PluginManager
+{
+    protected $composer;
+    protected $io;
+    protected $globalRepository;
+    protected $versionParser;
+
+    protected $plugins = array();
+
+    private static $classCounter = 0;
+
+    /**
+     * Initializes plugin manager
+     *
+     * @param Composer $composer
+     */
+    public function __construct(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null)
+    {
+        $this->composer = $composer;
+        $this->io = $io;
+        $this->globalRepository = $globalRepository;
+        $this->versionParser = new VersionParser();
+    }
+
+    /**
+     * Loads all plugins from currently installed plugin packages
+     */
+    public function loadInstalledPlugins()
+    {
+        $repo = $this->composer->getRepositoryManager()->getLocalRepository();
+
+        if ($repo) {
+            $this->loadRepository($repo);
+        }
+        if ($this->globalRepository) {
+            $this->loadRepository($this->globalRepository);
+        }
+    }
+
+    /**
+     * Adds a plugin, activates it and registers it with the event dispatcher
+     *
+     * @param PluginInterface $plugin plugin instance
+     */
+    public function addPlugin(PluginInterface $plugin)
+    {
+        $this->plugins[] =  $plugin;
+        $plugin->activate($this->composer, $this->io);
+
+        if ($plugin instanceof EventSubscriberInterface) {
+            $this->composer->getEventDispatcher()->addSubscriber($plugin);
+        }
+    }
+
+    /**
+     * Gets all currently active plugin instances
+     *
+     * @return array plugins
+     */
+    public function getPlugins()
+    {
+        return $this->plugins;
+    }
+
+    protected function loadRepository(RepositoryInterface $repo)
+    {
+        foreach ($repo->getPackages() as $package) {
+            if ('composer-plugin' === $package->getType() || 'composer-installer' === $package->getType()) {
+                $requiresComposer = null;
+                foreach ($package->getRequires() as $link) {
+                    if ($link->getTarget() == 'composer-plugin-api') {
+                        $requiresComposer = $link->getConstraint();
+                    }
+                }
+
+                if (!$requiresComposer) {
+                    throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package.");
+                }
+
+                if (!$requiresComposer->matches(new VersionConstraint('==', $this->versionParser->normalize(PluginInterface::PLUGIN_API_VERSION)))) {
+                    $this->io->write("<warning>The plugin ".$package->getName()." requires a version of composer-plugin-api that does not match your composer installation. You may need to run composer update with the '--no-plugins' option.</warning>");
+                }
+
+                $this->registerPackage($package);
+            }
+            // Backward compatability
+            if ('composer-installer' === $package->getType()) {
+                $this->registerPackage($package);
+            }
+        }
+    }
+
+    /**
+     * Recursively generates a map of package names to packages for all deps
+     *
+     * @param Pool             $pool      Package pool of installed packages
+     * @param array            $collected Current state of the map for recursion
+     * @param PackageInterface $package   The package to analyze
+     *
+     * @return array Map of package names to packages
+     */
+    protected function collectDependencies(Pool $pool, array $collected, PackageInterface $package)
+    {
+        $requires = array_merge(
+            $package->getRequires(),
+            $package->getDevRequires()
+        );
+
+        foreach ($requires as $requireLink) {
+            $requiredPackage = $this->lookupInstalledPackage($pool, $requireLink);
+            if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) {
+                $collected[$requiredPackage->getName()] = $requiredPackage;
+                $collected = $this->collectDependencies($pool, $collected, $requiredPackage);
+            }
+        }
+
+        return $collected;
+    }
+
+    /**
+     * Resolves a package link to a package in the installed pool
+     *
+     * Since dependencies are already installed this should always find one.
+     *
+     * @param Pool $pool Pool of installed packages only
+     * @param Link $link Package link to look up
+     *
+     * @return PackageInterface|null The found package
+     */
+    protected function lookupInstalledPackage(Pool $pool, Link $link)
+    {
+        $packages = $pool->whatProvides($link->getTarget(), $link->getConstraint());
+
+        return (!empty($packages)) ? $packages[0] : null;
+    }
+
+    /**
+     * Register a plugin package, activate it etc.
+     *
+     * If it's of type composer-installer it is registered as an installer
+     * instead for BC
+     *
+     * @param PackageInterface $package
+     */
+    public function registerPackage(PackageInterface $package)
+    {
+        $oldInstallerPlugin = ($package->getType() === 'composer-installer');
+
+        $extra = $package->getExtra();
+        if (empty($extra['class'])) {
+            throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
+        }
+        $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
+
+        $pool = new Pool('dev');
+        $localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
+        $pool->addRepository($localRepo);
+        if ($this->globalRepository) {
+            $pool->addRepository($this->globalRepository);
+        }
+
+        $autoloadPackages = array($package->getName() => $package);
+        $autoloadPackages = $this->collectDependencies($pool, $autoloadPackages, $package);
+
+        $generator = $this->composer->getAutoloadGenerator();
+        $autoloads = array();
+        foreach ($autoloadPackages as $autoloadPackage) {
+            $downloadPath = $this->getInstallPath($autoloadPackage, ($this->globalRepository && $this->globalRepository->hasPackage($autoloadPackage)));
+            $autoloads[] = array($autoloadPackage, $downloadPath);
+        }
+
+        $map = $generator->parseAutoloads($autoloads, new Package('dummy', '1.0.0.0', '1.0.0'));
+        $classLoader = $generator->createLoader($map);
+        $classLoader->register();
+
+        foreach ($classes as $class) {
+            if (class_exists($class, false)) {
+                $code = file_get_contents($classLoader->findFile($class));
+                $code = preg_replace('{^(\s*)class\s+(\S+)}mi', '$1class $2_composer_tmp'.self::$classCounter, $code);
+                eval('?>'.$code);
+                $class .= '_composer_tmp'.self::$classCounter;
+                self::$classCounter++;
+            }
+
+            if ($oldInstallerPlugin) {
+                $installer = new $class($this->io, $this->composer);
+                $this->composer->getInstallationManager()->addInstaller($installer);
+            } else {
+                $plugin = new $class();
+                $this->addPlugin($plugin);
+            }
+        }
+    }
+
+    /**
+     * Retrieves the path a package is installed to.
+     *
+     * @param PackageInterface $package
+     * @param bool             $global  Whether this is a global package
+     *
+     * @return string Install path
+     */
+    public function getInstallPath(PackageInterface $package, $global = false)
+    {
+        $targetDir = $package->getTargetDir();
+
+        return $this->getPackageBasePath($package, $global) . ($targetDir ? '/'.$targetDir : '');
+    }
+
+    /**
+     * Retrieves the base path a package gets installed into.
+     *
+     * Does not take targetDir into account.
+     *
+     * @param PackageInterface $package
+     * @param bool             $global  Whether this is a global package
+     *
+     * @return string Base path
+     */
+    protected function getPackageBasePath(PackageInterface $package, $global = false)
+    {
+        if ($global) {
+            $vendorDir = $this->composer->getConfig()->get('home').'/vendor';
+        } else {
+            $vendorDir = rtrim($this->composer->getConfig()->get('vendor-dir'), '/');
+        }
+        return ($vendorDir ? $vendorDir.'/' : '') . $package->getPrettyName();
+    }
+}

+ 80 - 0
src/Composer/Plugin/PreFileDownloadEvent.php

@@ -0,0 +1,80 @@
+<?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;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\EventDispatcher\Event;
+use Composer\Util\RemoteFilesystem;
+
+/**
+ * The pre file download event.
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class PreFileDownloadEvent extends Event
+{
+    /**
+     * @var RemoteFilesystem
+     */
+    private $rfs;
+
+    /**
+     * @var string
+     */
+    private $processedUrl;
+
+    /**
+     * Constructor.
+     *
+     * @param string           $name        The event name
+     * @param RemoteFilesystem $rfs
+     * @param string           $processedUrl
+     */
+    public function __construct($name, RemoteFilesystem $rfs, $processedUrl)
+    {
+        parent::__construct($name);
+        $this->rfs = $rfs;
+        $this->processedUrl = $processedUrl;
+    }
+
+    /**
+     * Returns the remote filesystem
+     *
+     * @return RemoteFilesystem
+     */
+    public function getRemoteFilesystem()
+    {
+        return $this->rfs;
+    }
+
+    /**
+     * Sets the remote filesystem
+     *
+     * @param RemoteFilesystem $rfs
+     */
+    public function setRemoteFilesystem(RemoteFilesystem $rfs)
+    {
+        $this->rfs = $rfs;
+    }
+
+    /**
+     * Retrieves the processed URL this remote filesystem will be used for
+     *
+     * @return string
+     */
+    public function getProcessedUrl()
+    {
+        return $this->processedUrl;
+    }
+}

+ 8 - 0
src/Composer/Repository/PlatformRepository.php

@@ -14,6 +14,7 @@ namespace Composer\Repository;
 
 use Composer\Package\CompletePackage;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\PluginInterface;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -28,6 +29,12 @@ class PlatformRepository extends ArrayRepository
 
         $versionParser = new VersionParser();
 
+        $prettyVersion = PluginInterface::PLUGIN_API_VERSION;
+        $version = $versionParser->normalize($prettyVersion);
+        $composerPluginApi = new CompletePackage('composer-plugin-api', $version, $prettyVersion);
+        $composerPluginApi->setDescription('The Composer Plugin API');
+        parent::addPackage($composerPluginApi);
+
         try {
             $prettyVersion = PHP_VERSION;
             $version = $versionParser->normalize($prettyVersion);
@@ -36,6 +43,7 @@ class PlatformRepository extends ArrayRepository
             $version = $versionParser->normalize($prettyVersion);
         }
 
+
         $php = new CompletePackage('php', $version, $prettyVersion);
         $php->setDescription('The PHP interpreter');
         parent::addPackage($php);

+ 4 - 18
src/Composer/Script/Event.php

@@ -16,17 +16,13 @@ use Composer\Composer;
 use Composer\IO\IOInterface;
 
 /**
- * The base event class
+ * The script event class
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
-class Event
+class Event extends \Composer\EventDispatcher\Event
 {
-    /**
-     * @var string This event's name
-     */
-    private $name;
-
     /**
      * @var Composer The composer instance
      */
@@ -52,22 +48,12 @@ class Event
      */
     public function __construct($name, Composer $composer, IOInterface $io, $devMode = false)
     {
-        $this->name = $name;
+        parent::__construct($name);
         $this->composer = $composer;
         $this->io = $io;
         $this->devMode = $devMode;
     }
 
-    /**
-     * Returns the event's name.
-     *
-     * @return string The event name
-     */
-    public function getName()
-    {
-        return $this->name;
-    }
-
     /**
      * Returns the composer instance.
      *

+ 11 - 0
src/Composer/Util/RemoteFilesystem.php

@@ -19,6 +19,7 @@ use Composer\Downloader\TransportException;
 /**
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
 class RemoteFilesystem
 {
@@ -76,6 +77,16 @@ class RemoteFilesystem
         return $this->get($originUrl, $fileUrl, $options, null, $progress);
     }
 
+    /**
+     * Retrieve the options set in the constructor
+     *
+     * @return array Options
+     */
+    public function getOptions()
+    {
+        return $this->options;
+    }
+
     /**
      * Get file content or copy action.
      *

+ 3 - 3
tests/Composer/Test/Autoload/AutoloadGeneratorTest.php

@@ -72,7 +72,7 @@ class AutoloadGeneratorTest extends TestCase
             }));
         $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
 
-        $this->eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $this->eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->disableOriginalConstructor()
             ->getMock();
 
@@ -626,12 +626,12 @@ EOF;
     {
         $this->eventDispatcher
             ->expects($this->at(0))
-            ->method('dispatch')
+            ->method('dispatchScript')
             ->with(ScriptEvents::PRE_AUTOLOAD_DUMP, false);
 
         $this->eventDispatcher
             ->expects($this->at(1))
-            ->method('dispatch')
+            ->method('dispatchScript')
             ->with(ScriptEvents::POST_AUTOLOAD_DUMP, false);
 
         $package = new Package('a', '1.0', '1.0');

+ 1 - 1
tests/Composer/Test/Downloader/FileDownloaderTest.php

@@ -23,7 +23,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
         $config = $config ?: $this->getMock('Composer\Config');
         $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock();
 
-        return new FileDownloader($io, $config, null, $rfs, $filesystem);
+        return new FileDownloader($io, $config, null, null, $rfs, $filesystem);
     }
 
     /**

+ 13 - 12
tests/Composer/Test/Script/EventDispatcherTest.php → tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -10,11 +10,12 @@
  * file that was distributed with this source code.
  */
 
-namespace Composer\Test\Script;
+namespace Composer\Test\EventDispatcher;
 
+use Composer\EventDispatcher\Event;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Test\TestCase;
-use Composer\Script\Event;
-use Composer\Script\EventDispatcher;
+use Composer\Script;
 use Composer\Util\ProcessExecutor;
 
 class EventDispatcherTest extends TestCase
@@ -26,12 +27,12 @@ class EventDispatcherTest extends TestCase
     {
         $io = $this->getMock('Composer\IO\IOInterface');
         $dispatcher = $this->getDispatcherStubForListenersTest(array(
-            "Composer\Test\Script\EventDispatcherTest::call"
+            "Composer\Test\EventDispatcher\EventDispatcherTest::call"
         ), $io);
 
         $io->expects($this->once())
             ->method('write')
-            ->with('<error>Script Composer\Test\Script\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception</error>');
+            ->with('<error>Script Composer\Test\EventDispatcher\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception</error>');
 
         $dispatcher->dispatchCommandEvent("post-install-cmd", false);
     }
@@ -43,7 +44,7 @@ class EventDispatcherTest extends TestCase
     public function testDispatcherCanExecuteSingleCommandLineScript($command)
     {
         $process = $this->getMock('Composer\Util\ProcessExecutor');
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -68,7 +69,7 @@ class EventDispatcherTest extends TestCase
     public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack()
     {
         $process = $this->getMock('Composer\Util\ProcessExecutor');
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -86,7 +87,7 @@ class EventDispatcherTest extends TestCase
 
         $listeners = array(
             'echo -n foo',
-            'Composer\\Test\\Script\\EventDispatcherTest::someMethod',
+            'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod',
             'echo -n bar',
         );
         $dispatcher->expects($this->atLeastOnce())
@@ -95,7 +96,7 @@ class EventDispatcherTest extends TestCase
 
         $dispatcher->expects($this->once())
             ->method('executeEventPhpScript')
-            ->with('Composer\Test\Script\EventDispatcherTest', 'someMethod')
+            ->with('Composer\Test\EventDispatcher\EventDispatcherTest', 'someMethod')
             ->will($this->returnValue(true));
 
         $dispatcher->dispatchCommandEvent("post-install-cmd", false);
@@ -103,7 +104,7 @@ class EventDispatcherTest extends TestCase
 
     private function getDispatcherStubForListenersTest($listeners, $io)
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $io,
@@ -129,7 +130,7 @@ class EventDispatcherTest extends TestCase
 
     public function testDispatcherOutputsCommands()
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -150,7 +151,7 @@ class EventDispatcherTest extends TestCase
 
     public function testDispatcherOutputsErrorOnFailedCommand()
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $io = $this->getMock('Composer\IO\IOInterface'),

+ 2 - 2
tests/Composer/Test/Fixtures/installer/custom-installers-are-installed-first.test → tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test

@@ -8,8 +8,8 @@ Composer installers are installed first if they have no requirements
             "package": [
                 { "name": "pkg", "version": "1.0.0" },
                 { "name": "pkg2", "version": "1.0.0" },
-                { "name": "inst", "version": "1.0.0", "type": "composer-installer" },
-                { "name": "inst2", "version": "1.0.0", "type": "composer-installer", "require": { "pkg2": "*" } }
+                { "name": "inst", "version": "1.0.0", "type": "composer-plugin" },
+                { "name": "inst2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } }
             ]
         }
     ],

+ 0 - 19
tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php

@@ -1,19 +0,0 @@
-<?php
-
-namespace Installer;
-
-use Composer\Installer\InstallerInterface;
-use Composer\Package\PackageInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-
-class Custom implements InstallerInterface
-{
-    public $version = 'installer-v1';
-
-    public function supports($packageType) {}
-    public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {}
-    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function getInstallPath(PackageInterface $package) {}
-}

+ 0 - 7
tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Exception.php

@@ -1,7 +0,0 @@
-<?php
-
-namespace Installer;
-
-class Exception extends \Exception
-{
-}

+ 0 - 9
tests/Composer/Test/Installer/Fixtures/installer-v1/composer.json

@@ -1,9 +0,0 @@
-{
-    "name": "",
-    "version": "1.0.0",
-    "type": "composer-installer",
-    "autoload": { "psr-0": { "Installer": "" } },
-    "extra": {
-        "class": "Installer\\Custom"
-    }
-}

+ 0 - 19
tests/Composer/Test/Installer/Fixtures/installer-v2/Installer/Custom2.php

@@ -1,19 +0,0 @@
-<?php
-
-namespace Installer;
-
-use Composer\Installer\InstallerInterface;
-use Composer\Package\PackageInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-
-class Custom2 implements InstallerInterface
-{
-    public $version = 'installer-v2';
-
-    public function supports($packageType) {}
-    public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {}
-    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function getInstallPath(PackageInterface $package) {}
-}

+ 0 - 7
tests/Composer/Test/Installer/Fixtures/installer-v2/Installer/Exception.php

@@ -1,7 +0,0 @@
-<?php
-
-namespace Installer;
-
-class Exception extends \Exception
-{
-}

+ 0 - 9
tests/Composer/Test/Installer/Fixtures/installer-v2/composer.json

@@ -1,9 +0,0 @@
-{
-    "name": "",
-    "version": "2.0.0",
-    "type": "composer-installer",
-    "autoload": { "psr-0": { "Installer": "" } },
-    "extra": {
-        "class": "Installer\\Custom2"
-    }
-}

+ 0 - 19
tests/Composer/Test/Installer/Fixtures/installer-v3/Installer/Custom2.php

@@ -1,19 +0,0 @@
-<?php
-
-namespace Installer;
-
-use Composer\Installer\InstallerInterface;
-use Composer\Package\PackageInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-
-class Custom2 implements InstallerInterface
-{
-    public $version = 'installer-v3';
-
-    public function supports($packageType) {}
-    public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {}
-    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function getInstallPath(PackageInterface $package) {}
-}

+ 0 - 7
tests/Composer/Test/Installer/Fixtures/installer-v3/Installer/Exception.php

@@ -1,7 +0,0 @@
-<?php
-
-namespace Installer;
-
-class Exception extends \Exception
-{
-}

+ 0 - 9
tests/Composer/Test/Installer/Fixtures/installer-v3/composer.json

@@ -1,9 +0,0 @@
-{
-    "name": "",
-    "version": "3.0.0",
-    "type": "composer-installer",
-    "autoload": { "psr-0": { "Installer": "" } },
-    "extra": {
-        "class": "Installer\\Custom2"
-    }
-}

+ 0 - 20
tests/Composer/Test/Installer/Fixtures/installer-v4/Installer/Custom1.php

@@ -1,20 +0,0 @@
-<?php
-
-namespace Installer;
-
-use Composer\Installer\InstallerInterface;
-use Composer\Package\PackageInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-
-class Custom1 implements InstallerInterface
-{
-    public $name = 'custom1';
-    public $version = 'installer-v4';
-
-    public function supports($packageType) {}
-    public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {}
-    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function getInstallPath(PackageInterface $package) {}
-}

+ 0 - 20
tests/Composer/Test/Installer/Fixtures/installer-v4/Installer/Custom2.php

@@ -1,20 +0,0 @@
-<?php
-
-namespace Installer;
-
-use Composer\Installer\InstallerInterface;
-use Composer\Package\PackageInterface;
-use Composer\Repository\InstalledRepositoryInterface;
-
-class Custom2 implements InstallerInterface
-{
-    public $name = 'custom2';
-    public $version = 'installer-v4';
-
-    public function supports($packageType) {}
-    public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function install(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {}
-    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) {}
-    public function getInstallPath(PackageInterface $package) {}
-}

+ 0 - 12
tests/Composer/Test/Installer/Fixtures/installer-v4/composer.json

@@ -1,12 +0,0 @@
-{
-    "name": "",
-    "version": "4.0.0",
-    "type": "composer-installer",
-    "autoload": { "psr-0": { "Installer": "" } },
-    "extra": {
-        "class": [
-            "Installer\\Custom1",
-            "Installer\\Custom2"
-        ]
-    }
-}

+ 2 - 2
tests/Composer/Test/InstallerTest.php

@@ -66,7 +66,7 @@ class InstallerTest extends TestCase
         $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock();
         $installationManager = new InstallationManagerMock();
 
-        $eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock();
+        $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
         $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
 
         $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);
@@ -189,7 +189,7 @@ class InstallerTest extends TestCase
         $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig)));
         $composer->setLocker($locker);
 
-        $eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock();
+        $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
         $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher));
         $composer->setAutoloadGenerator($autoloadGenerator);
         $composer->setEventDispatcher($eventDispatcher);

+ 16 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Installer;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class Plugin implements PluginInterface
+{
+    public $version = 'installer-v1';
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json

@@ -0,0 +1,12 @@
+{
+    "name": "plugin-v1",
+    "version": "1.0.0",
+    "type": "composer-plugin",
+    "autoload": { "psr-0": { "Installer": "" } },
+    "extra": {
+        "class": "Installer\\Plugin"
+    },
+    "require": {
+        "composer-plugin-api": "1.0.0"
+    }
+}

+ 16 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v2/Installer/Plugin2.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Installer;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class Plugin2 implements PluginInterface
+{
+    public $version = 'installer-v2';
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json

@@ -0,0 +1,12 @@
+{
+    "name": "plugin-v2",
+    "version": "2.0.0",
+    "type": "composer-plugin",
+    "autoload": { "psr-0": { "Installer": "" } },
+    "extra": {
+        "class": "Installer\\Plugin2"
+    },
+    "require": {
+        "composer-plugin-api": "1.0.0"
+    }
+}

+ 16 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v3/Installer/Plugin2.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Installer;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class Plugin2 implements PluginInterface
+{
+    public $version = 'installer-v3';
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json

@@ -0,0 +1,12 @@
+{
+    "name": "plugin-v3",
+    "version": "3.0.0",
+    "type": "composer-plugin",
+    "autoload": { "psr-0": { "Installer": "" } },
+    "extra": {
+        "class": "Installer\\Plugin2"
+    },
+    "require": {
+        "composer-plugin-api": "1.0.0"
+    }
+}

+ 17 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin1.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Installer;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class Plugin1 implements PluginInterface
+{
+    public $name = 'plugin1';
+    public $version = 'installer-v4';
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+    }
+}

+ 17 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v4/Installer/Plugin2.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Installer;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class Plugin2 implements PluginInterface
+{
+    public $name = 'plugin2';
+    public $version = 'installer-v4';
+
+    public function activate(Composer $composer, IOInterface $io)
+    {
+    }
+}

+ 15 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json

@@ -0,0 +1,15 @@
+{
+    "name": "plugin-v4",
+    "version": "4.0.0",
+    "type": "composer-plugin",
+    "autoload": { "psr-0": { "Installer": "" } },
+    "extra": {
+        "class": [
+            "Installer\\Plugin1",
+            "Installer\\Plugin2"
+        ]
+    },
+    "require": {
+        "composer-plugin-api": "1.0.0"
+    }
+}

+ 39 - 71
tests/Composer/Test/Installer/InstallerInstallerTest.php → tests/Composer/Test/Plugin/PluginInstallerTest.php

@@ -14,17 +14,19 @@ namespace Composer\Test\Installer;
 
 use Composer\Composer;
 use Composer\Config;
-use Composer\Installer\InstallerInstaller;
+use Composer\Installer\PluginInstaller;
 use Composer\Package\Loader\JsonLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\PackageInterface;
+use Composer\Plugin\PluginManager;
 use Composer\Autoload\AutoloadGenerator;
 
-class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
+class PluginInstallerTest extends \PHPUnit_Framework_TestCase
 {
     protected $composer;
     protected $packages;
     protected $im;
+    protected $pm;
     protected $repository;
     protected $io;
     protected $autoloadGenerator;
@@ -34,17 +36,13 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
         $loader = new JsonLoader(new ArrayLoader());
         $this->packages = array();
         for ($i = 1; $i <= 4; $i++) {
-            $this->packages[] = $loader->load(__DIR__.'/Fixtures/installer-v'.$i.'/composer.json');
+            $this->packages[] = $loader->load(__DIR__.'/Fixtures/plugin-v'.$i.'/composer.json');
         }
 
         $dm = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->disableOriginalConstructor()
             ->getMock();
 
-        $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager')
-            ->disableOriginalConstructor()
-            ->getMock();
-
         $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
 
         $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')
@@ -56,127 +54,97 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
 
         $this->io = $this->getMock('Composer\IO\IOInterface');
 
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock();
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
         $this->autoloadGenerator = new AutoloadGenerator($dispatcher);
 
         $this->composer = new Composer();
         $config = new Config();
         $this->composer->setConfig($config);
         $this->composer->setDownloadManager($dm);
-        $this->composer->setInstallationManager($this->im);
         $this->composer->setRepositoryManager($rm);
         $this->composer->setAutoloadGenerator($this->autoloadGenerator);
 
+        $this->pm = new PluginManager($this->composer, $this->io);
+        $this->composer->setPluginManager($this->pm);
+
         $config->merge(array(
             'config' => array(
                 'vendor-dir' => __DIR__.'/Fixtures/',
+                'home' => __DIR__.'/Fixtures',
                 'bin-dir' => __DIR__.'/Fixtures/bin',
             ),
         ));
     }
 
-    public function testInstallNewInstaller()
+    public function testInstallNewPlugin()
     {
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(2))
             ->method('getPackages')
             ->will($this->returnValue(array()));
-        $installer = new InstallerInstallerMock($this->io, $this->composer);
-
-        $test = $this;
-        $this->im
-            ->expects($this->once())
-            ->method('addInstaller')
-            ->will($this->returnCallback(function ($installer) use ($test) {
-                $test->assertEquals('installer-v1', $installer->version);
-            }));
+        $installer = new PluginInstaller($this->io, $this->composer);
+        $this->pm->loadInstalledPlugins();
 
         $installer->install($this->repository, $this->packages[0]);
+
+        $plugins = $this->pm->getPlugins();
+        $this->assertEquals('installer-v1', $plugins[0]->version);
     }
 
-    public function testInstallMultipleInstallers()
+    public function testInstallMultiplePlugins()
     {
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(2))
             ->method('getPackages')
             ->will($this->returnValue(array()));
-
-        $installer = new InstallerInstallerMock($this->io, $this->composer);
-
-        $test = $this;
-
-        $this->im
-            ->expects($this->at(0))
-            ->method('addInstaller')
-            ->will($this->returnCallback(function ($installer) use ($test) {
-                $test->assertEquals('custom1', $installer->name);
-                $test->assertEquals('installer-v4', $installer->version);
-            }));
-
-        $this->im
-            ->expects($this->at(1))
-            ->method('addInstaller')
-            ->will($this->returnCallback(function ($installer) use ($test) {
-                $test->assertEquals('custom2', $installer->name);
-                $test->assertEquals('installer-v4', $installer->version);
-            }));
+        $installer = new PluginInstaller($this->io, $this->composer);
+        $this->pm->loadInstalledPlugins();
 
         $installer->install($this->repository, $this->packages[3]);
+
+        $plugins = $this->pm->getPlugins();
+        $this->assertEquals('plugin1', $plugins[0]->name);
+        $this->assertEquals('installer-v4', $plugins[0]->version);
+        $this->assertEquals('plugin2', $plugins[1]->name);
+        $this->assertEquals('installer-v4', $plugins[1]->version);
     }
 
     public function testUpgradeWithNewClassName()
     {
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('getPackages')
             ->will($this->returnValue(array($this->packages[0])));
         $this->repository
             ->expects($this->exactly(2))
             ->method('hasPackage')
             ->will($this->onConsecutiveCalls(true, false));
-        $installer = new InstallerInstallerMock($this->io, $this->composer);
-
-        $test = $this;
-        $this->im
-            ->expects($this->once())
-            ->method('addInstaller')
-            ->will($this->returnCallback(function ($installer) use ($test) {
-                $test->assertEquals('installer-v2', $installer->version);
-            }));
+        $installer = new PluginInstaller($this->io, $this->composer);
+        $this->pm->loadInstalledPlugins();
 
         $installer->update($this->repository, $this->packages[0], $this->packages[1]);
+
+        $plugins = $this->pm->getPlugins();
+        $this->assertEquals('installer-v2', $plugins[1]->version);
     }
 
     public function testUpgradeWithSameClassName()
     {
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('getPackages')
             ->will($this->returnValue(array($this->packages[1])));
         $this->repository
             ->expects($this->exactly(2))
             ->method('hasPackage')
             ->will($this->onConsecutiveCalls(true, false));
-        $installer = new InstallerInstallerMock($this->io, $this->composer);
-
-        $test = $this;
-        $this->im
-            ->expects($this->once())
-            ->method('addInstaller')
-            ->will($this->returnCallback(function ($installer) use ($test) {
-                $test->assertEquals('installer-v3', $installer->version);
-            }));
+        $installer = new PluginInstaller($this->io, $this->composer);
+        $this->pm->loadInstalledPlugins();
 
         $installer->update($this->repository, $this->packages[1], $this->packages[2]);
-    }
-}
 
-class InstallerInstallerMock extends InstallerInstaller
-{
-    public function getInstallPath(PackageInterface $package)
-    {
-        $version = $package->getVersion();
-
-        return __DIR__.'/Fixtures/installer-v'.$version[0].'/';
+        $plugins = $this->pm->getPlugins();
+        $this->assertEquals('installer-v3', $plugins[1]->version);
     }
 }
+