Browse Source

Merge pull request #2179 from naderman/plugins

Plugins
Nils Adermann 12 years ago
parent
commit
242c58c789
64 changed files with 1433 additions and 463 deletions
  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
   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
   filesystem. As such, it does not require a dist or source key to be
   installable.
   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
   installer for other packages that have a custom type. Read more in the
   [dedicated article](articles/custom-installers.md).
   [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
 > phpDocumentor features Templates that need to be installed outside of the
 > default /vendor folder structure. As such they have chosen to adopt 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:
 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",
         "name": "phpdocumentor/template-responsive",
         "type": "phpdocumentor-template",
         "type": "phpdocumentor-template",
         "require": {
         "require": {
-            "phpdocumentor/template-installer": "*"
+            "phpdocumentor/template-installer-plugin": "*"
         }
         }
     }
     }
 
 
 > **IMPORTANT**: to make sure that the template installer is present at the
 > **IMPORTANT**: to make sure that the template installer is present at the
 > time the template package is installed, template packages should require
 > time the template package is installed, template packages should require
-> the installer package.
+> the plugin package.
 
 
 ## Creating an Installer
 ## Creating an Installer
 
 
 A Custom Installer is defined as a class that implements the
 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
 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`.
 2. The Installer class, e.g.: `My\Project\Composer\Installer.php`, containing a class that implements `Composer\Installer\InstallerInterface`.
 
 
 ### composer.json
 ### 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
 The package file is the same as any other package file but with the following
 requirements:
 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
 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:
 Example:
 
 
     {
     {
-        "name": "phpdocumentor/template-installer",
-        "type": "composer-installer",
+        "name": "phpdocumentor/template-installer-plugin",
+        "type": "composer-installer-plugin",
         "license": "MIT",
         "license": "MIT",
         "autoload": {
         "autoload": {
             "psr-0": {"phpDocumentor\\Composer": "src/"}
             "psr-0": {"phpDocumentor\\Composer": "src/"}
         },
         },
         "extra": {
         "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
 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.
 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
 > **NOTE**: _choose your [type][1] name carefully, it is recommended to follow
 > the format: `vendor-type`_. For example: `phpdocumentor-template`.
 > the format: `vendor-type`_. For example: `phpdocumentor-template`.
@@ -146,7 +169,7 @@ Example:
     }
     }
 
 
 The example demonstrates that it is quite simple to extend the
 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
 (`phpdocumentor/template-`) and use the remaining part to assemble a completely
 different installation path.
 different installation path.
 
 
@@ -155,5 +178,6 @@ different installation path.
 
 
 [1]: ../04-schema.md#type
 [1]: ../04-schema.md#type
 [2]: ../04-schema.md#extra
 [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
             "required": true
         },
         },
         "type": {
         "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"
             "type": "string"
         },
         },
         "target-dir": {
         "target-dir": {
@@ -180,7 +180,7 @@
         },
         },
         "extra": {
         "extra": {
             "type": ["object", "array"],
             "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
             "additionalProperties": true
         },
         },
         "autoload": {
         "autoload": {

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

@@ -13,12 +13,12 @@
 namespace Composer\Autoload;
 namespace Composer\Autoload;
 
 
 use Composer\Config;
 use Composer\Config;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Installer\InstallationManager;
 use Composer\Installer\InstallationManager;
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
-use Composer\Script\EventDispatcher;
 use Composer\Script\ScriptEvents;
 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 = '')
     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 = new Filesystem();
         $filesystem->ensureDirectoryExists($config->get('vendor-dir'));
         $filesystem->ensureDirectoryExists($config->get('vendor-dir'));
@@ -191,7 +191,7 @@ EOF;
         fclose($targetLoader);
         fclose($targetLoader);
         unset($sourceLoader, $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)
     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              $required
+     * @param  bool              $disablePlugins
      * @throws \RuntimeException
      * @throws \RuntimeException
      * @return Composer
      * @return Composer
      */
      */
-    public function getComposer($required = true)
+    public function getComposer($required = true, $disablePlugins = false)
     {
     {
         if (null === $this->composer) {
         if (null === $this->composer) {
             $application = $this->getApplication();
             $application = $this->getApplication();
             if ($application instanceof Application) {
             if ($application instanceof Application) {
                 /* @var $application    Application */
                 /* @var $application    Application */
-                $this->composer = $application->getComposer($required);
+                $this->composer = $application->getComposer($required, $disablePlugins);
             } elseif ($required) {
             } elseif ($required) {
                 throw new \RuntimeException(
                 throw new \RuntimeException(
                     'Could not create a Composer\Composer instance, you must inject '.
                     '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 Benjamin Eberlei <kontakt@beberlei.de>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Tobias Munk <schmunk@usrbin.de>
  * @author Tobias Munk <schmunk@usrbin.de>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class CreateProjectCommand extends Command
 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('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('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-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-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('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.'),
                 new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deletion vcs folder.'),
@@ -116,6 +118,11 @@ EOT
             $preferDist = $input->getOption('prefer-dist');
             $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(
         return $this->installProject(
             $this->getIO(),
             $this->getIO(),
             $config,
             $config,
@@ -127,24 +134,24 @@ EOT
             $preferDist,
             $preferDist,
             !$input->getOption('no-dev'),
             !$input->getOption('no-dev'),
             $input->getOption('repository-url'),
             $input->getOption('repository-url'),
-            $input->getOption('no-custom-installers'),
+            $input->getOption('no-plugins'),
             $input->getOption('no-scripts'),
             $input->getOption('no-scripts'),
             $input->getOption('keep-vcs'),
             $input->getOption('keep-vcs'),
             $input->getOption('no-progress')
             $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();
         $oldCwd = getcwd();
 
 
         if ($packageName !== null) {
         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 {
         } else {
             $installedFromVcs = false;
             $installedFromVcs = false;
         }
         }
 
 
-        $composer = Factory::create($io);
+        $composer = Factory::create($io, null, $disablePlugins);
 
 
         if ($noScripts === false) {
         if ($noScripts === false) {
             // dispatch event
             // dispatch event
@@ -158,8 +165,8 @@ EOT
             ->setDevMode($installDevPackages)
             ->setDevMode($installDevPackages)
             ->setRunScripts( ! $noScripts);
             ->setRunScripts( ! $noScripts);
 
 
-        if ($disableCustomInstallers) {
-            $installer->disableCustomInstallers();
+        if ($disablePlugins) {
+            $installer->disablePlugins();
         }
         }
 
 
         if (!$installer->run()) {
         if (!$installer->run()) {
@@ -226,7 +233,7 @@ EOT
         return 0;
         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);
         $stability = strtolower($stability);
         if ($stability === 'rc') {
         if ($stability === 'rc') {
@@ -285,8 +292,8 @@ EOT
 
 
         $io->write('<info>Installing ' . $package->getName() . ' (' . VersionParser::formatVersion($package, false) . ')</info>');
         $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'))) {
         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;
 namespace Composer\Command;
 
 
 use Composer\DependencyResolver\Pool;
 use Composer\DependencyResolver\Pool;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -50,7 +52,12 @@ EOT
 
 
     protected function execute(InputInterface $input, OutputInterface $output)
     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');
         $needle = $input->getArgument('package');
 
 
         $pool = new Pool();
         $pool = new Pool();

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

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

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

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

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

@@ -13,6 +13,8 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Installer;
 use Composer\Installer;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
@@ -21,6 +23,7 @@ use Symfony\Component\Console\Output\OutputInterface;
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class InstallCommand extends Command
 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('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('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-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-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('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.'),
                 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)
     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'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
         $install = Installer::create($io, $composer);
 
 
         $preferSource = false;
         $preferSource = false;
@@ -90,8 +103,8 @@ EOT
             ->setOptimizeAutoloader($input->getOption('optimize-autoloader'))
             ->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;
         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\Package\PackageInterface;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Helper\TableHelper;
 use Symfony\Component\Console\Helper\TableHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
@@ -46,6 +48,10 @@ EOT
     protected function execute(InputInterface $input, OutputInterface $output)
     protected function execute(InputInterface $input, OutputInterface $output)
     {
     {
         $composer = $this->getComposer();
         $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'licenses', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $root = $composer->getPackage();
         $root = $composer->getPackage();
         $repo = $composer->getRepositoryManager()->getLocalRepository();
         $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\JsonFile;
 use Composer\Json\JsonManipulator;
 use Composer\Json\JsonManipulator;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 
 
 /**
 /**
  * @author Jérémy Romey <jeremy@free-agent.fr>
  * @author Jérémy Romey <jeremy@free-agent.fr>
@@ -106,6 +108,10 @@ EOT
         $composer = $this->getComposer();
         $composer = $this->getComposer();
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
         $install = Installer::create($io, $composer);
 
 
         $install
         $install

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

@@ -20,6 +20,8 @@ use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Factory;
 use Composer\Factory;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 
 
 /**
 /**
  * @author Robert Schönthal <seroscho@googlemail.com>
  * @author Robert Schönthal <seroscho@googlemail.com>
@@ -65,6 +67,11 @@ EOT
             $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
             $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');
         $onlyName = $input->getOption('only-name');
 
 
         $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT;
         $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\Factory;
 use Composer\Package\CompletePackageInterface;
 use Composer\Package\CompletePackageInterface;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
@@ -94,6 +96,11 @@ EOT
             $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos));
             $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
         // show single package or single version
         if ($input->getArgument('package') || !empty($package)) {
         if ($input->getArgument('package') || !empty($package)) {
             $versions = array();
             $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 Symfony\Component\Console\Output\OutputInterface;
 use Composer\Downloader\ChangeReportInterface;
 use Composer\Downloader\ChangeReportInterface;
 use Composer\Downloader\VcsDownloader;
 use Composer\Downloader\VcsDownloader;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Composer\Script\ScriptEvents;
 use Composer\Script\ScriptEvents;
 
 
 /**
 /**
@@ -46,6 +48,10 @@ EOT
     {
     {
         // init repos
         // init repos
         $composer = $this->getComposer();
         $composer = $this->getComposer();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'status', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
         $installedRepo = $composer->getRepositoryManager()->getLocalRepository();
 
 
         $dm = $composer->getDownloadManager();
         $dm = $composer->getDownloadManager();

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

@@ -13,6 +13,8 @@
 namespace Composer\Command;
 namespace Composer\Command;
 
 
 use Composer\Installer;
 use Composer\Installer;
+use Composer\Plugin\CommandEvent;
+use Composer\Plugin\PluginEvents;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputArgument;
@@ -20,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class UpdateCommand extends Command
 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('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-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('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-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('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.'),
                 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)
     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'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
         $io = $this->getIO();
         $io = $this->getIO();
+
+        $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
+        $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
+
         $install = Installer::create($io, $composer);
         $install = Installer::create($io, $composer);
 
 
         $preferSource = false;
         $preferSource = false;
@@ -96,8 +109,8 @@ EOT
             ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages'))
             ->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;
         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\Package\Locker;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\RepositoryManager;
 use Composer\Installer\InstallationManager;
 use Composer\Installer\InstallationManager;
+use Composer\Plugin\PluginManager;
 use Composer\Downloader\DownloadManager;
 use Composer\Downloader\DownloadManager;
-use Composer\Script\EventDispatcher;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Autoload\AutoloadGenerator;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Konstantin Kudryashiv <ever.zet@gmail.com>
  * @author Konstantin Kudryashiv <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class Composer
 class Composer
 {
 {
@@ -53,13 +55,18 @@ class Composer
      */
      */
     private $installationManager;
     private $installationManager;
 
 
+    /**
+     * @var Plugin\PluginManager
+     */
+    private $pluginManager;
+
     /**
     /**
      * @var Config
      * @var Config
      */
      */
     private $config;
     private $config;
 
 
     /**
     /**
-     * @var Script\EventDispatcher
+     * @var EventDispatcher\EventDispatcher
      */
      */
     private $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)
     public function setEventDispatcher(EventDispatcher $eventDispatcher)
     {
     {
@@ -174,7 +197,7 @@ class Composer
     }
     }
 
 
     /**
     /**
-     * @return Script\EventDispatcher
+     * @return EventDispatcher\EventDispatcher
      */
      */
     public function getEventDispatcher()
     public function getEventDispatcher()
     {
     {

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

@@ -165,14 +165,15 @@ class Application extends BaseApplication
 
 
     /**
     /**
      * @param  bool                    $required
      * @param  bool                    $required
+     * @param  bool                    $disablePlugins
      * @throws JsonValidationException
      * @throws JsonValidationException
      * @return \Composer\Composer
      * @return \Composer\Composer
      */
      */
-    public function getComposer($required = true)
+    public function getComposer($required = true, $disablePlugins = false)
     {
     {
         if (null === $this->composer) {
         if (null === $this->composer) {
             try {
             try {
-                $this->composer = Factory::create($this->io);
+                $this->composer = Factory::create($this->io, null, $disablePlugins);
             } catch (\InvalidArgumentException $e) {
             } catch (\InvalidArgumentException $e) {
                 if ($required) {
                 if ($required) {
                     $this->io->write($e->getMessage());
                     $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\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\PluginEvents;
+use Composer\Plugin\PreFileDownloadEvent;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
 use Composer\Util\GitHub;
 use Composer\Util\GitHub;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\RemoteFilesystem;
@@ -27,6 +30,7 @@ use Composer\Util\RemoteFilesystem;
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class FileDownloader implements DownloaderInterface
 class FileDownloader implements DownloaderInterface
 {
 {
@@ -43,14 +47,16 @@ class FileDownloader implements DownloaderInterface
      *
      *
      * @param IOInterface      $io         The IO instance
      * @param IOInterface      $io         The IO instance
      * @param Config           $config     The config
      * @param Config           $config     The config
+     * @param EventDispatcher  $eventDispatcher The event dispatcher
      * @param Cache            $cache      Optional cache instance
      * @param Cache            $cache      Optional cache instance
      * @param RemoteFilesystem $rfs        The remote filesystem
      * @param RemoteFilesystem $rfs        The remote filesystem
      * @param Filesystem       $filesystem The 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->io = $io;
         $this->config = $config;
         $this->config = $config;
+        $this->eventDispatcher = $eventDispatcher;
         $this->rfs = $rfs ?: new RemoteFilesystem($io);
         $this->rfs = $rfs ?: new RemoteFilesystem($io);
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->cache = $cache;
         $this->cache = $cache;
@@ -89,6 +95,12 @@ class FileDownloader implements DownloaderInterface
         $processedUrl = $this->processUrl($package, $url);
         $processedUrl = $this->processUrl($package, $url);
         $hostname = parse_url($processedUrl, PHP_URL_HOST);
         $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)) {
         if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) {
             $hostname = 'github.com';
             $hostname = 'github.com';
         }
         }
@@ -104,7 +116,7 @@ class FileDownloader implements DownloaderInterface
                     $retries = 3;
                     $retries = 3;
                     while ($retries--) {
                     while ($retries--) {
                         try {
                         try {
-                            $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
+                            $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
                             break;
                             break;
                         } catch (TransportException $e) {
                         } catch (TransportException $e) {
                             // if we got an http response with a proper code, then requesting again will probably not help, abort
                             // 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');
                     $this->io->write('    Loading from cache');
                 }
                 }
             } catch (TransportException $e) {
             } 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');
                     $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)
                     if (!$gitHubUtil->authorizeOAuth($hostname)
                         && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
                         && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
                     ) {
                     ) {
                         throw $e;
                         throw $e;
                     }
                     }
-                    $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
+                    $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
                 } else {
                 } else {
                     throw $e;
                     throw $e;
                 }
                 }

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

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 
 use Composer\Config;
 use Composer\Config;
 use Composer\Cache;
 use Composer\Cache;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use ZipArchive;
 use ZipArchive;
@@ -25,10 +26,10 @@ class ZipDownloader extends ArchiveDownloader
 {
 {
     protected $process;
     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);
         $this->process = $process ?: new ProcessExecutor($io);
-        parent::__construct($io, $config, $cache);
+        parent::__construct($io, $config, $eventDispatcher, $cache);
     }
     }
 
 
     protected function extract($file, $path)
     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.
  * file that was distributed with this source code.
  */
  */
 
 
-namespace Composer\Script;
+namespace Composer\EventDispatcher;
 
 
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Composer;
 use Composer\Composer;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\Script;
+use Composer\Script\CommandEvent;
+use Composer\Script\PackageEvent;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 
 
 /**
 /**
@@ -28,6 +31,7 @@ use Composer\Util\ProcessExecutor;
  *
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class EventDispatcher
 class EventDispatcher
 {
 {
@@ -50,16 +54,31 @@ class EventDispatcher
         $this->process = $process ?: new ProcessExecutor($io);
         $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.
      * Dispatch a script event.
      *
      *
      * @param string $eventName The constant in ScriptEvents
      * @param string $eventName The constant in ScriptEvents
      * @param Event  $event
      * @param Event  $event
      */
      */
-    public function dispatch($eventName, Event $event = null)
+    public function dispatchScript($eventName, Script\Event $event = null)
     {
     {
         if (null == $event) {
         if (null == $event) {
-            $event = new Event($eventName, $this->composer, $this->io);
+            $event = new Script\Event($eventName, $this->composer, $this->io);
         }
         }
 
 
         $this->doDispatch($event);
         $this->doDispatch($event);
@@ -100,7 +119,9 @@ class EventDispatcher
         $listeners = $this->getListeners($event);
         $listeners = $this->getListeners($event);
 
 
         foreach ($listeners as $callable) {
         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, '::'));
                 $className = substr($callable, 0, strpos($callable, '::'));
                 $methodName = substr($callable, strpos($callable, '::') + 2);
                 $methodName = substr($callable, strpos($callable, '::') + 2);
 
 
@@ -127,6 +148,10 @@ class EventDispatcher
                     throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
                     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
      * @param  Event $event Event object
      * @return array Listeners
      * @return array Listeners
      */
      */
-    protected function getListeners(Event $event)
+    protected function getScriptListeners(Event $event)
     {
     {
         $package = $this->composer->getPackage();
         $package = $this->composer->getPackage();
         $scripts = $package->getScripts();
         $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\Package\Archiver;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\RepositoryManager;
+use Composer\Repository\RepositoryInterface;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\RemoteFilesystem;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
-use Composer\Script\EventDispatcher;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
 
 
@@ -31,6 +32,7 @@ use Composer\Package\Version\VersionParser;
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Ryan Weaver <ryan@knplabs.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Igor Wiedler <igor@wiedler.ch>
  * @author Igor Wiedler <igor@wiedler.ch>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class Factory
 class Factory
 {
 {
@@ -176,11 +178,12 @@ class Factory
      * @param IOInterface       $io          IO instance
      * @param IOInterface       $io          IO instance
      * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will
      * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will
      *                                       read from the default filename
      *                                       read from the default filename
+     * @param bool        $disablePlugins Whether plugins should not be loaded
      * @throws \InvalidArgumentException
      * @throws \InvalidArgumentException
      * @throws \UnexpectedValueException
      * @throws \UnexpectedValueException
      * @return Composer
      * @return Composer
      */
      */
-    public function createComposer(IOInterface $io, $localConfig = null)
+    public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false)
     {
     {
         // load Composer configuration
         // load Composer configuration
         if (null === $localConfig) {
         if (null === $localConfig) {
@@ -227,9 +230,6 @@ class Factory
         $loader  = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io));
         $loader  = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io));
         $package = $loader->load($localConfig);
         $package = $loader->load($localConfig);
 
 
-        // initialize download manager
-        $dm = $this->createDownloadManager($io, $config);
-
         // initialize installation manager
         // initialize installation manager
         $im = $this->createInstallationManager();
         $im = $this->createInstallationManager();
 
 
@@ -238,20 +238,32 @@ class Factory
         $composer->setConfig($config);
         $composer->setConfig($config);
         $composer->setPackage($package);
         $composer->setPackage($package);
         $composer->setRepositoryManager($rm);
         $composer->setRepositoryManager($rm);
-        $composer->setDownloadManager($dm);
         $composer->setInstallationManager($im);
         $composer->setInstallationManager($im);
 
 
         // initialize event dispatcher
         // initialize event dispatcher
         $dispatcher = new EventDispatcher($composer, $io);
         $dispatcher = new EventDispatcher($composer, $io);
+
+        // initialize download manager
+        $dm = $this->createDownloadManager($io, $config, $dispatcher);
+
+        $composer->setDownloadManager($dm);
         $composer->setEventDispatcher($dispatcher);
         $composer->setEventDispatcher($dispatcher);
 
 
         // initialize autoload generator
         // initialize autoload generator
         $generator = new AutoloadGenerator($dispatcher);
         $generator = new AutoloadGenerator($dispatcher);
         $composer->setAutoloadGenerator($generator);
         $composer->setAutoloadGenerator($generator);
 
 
+        $globalRepository = $this->createGlobalRepository($config, $vendorDir);
+        $pm = $this->createPluginManager($composer, $io, $globalRepository);
+        $composer->setPluginManager($pm);
+
         // add installers to the manager
         // add installers to the manager
         $this->createDefaultInstallers($im, $composer, $io);
         $this->createDefaultInstallers($im, $composer, $io);
 
 
+        if (!$disablePlugins) {
+            $pm->loadInstalledPlugins();
+        }
+
         // purge packages if they have been deleted on the filesystem
         // purge packages if they have been deleted on the filesystem
         $this->purgePackages($rm, $im);
         $this->purgePackages($rm, $im);
 
 
@@ -296,12 +308,31 @@ class Factory
         $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json')));
         $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  IO\IOInterface             $io
      * @param  Config                     $config
      * @param  Config                     $config
+     * @param  EventDispatcher            $eventDispatcher
      * @return Downloader\DownloadManager
      * @return Downloader\DownloadManager
      */
      */
-    public function createDownloadManager(IOInterface $io, Config $config)
+    public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null)
     {
     {
         $cache = null;
         $cache = null;
         if ($config->get('cache-files-ttl') > 0) {
         if ($config->get('cache-files-ttl') > 0) {
@@ -325,10 +356,10 @@ class Factory
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
         $dm->setDownloader('hg', new Downloader\HgDownloader($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;
         return $dm;
     }
     }
@@ -353,6 +384,14 @@ class Factory
         return $am;
         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
      * @return Installer\InstallationManager
      */
      */
@@ -370,7 +409,7 @@ class Factory
     {
     {
         $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null));
         $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null));
         $im->addInstaller(new Installer\PearInstaller($io, $composer, 'pear-library'));
         $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));
         $im->addInstaller(new Installer\MetapackageInstaller($io));
     }
     }
 
 
@@ -392,12 +431,13 @@ class Factory
      * @param IOInterface $io     IO instance
      * @param IOInterface $io     IO instance
      * @param mixed       $config either a configuration array or a filename to read from, if null it will read from
      * @param mixed       $config either a configuration array or a filename to read from, if null it will read from
      *                             the default filename
      *                             the default filename
+     * @param bool        $disablePlugins Whether plugins should not be loaded
      * @return Composer
      * @return Composer
      */
      */
-    public static function create(IOInterface $io, $config = null)
+    public static function create(IOInterface $io, $config = null, $disablePlugins = false)
     {
     {
         $factory = new static();
         $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\Solver;
 use Composer\DependencyResolver\SolverProblemsException;
 use Composer\DependencyResolver\SolverProblemsException;
 use Composer\Downloader\DownloadManager;
 use Composer\Downloader\DownloadManager;
+use Composer\EventDispatcher\EventDispatcher;
 use Composer\Installer\InstallationManager;
 use Composer\Installer\InstallationManager;
 use Composer\Config;
 use Composer\Config;
 use Composer\Installer\NoopInstaller;
 use Composer\Installer\NoopInstaller;
@@ -41,13 +42,13 @@ use Composer\Repository\InstalledFilesystemRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\RepositoryManager;
-use Composer\Script\EventDispatcher;
 use Composer\Script\ScriptEvents;
 use Composer\Script\ScriptEvents;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Beau Simensen <beau@dflydev.com>
  * @author Beau Simensen <beau@dflydev.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class Installer
 class Installer
 {
 {
@@ -461,7 +462,7 @@ class Installer
             $this->io->write('Nothing to install or update');
             $this->io->write('Nothing to install or update');
         }
         }
 
 
-        $operations = $this->moveCustomInstallersToFront($operations);
+        $operations = $this->movePluginsToFront($operations);
 
 
         foreach ($operations as $operation) {
         foreach ($operations as $operation) {
             // collect suggestions
             // 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
      * that those are installed / updated first; else it would lead to packages
      * being installed multiple times in different folders, when running Composer
      * being installed multiple times in different folders, when running Composer
      * twice.
      * twice.
@@ -552,7 +553,7 @@ class Installer
      * @param OperationInterface[] $operations
      * @param OperationInterface[] $operations
      * @return OperationInterface[] reordered operation list
      * @return OperationInterface[] reordered operation list
      */
      */
-    private function moveCustomInstallersToFront(array $operations)
+    private function movePluginsToFront(array $operations)
     {
     {
         $installerOps = array();
         $installerOps = array();
         foreach ($operations as $idx => $op) {
         foreach ($operations as $idx => $op) {
@@ -564,7 +565,7 @@ class Installer
                 continue;
                 continue;
             }
             }
 
 
-            if ($package->getRequires() === array() && $package->getType() === 'composer-installer') {
+            if ($package->getRequires() === array() && ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer')) {
                 $installerOps[] = $op;
                 $installerOps[] = $op;
                 unset($operations[$idx]);
                 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
      * Call this if you want to ensure that third-party code never gets
      * executed. The default is to automatically install, and execute
      * executed. The default is to automatically install, and execute
@@ -1063,9 +1064,9 @@ class Installer
      *
      *
      * @return Installer
      * @return Installer
      */
      */
-    public function disableCustomInstallers()
+    public function disablePlugins()
     {
     {
-        $this->installationManager->disableCustomInstallers();
+        $this->installationManager->disablePlugins();
 
 
         return $this;
         return $this;
     }
     }

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

@@ -14,6 +14,7 @@ namespace Composer\Installer;
 
 
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\AliasPackage;
 use Composer\Package\AliasPackage;
+use Composer\Plugin\PluginInstaller;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
 use Composer\DependencyResolver\Operation\OperationInterface;
@@ -29,6 +30,7 @@ use Composer\Util\StreamContextFactory;
  *
  *
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class InstallationManager
 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
      * deactivating the installer for them. This ensure that no third-party
      * code is ever executed.
      * code is ever executed.
      */
      */
-    public function disableCustomInstallers()
+    public function disablePlugins()
     {
     {
         foreach ($this->installers as $i => $installer) {
         foreach ($this->installers as $i => $installer) {
-            if (!$installer instanceof InstallerInstaller) {
+            if (!$installer instanceof PluginInstaller) {
                 continue;
                 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\CompletePackage;
 use Composer\Package\Version\VersionParser;
 use Composer\Package\Version\VersionParser;
+use Composer\Plugin\PluginInterface;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -28,6 +29,12 @@ class PlatformRepository extends ArrayRepository
 
 
         $versionParser = new VersionParser();
         $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 {
         try {
             $prettyVersion = PHP_VERSION;
             $prettyVersion = PHP_VERSION;
             $version = $versionParser->normalize($prettyVersion);
             $version = $versionParser->normalize($prettyVersion);
@@ -36,6 +43,7 @@ class PlatformRepository extends ArrayRepository
             $version = $versionParser->normalize($prettyVersion);
             $version = $versionParser->normalize($prettyVersion);
         }
         }
 
 
+
         $php = new CompletePackage('php', $version, $prettyVersion);
         $php = new CompletePackage('php', $version, $prettyVersion);
         $php->setDescription('The PHP interpreter');
         $php->setDescription('The PHP interpreter');
         parent::addPackage($php);
         parent::addPackage($php);

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

@@ -16,17 +16,13 @@ use Composer\Composer;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 
 
 /**
 /**
- * The base event class
+ * The script event class
  *
  *
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  * @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
      * @var Composer The composer instance
      */
      */
@@ -52,22 +48,12 @@ class Event
      */
      */
     public function __construct($name, Composer $composer, IOInterface $io, $devMode = false)
     public function __construct($name, Composer $composer, IOInterface $io, $devMode = false)
     {
     {
-        $this->name = $name;
+        parent::__construct($name);
         $this->composer = $composer;
         $this->composer = $composer;
         $this->io = $io;
         $this->io = $io;
         $this->devMode = $devMode;
         $this->devMode = $devMode;
     }
     }
 
 
-    /**
-     * Returns the event's name.
-     *
-     * @return string The event name
-     */
-    public function getName()
-    {
-        return $this->name;
-    }
-
     /**
     /**
      * Returns the composer instance.
      * 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 François Pluchino <francois.pluchino@opendisplay.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Nils Adermann <naderman@naderman.de>
  */
  */
 class RemoteFilesystem
 class RemoteFilesystem
 {
 {
@@ -76,6 +77,16 @@ class RemoteFilesystem
         return $this->get($originUrl, $fileUrl, $options, null, $progress);
         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.
      * 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->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
 
 
-        $this->eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $this->eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->disableOriginalConstructor()
             ->disableOriginalConstructor()
             ->getMock();
             ->getMock();
 
 
@@ -626,12 +626,12 @@ EOF;
     {
     {
         $this->eventDispatcher
         $this->eventDispatcher
             ->expects($this->at(0))
             ->expects($this->at(0))
-            ->method('dispatch')
+            ->method('dispatchScript')
             ->with(ScriptEvents::PRE_AUTOLOAD_DUMP, false);
             ->with(ScriptEvents::PRE_AUTOLOAD_DUMP, false);
 
 
         $this->eventDispatcher
         $this->eventDispatcher
             ->expects($this->at(1))
             ->expects($this->at(1))
-            ->method('dispatch')
+            ->method('dispatchScript')
             ->with(ScriptEvents::POST_AUTOLOAD_DUMP, false);
             ->with(ScriptEvents::POST_AUTOLOAD_DUMP, false);
 
 
         $package = new Package('a', '1.0', '1.0');
         $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');
         $config = $config ?: $this->getMock('Composer\Config');
         $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock();
         $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.
  * 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\Test\TestCase;
-use Composer\Script\Event;
-use Composer\Script\EventDispatcher;
+use Composer\Script;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\ProcessExecutor;
 
 
 class EventDispatcherTest extends TestCase
 class EventDispatcherTest extends TestCase
@@ -26,12 +27,12 @@ class EventDispatcherTest extends TestCase
     {
     {
         $io = $this->getMock('Composer\IO\IOInterface');
         $io = $this->getMock('Composer\IO\IOInterface');
         $dispatcher = $this->getDispatcherStubForListenersTest(array(
         $dispatcher = $this->getDispatcherStubForListenersTest(array(
-            "Composer\Test\Script\EventDispatcherTest::call"
+            "Composer\Test\EventDispatcher\EventDispatcherTest::call"
         ), $io);
         ), $io);
 
 
         $io->expects($this->once())
         $io->expects($this->once())
             ->method('write')
             ->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);
         $dispatcher->dispatchCommandEvent("post-install-cmd", false);
     }
     }
@@ -43,7 +44,7 @@ class EventDispatcherTest extends TestCase
     public function testDispatcherCanExecuteSingleCommandLineScript($command)
     public function testDispatcherCanExecuteSingleCommandLineScript($command)
     {
     {
         $process = $this->getMock('Composer\Util\ProcessExecutor');
         $process = $this->getMock('Composer\Util\ProcessExecutor');
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -68,7 +69,7 @@ class EventDispatcherTest extends TestCase
     public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack()
     public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack()
     {
     {
         $process = $this->getMock('Composer\Util\ProcessExecutor');
         $process = $this->getMock('Composer\Util\ProcessExecutor');
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -86,7 +87,7 @@ class EventDispatcherTest extends TestCase
 
 
         $listeners = array(
         $listeners = array(
             'echo -n foo',
             'echo -n foo',
-            'Composer\\Test\\Script\\EventDispatcherTest::someMethod',
+            'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod',
             'echo -n bar',
             'echo -n bar',
         );
         );
         $dispatcher->expects($this->atLeastOnce())
         $dispatcher->expects($this->atLeastOnce())
@@ -95,7 +96,7 @@ class EventDispatcherTest extends TestCase
 
 
         $dispatcher->expects($this->once())
         $dispatcher->expects($this->once())
             ->method('executeEventPhpScript')
             ->method('executeEventPhpScript')
-            ->with('Composer\Test\Script\EventDispatcherTest', 'someMethod')
+            ->with('Composer\Test\EventDispatcher\EventDispatcherTest', 'someMethod')
             ->will($this->returnValue(true));
             ->will($this->returnValue(true));
 
 
         $dispatcher->dispatchCommandEvent("post-install-cmd", false);
         $dispatcher->dispatchCommandEvent("post-install-cmd", false);
@@ -103,7 +104,7 @@ class EventDispatcherTest extends TestCase
 
 
     private function getDispatcherStubForListenersTest($listeners, $io)
     private function getDispatcherStubForListenersTest($listeners, $io)
     {
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\Composer'),
                 $io,
                 $io,
@@ -129,7 +130,7 @@ class EventDispatcherTest extends TestCase
 
 
     public function testDispatcherOutputsCommands()
     public function testDispatcherOutputsCommands()
     {
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\IO\IOInterface'),
                 $this->getMock('Composer\IO\IOInterface'),
@@ -150,7 +151,7 @@ class EventDispatcherTest extends TestCase
 
 
     public function testDispatcherOutputsErrorOnFailedCommand()
     public function testDispatcherOutputsErrorOnFailedCommand()
     {
     {
-        $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
                 $this->getMock('Composer\Composer'),
                 $io = $this->getMock('Composer\IO\IOInterface'),
                 $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": [
             "package": [
                 { "name": "pkg", "version": "1.0.0" },
                 { "name": "pkg", "version": "1.0.0" },
                 { "name": "pkg2", "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();
         $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock();
         $installationManager = new InstallationManagerMock();
         $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();
         $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
 
 
         $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);
         $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)));
         $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig)));
         $composer->setLocker($locker);
         $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));
         $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher));
         $composer->setAutoloadGenerator($autoloadGenerator);
         $composer->setAutoloadGenerator($autoloadGenerator);
         $composer->setEventDispatcher($eventDispatcher);
         $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\Composer;
 use Composer\Config;
 use Composer\Config;
-use Composer\Installer\InstallerInstaller;
+use Composer\Installer\PluginInstaller;
 use Composer\Package\Loader\JsonLoader;
 use Composer\Package\Loader\JsonLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
+use Composer\Plugin\PluginManager;
 use Composer\Autoload\AutoloadGenerator;
 use Composer\Autoload\AutoloadGenerator;
 
 
-class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
+class PluginInstallerTest extends \PHPUnit_Framework_TestCase
 {
 {
     protected $composer;
     protected $composer;
     protected $packages;
     protected $packages;
     protected $im;
     protected $im;
+    protected $pm;
     protected $repository;
     protected $repository;
     protected $io;
     protected $io;
     protected $autoloadGenerator;
     protected $autoloadGenerator;
@@ -34,17 +36,13 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
         $loader = new JsonLoader(new ArrayLoader());
         $loader = new JsonLoader(new ArrayLoader());
         $this->packages = array();
         $this->packages = array();
         for ($i = 1; $i <= 4; $i++) {
         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')
         $dm = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->disableOriginalConstructor()
             ->disableOriginalConstructor()
             ->getMock();
             ->getMock();
 
 
-        $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager')
-            ->disableOriginalConstructor()
-            ->getMock();
-
         $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
         $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
 
 
         $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')
         $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')
@@ -56,127 +54,97 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase
 
 
         $this->io = $this->getMock('Composer\IO\IOInterface');
         $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->autoloadGenerator = new AutoloadGenerator($dispatcher);
 
 
         $this->composer = new Composer();
         $this->composer = new Composer();
         $config = new Config();
         $config = new Config();
         $this->composer->setConfig($config);
         $this->composer->setConfig($config);
         $this->composer->setDownloadManager($dm);
         $this->composer->setDownloadManager($dm);
-        $this->composer->setInstallationManager($this->im);
         $this->composer->setRepositoryManager($rm);
         $this->composer->setRepositoryManager($rm);
         $this->composer->setAutoloadGenerator($this->autoloadGenerator);
         $this->composer->setAutoloadGenerator($this->autoloadGenerator);
 
 
+        $this->pm = new PluginManager($this->composer, $this->io);
+        $this->composer->setPluginManager($this->pm);
+
         $config->merge(array(
         $config->merge(array(
             'config' => array(
             'config' => array(
                 'vendor-dir' => __DIR__.'/Fixtures/',
                 'vendor-dir' => __DIR__.'/Fixtures/',
+                'home' => __DIR__.'/Fixtures',
                 'bin-dir' => __DIR__.'/Fixtures/bin',
                 'bin-dir' => __DIR__.'/Fixtures/bin',
             ),
             ),
         ));
         ));
     }
     }
 
 
-    public function testInstallNewInstaller()
+    public function testInstallNewPlugin()
     {
     {
         $this->repository
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(2))
             ->method('getPackages')
             ->method('getPackages')
             ->will($this->returnValue(array()));
             ->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]);
         $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
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(2))
             ->method('getPackages')
             ->method('getPackages')
             ->will($this->returnValue(array()));
             ->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]);
         $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()
     public function testUpgradeWithNewClassName()
     {
     {
         $this->repository
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('getPackages')
             ->method('getPackages')
             ->will($this->returnValue(array($this->packages[0])));
             ->will($this->returnValue(array($this->packages[0])));
         $this->repository
         $this->repository
             ->expects($this->exactly(2))
             ->expects($this->exactly(2))
             ->method('hasPackage')
             ->method('hasPackage')
             ->will($this->onConsecutiveCalls(true, false));
             ->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]);
         $installer->update($this->repository, $this->packages[0], $this->packages[1]);
+
+        $plugins = $this->pm->getPlugins();
+        $this->assertEquals('installer-v2', $plugins[1]->version);
     }
     }
 
 
     public function testUpgradeWithSameClassName()
     public function testUpgradeWithSameClassName()
     {
     {
         $this->repository
         $this->repository
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('getPackages')
             ->method('getPackages')
             ->will($this->returnValue(array($this->packages[1])));
             ->will($this->returnValue(array($this->packages[1])));
         $this->repository
         $this->repository
             ->expects($this->exactly(2))
             ->expects($this->exactly(2))
             ->method('hasPackage')
             ->method('hasPackage')
             ->will($this->onConsecutiveCalls(true, false));
             ->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]);
         $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);
     }
     }
 }
 }
+