1
0
Fork 0

Merge pull request #2179 from naderman/plugins

Plugins
pull/2236/merge
Nils Adermann 2013-09-06 04:42:27 -07:00
commit 242c58c789
64 changed files with 1435 additions and 465 deletions

View File

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

View File

@ -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 > `phpdocumentor-template` [type][1] and create a plugin providing the Custom
> these templates to the correct folder. > 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 [`Composer\Installer\InstallerInterface`][3] and is usually distributed in a
package that has the [type][1] `composer-installer`. 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 class name of the plugin (including namespace). If a package contains
multiple installers this can be array of class names. multiple plugins this can be array of class names.
Example: Example:
{ {
"name": "phpdocumentor/template-installer", "name": "phpdocumentor/template-installer-plugin",
"type": "composer-installer", "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 Plugin class
The class defining the Composer plugin must implement the
[`Composer\Plugin\PluginInterface`][3]. It can then register the Custom
Installer in its `activate()` method.
The class may be placed in any location and have any name, as long as it is
autoloadable and matches the `extra.class` element in the package definition.
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 Custom Installer class
The class that executes the custom installation should implement the The class that executes the custom installation should implement the
[`Composer\Installer\InstallerInterface`][3] (or extend another installer that [`Composer\Installer\InstallerInterface`][4] (or extend another installer that
implements that interface). 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.
The class may be placed in any location and have any name, as long as it is
autoloadable and matches the `extra.class` element in the package definition.
It will also define the [type][1] string as it will be recognized by packages
that will use this installer in the `supports()` method.
> **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 [3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php
[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.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
doc/articles/plugins.md Normal file
View File

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

View File

@ -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": {

View File

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

View File

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

View File

@ -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) { if ($disablePlugins) {
$installer->disableCustomInstallers(); $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) { if ($disablePlugins) {
$io->write('<info>Custom installers have been disabled.</info>'); $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'))) {

View File

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

View File

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

View File

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

View File

@ -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')) { if ($input->getOption('no-plugins')) {
$install->disableCustomInstallers(); $install->disablePlugins();
} }
return $install->run() ? 0 : 1; return $install->run() ? 0 : 1;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')) { if ($input->getOption('no-plugins')) {
$install->disableCustomInstallers(); $install->disablePlugins();
} }
return $install->run() ? 0 : 1; return $install->run() ? 0 : 1;

View File

@ -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()
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{ {
@ -51,15 +55,30 @@ class EventDispatcher
} }
/** /**
* Dispatch a script event. * Dispatch an event
* *
* @param string $eventName The constant in ScriptEvents * @param string $eventName An event name
* @param Event $event * @param Event $event
*/ */
public function dispatch($eventName, Event $event = null) public function dispatch($eventName, Event $event = null)
{ {
if (null == $event) { if (null == $event) {
$event = new Event($eventName, $this->composer, $this->io); $event = new Event($eventName);
}
$this->doDispatch($event);
}
/**
* Dispatch a script event.
*
* @param string $eventName The constant in ScriptEvents
* @param Event $event
*/
public function dispatchScript($eventName, Script\Event $event = null)
{
if (null == $event) {
$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();

View File

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

View File

@ -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('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $cache)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $cache)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $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);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
use Composer\Script\EventDispatcher;
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'),

View File

@ -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": "inst", "version": "1.0.0", "type": "composer-plugin" },
{ "name": "inst2", "version": "1.0.0", "type": "composer-installer", "require": { "pkg2": "*" } } { "name": "inst2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } }
] ]
} }
], ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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); $installer = new PluginInstaller($this->io, $this->composer);
$this->pm->loadInstalledPlugins();
$test = $this;
$this->im
->expects($this->once())
->method('addInstaller')
->will($this->returnCallback(function ($installer) use ($test) {
$test->assertEquals('installer-v1', $installer->version);
}));
$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 PluginInstaller($this->io, $this->composer);
$installer = new InstallerInstallerMock($this->io, $this->composer); $this->pm->loadInstalledPlugins();
$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->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); $installer = new PluginInstaller($this->io, $this->composer);
$this->pm->loadInstalledPlugins();
$test = $this;
$this->im
->expects($this->once())
->method('addInstaller')
->will($this->returnCallback(function ($installer) use ($test) {
$test->assertEquals('installer-v2', $installer->version);
}));
$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); $installer = new PluginInstaller($this->io, $this->composer);
$this->pm->loadInstalledPlugins();
$test = $this;
$this->im
->expects($this->once())
->method('addInstaller')
->will($this->returnCallback(function ($installer) use ($test) {
$test->assertEquals('installer-v3', $installer->version);
}));
$installer->update($this->repository, $this->packages[1], $this->packages[2]); $installer->update($this->repository, $this->packages[1], $this->packages[2]);
$plugins = $this->pm->getPlugins();
$this->assertEquals('installer-v3', $plugins[1]->version);
} }
} }
class InstallerInstallerMock extends InstallerInstaller
{
public function getInstallPath(PackageInterface $package)
{
$version = $package->getVersion();
return __DIR__.'/Fixtures/installer-v'.$version[0].'/';
}
}