diff --git a/doc/04-schema.md b/doc/04-schema.md index 7b8ef5558..7d2775af1 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -99,7 +99,7 @@ Out of the box, composer supports three types: their installation, but contains no files and will not write anything to the filesystem. As such, it does not require a dist or source key to be installable. -- **composer-installer:** A package of type `composer-installer` provides an +- **composer-plugin:** A package of type `composer-plugin` may provide an installer for other packages that have a custom type. Read more in the [dedicated article](articles/custom-installers.md). diff --git a/doc/articles/custom-installers.md b/doc/articles/custom-installers.md index 1eb55436e..7c7117248 100644 --- a/doc/articles/custom-installers.md +++ b/doc/articles/custom-installers.md @@ -29,8 +29,8 @@ An example use-case would be: > phpDocumentor features Templates that need to be installed outside of the > default /vendor folder structure. As such they have chosen to adopt the -> `phpdocumentor-template` [type][1] and create a Custom Installer to send -> these templates to the correct folder. +> `phpdocumentor-template` [type][1] and create a plugin providing the Custom +> Installer to send these templates to the correct folder. An example composer.json of such a template package would be: @@ -38,23 +38,24 @@ An example composer.json of such a template package would be: "name": "phpdocumentor/template-responsive", "type": "phpdocumentor-template", "require": { - "phpdocumentor/template-installer": "*" + "phpdocumentor/template-installer-plugin": "*" } } > **IMPORTANT**: to make sure that the template installer is present at the > time the template package is installed, template packages should require -> the installer package. +> the plugin package. ## Creating an Installer A Custom Installer is defined as a class that implements the -[`Composer\Installer\InstallerInterface`][3] and is contained in a Composer -package that has the [type][1] `composer-installer`. +[`Composer\Installer\InstallerInterface`][3] and is usually distributed in a +Composer Plugin. -A basic Installer would thus compose of two files: +A basic Installer Plugin would thus compose of three files: 1. the package file: composer.json +2. The Plugin class, e.g.: `My\Project\Composer\Plugin.php`, containing a class that implements `Composer\Plugin\PluginInterface`. 2. The Installer class, e.g.: `My\Project\Composer\Installer.php`, containing a class that implements `Composer\Installer\InstallerInterface`. ### composer.json @@ -62,35 +63,57 @@ A basic Installer would thus compose of two files: The package file is the same as any other package file but with the following requirements: -1. the [type][1] attribute must be `composer-installer`. +1. the [type][1] attribute must be `composer-plugin`. 2. the [extra][2] attribute must contain an element `class` defining the - class name of the installer (including namespace). If a package contains - multiple installers this can be array of class names. + class name of the plugin (including namespace). If a package contains + multiple plugins this can be array of class names. Example: { - "name": "phpdocumentor/template-installer", - "type": "composer-installer", + "name": "phpdocumentor/template-installer-plugin", + "type": "composer-installer-plugin", "license": "MIT", "autoload": { "psr-0": {"phpDocumentor\\Composer": "src/"} }, "extra": { - "class": "phpDocumentor\\Composer\\TemplateInstaller" + "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin" + } + } + +### The 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 class that executes the custom installation should implement the -[`Composer\Installer\InstallerInterface`][3] (or extend another installer that -implements that interface). - -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. +[`Composer\Installer\InstallerInterface`][4] (or extend another installer that +implements that interface). It defines the [type][1] string as it will be +recognized by packages that will use this installer in the `supports()` method. > **NOTE**: _choose your [type][1] name carefully, it is recommended to follow > the format: `vendor-type`_. For example: `phpdocumentor-template`. @@ -146,7 +169,7 @@ Example: } The example demonstrates that it is quite simple to extend the -[`Composer\Installer\LibraryInstaller`][4] class to strip a prefix +[`Composer\Installer\LibraryInstaller`][5] class to strip a prefix (`phpdocumentor/template-`) and use the remaining part to assemble a completely different installation path. @@ -155,5 +178,6 @@ different installation path. [1]: ../04-schema.md#type [2]: ../04-schema.md#extra -[3]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php -[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php +[3]: https://github.com/composer/composer/blob/master/src/Composer/Plugin/PluginInterface.php +[4]: https://github.com/composer/composer/blob/master/src/Composer/Installer/InstallerInterface.php +[5]: https://github.com/composer/composer/blob/master/src/Composer/Installer/LibraryInstaller.php diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md new file mode 100644 index 000000000..57296f8fe --- /dev/null +++ b/doc/articles/plugins.md @@ -0,0 +1,147 @@ + + +# 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 diff --git a/res/composer-schema.json b/res/composer-schema.json index 328794ef8..eef9aa3d6 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -9,7 +9,7 @@ "required": true }, "type": { - "description": "Package type, either 'library' for common packages, 'composer-installer' for custom installers, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", + "description": "Package type, either 'library' for common packages, 'composer-plugin' for plugins, 'metapackage' for empty packages, or a custom type ([a-z0-9-]+) defined by whatever project this package applies to.", "type": "string" }, "target-dir": { @@ -180,7 +180,7 @@ }, "extra": { "type": ["object", "array"], - "description": "Arbitrary extra data that can be used by custom installers, for example, package of type composer-installer must have a 'class' key defining the installer class name.", + "description": "Arbitrary extra data that can be used by plugins, for example, package of type composer-plugin may have a 'class' key defining an installer class name.", "additionalProperties": true }, "autoload": { diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index a3f1bec62..7c5821025 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -13,12 +13,12 @@ namespace Composer\Autoload; use Composer\Config; +use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; -use Composer\Script\EventDispatcher; use Composer\Script\ScriptEvents; /** @@ -39,7 +39,7 @@ class AutoloadGenerator public function dump(Config $config, InstalledRepositoryInterface $localRepo, PackageInterface $mainPackage, InstallationManager $installationManager, $targetDir, $scanPsr0Packages = false, $suffix = '') { - $this->eventDispatcher->dispatch(ScriptEvents::PRE_AUTOLOAD_DUMP); + $this->eventDispatcher->dispatchScript(ScriptEvents::PRE_AUTOLOAD_DUMP); $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($config->get('vendor-dir')); @@ -191,7 +191,7 @@ EOF; fclose($targetLoader); unset($sourceLoader, $targetLoader); - $this->eventDispatcher->dispatch(ScriptEvents::POST_AUTOLOAD_DUMP); + $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) diff --git a/src/Composer/Command/Command.php b/src/Composer/Command/Command.php index 6e91ff869..862b54e58 100644 --- a/src/Composer/Command/Command.php +++ b/src/Composer/Command/Command.php @@ -38,16 +38,17 @@ abstract class Command extends BaseCommand /** * @param bool $required + * @param bool $disablePlugins * @throws \RuntimeException * @return Composer */ - public function getComposer($required = true) + public function getComposer($required = true, $disablePlugins = false) { if (null === $this->composer) { $application = $this->getApplication(); if ($application instanceof Application) { /* @var $application Application */ - $this->composer = $application->getComposer($required); + $this->composer = $application->getComposer($required, $disablePlugins); } elseif ($required) { throw new \RuntimeException( 'Could not create a Composer\Composer instance, you must inject '. diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index b62033d17..ccf14fde3 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -44,6 +44,7 @@ use Composer\Package\Version\VersionParser; * @author Benjamin Eberlei * @author Jordi Boggiano * @author Tobias Munk + * @author Nils Adermann */ class CreateProjectCommand extends Command { @@ -62,7 +63,8 @@ class CreateProjectCommand extends Command new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'Pick a different repository url to look for the package.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Whether to disable custom installers.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.'), + new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Whether to prevent execution of all defined scripts in the root package.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deletion vcs folder.'), @@ -116,6 +118,11 @@ EOT $preferDist = $input->getOption('prefer-dist'); } + if ($input->getOption('no-custom-installers')) { + $output->writeln('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); + $input->setOption('no-plugins', true); + } + return $this->installProject( $this->getIO(), $config, @@ -127,24 +134,24 @@ EOT $preferDist, !$input->getOption('no-dev'), $input->getOption('repository-url'), - $input->getOption('no-custom-installers'), + $input->getOption('no-plugins'), $input->getOption('no-scripts'), $input->getOption('keep-vcs'), $input->getOption('no-progress') ); } - public function installProject(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disableCustomInstallers = false, $noScripts = false, $keepVcs = false, $noProgress = false) + public function installProject(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false) { $oldCwd = getcwd(); if ($packageName !== null) { - $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disableCustomInstallers, $noScripts, $keepVcs, $noProgress); + $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositoryUrl, $disablePlugins, $noScripts, $keepVcs, $noProgress); } else { $installedFromVcs = false; } - $composer = Factory::create($io); + $composer = Factory::create($io, null, $disablePlugins); if ($noScripts === false) { // dispatch event @@ -158,8 +165,8 @@ EOT ->setDevMode($installDevPackages) ->setRunScripts( ! $noScripts); - if ($disableCustomInstallers) { - $installer->disableCustomInstallers(); + if ($disablePlugins) { + $installer->disablePlugins(); } if (!$installer->run()) { @@ -226,7 +233,7 @@ EOT return 0; } - protected function installRootPackage(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disableCustomInstallers = false, $noScripts = false, $keepVcs = false, $noProgress = false) + protected function installRootPackage(IOInterface $io, $config, $packageName, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositoryUrl = null, $disablePlugins = false, $noScripts = false, $keepVcs = false, $noProgress = false) { $stability = strtolower($stability); if ($stability === 'rc') { @@ -285,8 +292,8 @@ EOT $io->write('Installing ' . $package->getName() . ' (' . VersionParser::formatVersion($package, false) . ')'); - if ($disableCustomInstallers) { - $io->write('Custom installers have been disabled.'); + if ($disablePlugins) { + $io->write('Plugins have been disabled.'); } if (0 === strpos($package->getPrettyVersion(), 'dev-') && in_array($package->getSourceType(), array('git', 'hg'))) { diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index 5603a17c0..755b40b90 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -13,6 +13,8 @@ namespace Composer\Command; use Composer\DependencyResolver\Pool; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -50,7 +52,12 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $repo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); + $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'depends', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $repo = $composer->getRepositoryManager()->getLocalRepository(); $needle = $input->getArgument('package'); $pool = new Pool(); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 57ed3a003..63ce4ee19 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -15,6 +15,8 @@ namespace Composer\Command; use Composer\Composer; use Composer\Factory; use Composer\Downloader\TransportException; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; @@ -64,6 +66,9 @@ EOT $composer = $this->getComposer(false); if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'diagnose', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $output->write('Checking composer.json: '); $this->outputResult($output, $this->checkComposerSchema()); } diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index 4855a409d..3e1541590 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -42,6 +44,10 @@ EOT $output->writeln('Generating autoload files'); $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'dump-autoload', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $installationManager = $composer->getInstallationManager(); $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $package = $composer->getPackage(); diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 05d3f37d9..6138009a3 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -13,6 +13,8 @@ namespace Composer\Command; use Composer\Installer; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -21,6 +23,7 @@ use Symfony\Component\Console\Output\OutputInterface; * @author Jordi Boggiano * @author Ryan Weaver * @author Konstantin Kudryashov + * @author Nils Adermann */ class InstallCommand extends Command { @@ -35,7 +38,8 @@ class InstallCommand extends Command new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), + new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), @@ -56,9 +60,18 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $composer = $this->getComposer(); + if ($input->getOption('no-custom-installers')) { + $output->writeln('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); + $input->setOption('no-plugins', true); + } + + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); $preferSource = false; @@ -90,8 +103,8 @@ EOT ->setOptimizeAutoloader($input->getOption('optimize-autoloader')) ; - if ($input->getOption('no-custom-installers')) { - $install->disableCustomInstallers(); + if ($input->getOption('no-plugins')) { + $install->disablePlugins(); } return $install->run() ? 0 : 1; diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index e30e371c2..a927156c4 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -15,6 +15,8 @@ namespace Composer\Command; use Composer\Package\PackageInterface; use Composer\Json\JsonFile; use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -46,6 +48,10 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'licenses', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $root = $composer->getPackage(); $repo = $composer->getRepositoryManager()->getLocalRepository(); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index fb4f9a9b1..11951dd08 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -21,6 +21,8 @@ use Composer\Installer; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; /** * @author Jérémy Romey @@ -106,6 +108,10 @@ EOT $composer = $this->getComposer(); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); $install diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index a212eb329..b9aaa8d74 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -20,6 +20,8 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Factory; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; /** * @author Robert Schönthal @@ -65,6 +67,11 @@ EOT $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'search', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + } + $onlyName = $input->getOption('only-name'); $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT; diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index a54de99c7..50dabd74a 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -18,6 +18,8 @@ use Composer\DependencyResolver\DefaultPolicy; use Composer\Factory; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -94,6 +96,11 @@ EOT $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } + if ($composer) { + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'show', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + } + // show single package or single version if ($input->getArgument('package') || !empty($package)) { $versions = array(); diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 5151d734b..5edf3b61e 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -17,6 +17,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Downloader\ChangeReportInterface; use Composer\Downloader\VcsDownloader; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; /** @@ -46,6 +48,10 @@ EOT { // init repos $composer = $this->getComposer(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'status', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); $dm = $composer->getDownloadManager(); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index dc103cf99..ceabf7ff4 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -13,6 +13,8 @@ namespace Composer\Command; use Composer\Installer; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -20,6 +22,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano + * @author Nils Adermann */ class UpdateCommand extends Command { @@ -36,7 +39,8 @@ class UpdateCommand extends Command new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('lock', null, InputOption::VALUE_NONE, 'Only updates the lock file hash to suppress warning about the lock file being out of date.'), - new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'Disables all custom installers.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), + new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), @@ -60,9 +64,18 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $composer = $this->getComposer(); + if ($input->getOption('no-custom-installers')) { + $output->writeln('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); + $input->setOption('no-plugins', true); + } + + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $io = $this->getIO(); + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $install = Installer::create($io, $composer); $preferSource = false; @@ -96,8 +109,8 @@ EOT ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $input->getArgument('packages')) ; - if ($input->getOption('no-custom-installers')) { - $install->disableCustomInstallers(); + if ($input->getOption('no-plugins')) { + $install->disablePlugins(); } return $install->run() ? 0 : 1; diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 8c5d0785f..20279b5ac 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -16,13 +16,15 @@ use Composer\Package\RootPackageInterface; use Composer\Package\Locker; use Composer\Repository\RepositoryManager; use Composer\Installer\InstallationManager; +use Composer\Plugin\PluginManager; use Composer\Downloader\DownloadManager; -use Composer\Script\EventDispatcher; +use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; /** * @author Jordi Boggiano * @author Konstantin Kudryashiv + * @author Nils Adermann */ class Composer { @@ -53,13 +55,18 @@ class Composer */ private $installationManager; + /** + * @var Plugin\PluginManager + */ + private $pluginManager; + /** * @var Config */ private $config; /** - * @var Script\EventDispatcher + * @var EventDispatcher\EventDispatcher */ private $eventDispatcher; @@ -166,7 +173,23 @@ class Composer } /** - * @param Script\EventDispatcher $eventDispatcher + * @param Plugin\PluginManager $manager + */ + public function setPluginManager(PluginManager $manager) + { + $this->pluginManager = $manager; + } + + /** + * @return Plugin\PluginManager + */ + public function getPluginManager() + { + return $this->pluginManager; + } + + /** + * @param EventDispatcher\EventDispatcher $eventDispatcher */ public function setEventDispatcher(EventDispatcher $eventDispatcher) { @@ -174,7 +197,7 @@ class Composer } /** - * @return Script\EventDispatcher + * @return EventDispatcher\EventDispatcher */ public function getEventDispatcher() { diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 0d1e45500..63bb59124 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -165,14 +165,15 @@ class Application extends BaseApplication /** * @param bool $required + * @param bool $disablePlugins * @throws JsonValidationException * @return \Composer\Composer */ - public function getComposer($required = true) + public function getComposer($required = true, $disablePlugins = false) { if (null === $this->composer) { try { - $this->composer = Factory::create($this->io); + $this->composer = Factory::create($this->io, null, $disablePlugins); } catch (\InvalidArgumentException $e) { if ($required) { $this->io->write($e->getMessage()); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index e67693563..e52fea0c1 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -17,6 +17,9 @@ use Composer\Cache; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; +use Composer\Plugin\PluginEvents; +use Composer\Plugin\PreFileDownloadEvent; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; use Composer\Util\GitHub; use Composer\Util\RemoteFilesystem; @@ -27,6 +30,7 @@ use Composer\Util\RemoteFilesystem; * @author Kirill chEbba Chebunin * @author Jordi Boggiano * @author François Pluchino + * @author Nils Adermann */ class FileDownloader implements DownloaderInterface { @@ -43,14 +47,16 @@ class FileDownloader implements DownloaderInterface * * @param IOInterface $io The IO instance * @param Config $config The config + * @param EventDispatcher $eventDispatcher The event dispatcher * @param Cache $cache Optional cache instance * @param RemoteFilesystem $rfs The remote filesystem * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) { $this->io = $io; $this->config = $config; + $this->eventDispatcher = $eventDispatcher; $this->rfs = $rfs ?: new RemoteFilesystem($io); $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; @@ -89,6 +95,12 @@ class FileDownloader implements DownloaderInterface $processedUrl = $this->processUrl($package, $url); $hostname = parse_url($processedUrl, PHP_URL_HOST); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); + if ($this->eventDispatcher) { + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + } + $rfs = $preFileDownloadEvent->getRemoteFilesystem(); + if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) { $hostname = 'github.com'; } @@ -104,7 +116,7 @@ class FileDownloader implements DownloaderInterface $retries = 3; while ($retries--) { try { - $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); + $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); break; } catch (TransportException $e) { // if we got an http response with a proper code, then requesting again will probably not help, abort @@ -125,15 +137,18 @@ class FileDownloader implements DownloaderInterface $this->io->write(' Loading from cache'); } } catch (TransportException $e) { - if (in_array($e->getCode(), array(404, 403)) && 'github.com' === $hostname && !$this->io->hasAuthentication($hostname)) { + if (!in_array($e->getCode(), array(404, 403, 412))) { + throw $e; + } + if ('github.com' === $hostname && !$this->io->hasAuthentication($hostname)) { $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode() === 404 ? 'to access private repos' : 'to go over the API rate limit'); - $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs); + $gitHubUtil = new GitHub($this->io, $this->config, null, $rfs); if (!$gitHubUtil->authorizeOAuth($hostname) && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message)) ) { throw $e; } - $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); + $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress); } else { throw $e; } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 80bc60272..c2394543d 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -14,6 +14,7 @@ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; +use Composer\EventDispatcher\EventDispatcher; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use ZipArchive; @@ -25,10 +26,10 @@ class ZipDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, Config $config, Cache $cache = null, ProcessExecutor $process = null) + public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $cache); + parent::__construct($io, $config, $eventDispatcher, $cache); } protected function extract($file, $path) diff --git a/src/Composer/EventDispatcher/Event.php b/src/Composer/EventDispatcher/Event.php new file mode 100644 index 000000000..8a9352653 --- /dev/null +++ b/src/Composer/EventDispatcher/Event.php @@ -0,0 +1,69 @@ + + * Jordi Boggiano + * + * 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 + */ +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; + } +} diff --git a/src/Composer/Script/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php similarity index 66% rename from src/Composer/Script/EventDispatcher.php rename to src/Composer/EventDispatcher/EventDispatcher.php index 46d3d94d7..912d4eaea 100644 --- a/src/Composer/Script/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -10,11 +10,14 @@ * file that was distributed with this source code. */ -namespace Composer\Script; +namespace Composer\EventDispatcher; use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\Script; +use Composer\Script\CommandEvent; +use Composer\Script\PackageEvent; use Composer\Util\ProcessExecutor; /** @@ -28,6 +31,7 @@ use Composer\Util\ProcessExecutor; * * @author François Pluchino * @author Jordi Boggiano + * @author Nils Adermann */ 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 */ public function dispatch($eventName, Event $event = null) { 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); @@ -100,7 +119,9 @@ class EventDispatcher $listeners = $this->getListeners($event); foreach ($listeners as $callable) { - if ($this->isPhpScript($callable)) { + if (!is_string($callable) && is_callable($callable)) { + call_user_func($callable, $event); + } elseif ($this->isPhpScript($callable)) { $className = substr($callable, 0, strpos($callable, '::')); $methodName = substr($callable, strpos($callable, '::') + 2); @@ -127,6 +148,10 @@ class EventDispatcher throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode); } } + + if ($event->isPropagationStopped()) { + break; + } } } @@ -141,10 +166,67 @@ class EventDispatcher } /** + * Add a listener for a particular event + * + * @param string $eventName The event name - typically a constant + * @param Callable $listener A callable expecting an event argument + * @param integer $priority A higher value represents a higher priority + */ + protected function addListener($eventName, $listener, $priority = 0) + { + $this->listeners[$eventName][$priority][] = $listener; + } + + /** + * Adds object methods as listeners for the events in getSubscribedEvents + * + * @see EventSubscriberInterface + * + * @param EventSubscriberInterface $subscriber + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (is_string($params)) { + $this->addListener($eventName, array($subscriber, $params)); + } elseif (is_string($params[0])) { + $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); + } else { + foreach ($params as $listener) { + $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); + } + } + } + } + + /** + * Retrieves all listeners for a given event + * + * @param Event $event + * @return array All listeners: callables and scripts + */ + protected function getListeners(Event $event) + { + $scriptListeners = $this->getScriptListeners($event); + + if (!isset($this->listeners[$event->getName()][0])) { + $this->listeners[$event->getName()][0] = array(); + } + krsort($this->listeners[$event->getName()]); + + $listeners = $this->listeners; + $listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners); + + return call_user_func_array('array_merge', $listeners[$event->getName()]); + } + + /** + * Finds all listeners defined as scripts in the package + * * @param Event $event Event object * @return array Listeners */ - protected function getListeners(Event $event) + protected function getScriptListeners(Event $event) { $package = $this->composer->getPackage(); $scripts = $package->getScripts(); diff --git a/src/Composer/EventDispatcher/EventSubscriberInterface.php b/src/Composer/EventDispatcher/EventSubscriberInterface.php new file mode 100644 index 000000000..6b0c4ca06 --- /dev/null +++ b/src/Composer/EventDispatcher/EventSubscriberInterface.php @@ -0,0 +1,48 @@ + + * Jordi Boggiano + * + * 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 + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + */ +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(); +} diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 82c5084c2..a1d17232d 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -18,10 +18,11 @@ use Composer\IO\IOInterface; use Composer\Package\Archiver; use Composer\Repository\ComposerRepository; use Composer\Repository\RepositoryManager; +use Composer\Repository\RepositoryInterface; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Composer\Script\EventDispatcher; +use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\Package\Version\VersionParser; @@ -31,6 +32,7 @@ use Composer\Package\Version\VersionParser; * @author Ryan Weaver * @author Jordi Boggiano * @author Igor Wiedler + * @author Nils Adermann */ class Factory { @@ -176,11 +178,12 @@ class Factory * @param IOInterface $io IO instance * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will * read from the default filename + * @param bool $disablePlugins Whether plugins should not be loaded * @throws \InvalidArgumentException * @throws \UnexpectedValueException * @return Composer */ - public function createComposer(IOInterface $io, $localConfig = null) + public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false) { // load Composer configuration if (null === $localConfig) { @@ -227,9 +230,6 @@ class Factory $loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, new ProcessExecutor($io)); $package = $loader->load($localConfig); - // initialize download manager - $dm = $this->createDownloadManager($io, $config); - // initialize installation manager $im = $this->createInstallationManager(); @@ -238,20 +238,32 @@ class Factory $composer->setConfig($config); $composer->setPackage($package); $composer->setRepositoryManager($rm); - $composer->setDownloadManager($dm); $composer->setInstallationManager($im); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io); + + // initialize download manager + $dm = $this->createDownloadManager($io, $config, $dispatcher); + + $composer->setDownloadManager($dm); $composer->setEventDispatcher($dispatcher); // initialize autoload generator $generator = new AutoloadGenerator($dispatcher); $composer->setAutoloadGenerator($generator); + $globalRepository = $this->createGlobalRepository($config, $vendorDir); + $pm = $this->createPluginManager($composer, $io, $globalRepository); + $composer->setPluginManager($pm); + // add installers to the manager $this->createDefaultInstallers($im, $composer, $io); + if (!$disablePlugins) { + $pm->loadInstalledPlugins(); + } + // purge packages if they have been deleted on the filesystem $this->purgePackages($rm, $im); @@ -296,12 +308,31 @@ class Factory $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json'))); } + /** + * @param Config $config + * @param string $vendorDir + */ + protected function createGlobalRepository(Config $config, $vendorDir) + { + if ($config->get('home') == $vendorDir) { + return null; + } + + $path = $config->get('home').'/vendor/composer/installed.json'; + if (!file_exists($path)) { + return null; + } + + return new Repository\InstalledFilesystemRepository(new JsonFile($path)); + } + /** * @param IO\IOInterface $io * @param Config $config + * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config) + public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { @@ -325,10 +356,10 @@ class Factory $dm->setDownloader('git', new Downloader\GitDownloader($io, $config)); $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config)); - $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $cache)); - $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $cache)); - $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $cache)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $cache)); + $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache)); + $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache)); + $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache)); return $dm; } @@ -353,6 +384,14 @@ class Factory return $am; } + /** + * @return Plugin\PluginManager + */ + protected function createPluginManager(Composer $composer, IOInterface $io, RepositoryInterface $globalRepository = null) + { + return new Plugin\PluginManager($composer, $io, $globalRepository); + } + /** * @return Installer\InstallationManager */ @@ -370,7 +409,7 @@ class Factory { $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null)); $im->addInstaller(new Installer\PearInstaller($io, $composer, 'pear-library')); - $im->addInstaller(new Installer\InstallerInstaller($io, $composer)); + $im->addInstaller(new Installer\PluginInstaller($io, $composer)); $im->addInstaller(new Installer\MetapackageInstaller($io)); } @@ -392,12 +431,13 @@ class Factory * @param IOInterface $io IO instance * @param mixed $config either a configuration array or a filename to read from, if null it will read from * the default filename + * @param bool $disablePlugins Whether plugins should not be loaded * @return Composer */ - public static function create(IOInterface $io, $config = null) + public static function create(IOInterface $io, $config = null, $disablePlugins = false) { $factory = new static(); - return $factory->createComposer($io, $config); + return $factory->createComposer($io, $config, $disablePlugins); } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8f424cc39..3e9a9bf6b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -24,6 +24,7 @@ use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Downloader\DownloadManager; +use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; use Composer\Config; use Composer\Installer\NoopInstaller; @@ -41,13 +42,13 @@ use Composer\Repository\InstalledFilesystemRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryManager; -use Composer\Script\EventDispatcher; use Composer\Script\ScriptEvents; /** * @author Jordi Boggiano * @author Beau Simensen * @author Konstantin Kudryashov + * @author Nils Adermann */ class Installer { @@ -461,7 +462,7 @@ class Installer $this->io->write('Nothing to install or update'); } - $operations = $this->moveCustomInstallersToFront($operations); + $operations = $this->movePluginsToFront($operations); foreach ($operations as $operation) { // collect suggestions @@ -540,7 +541,7 @@ class Installer /** - * Workaround: if your packages depend on custom installers, we must be sure + * Workaround: if your packages depend on plugins, we must be sure * that those are installed / updated first; else it would lead to packages * being installed multiple times in different folders, when running Composer * twice. @@ -552,7 +553,7 @@ class Installer * @param OperationInterface[] $operations * @return OperationInterface[] reordered operation list */ - private function moveCustomInstallersToFront(array $operations) + private function movePluginsToFront(array $operations) { $installerOps = array(); foreach ($operations as $idx => $op) { @@ -564,7 +565,7 @@ class Installer continue; } - if ($package->getRequires() === array() && $package->getType() === 'composer-installer') { + if ($package->getRequires() === array() && ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer')) { $installerOps[] = $op; unset($operations[$idx]); } @@ -1055,7 +1056,7 @@ class Installer } /** - * Disables custom installers. + * Disables plugins. * * Call this if you want to ensure that third-party code never gets * executed. The default is to automatically install, and execute @@ -1063,9 +1064,9 @@ class Installer * * @return Installer */ - public function disableCustomInstallers() + public function disablePlugins() { - $this->installationManager->disableCustomInstallers(); + $this->installationManager->disablePlugins(); return $this; } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 406ee1166..21b16e2fd 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -14,6 +14,7 @@ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; +use Composer\Plugin\PluginInstaller; use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\DependencyResolver\Operation\OperationInterface; @@ -29,6 +30,7 @@ use Composer\Util\StreamContextFactory; * * @author Konstantin Kudryashov * @author Jordi Boggiano + * @author Nils Adermann */ class InstallationManager { @@ -66,16 +68,16 @@ class InstallationManager } /** - * Disables custom installers. + * Disables plugins. * - * We prevent any custom installers from being instantiated by simply + * We prevent any plugins from being instantiated by simply * deactivating the installer for them. This ensure that no third-party * code is ever executed. */ - public function disableCustomInstallers() + public function disablePlugins() { foreach ($this->installers as $i => $installer) { - if (!$installer instanceof InstallerInstaller) { + if (!$installer instanceof PluginInstaller) { continue; } diff --git a/src/Composer/Installer/InstallerInstaller.php b/src/Composer/Installer/InstallerInstaller.php deleted file mode 100644 index a833b68d2..000000000 --- a/src/Composer/Installer/InstallerInstaller.php +++ /dev/null @@ -1,104 +0,0 @@ - - * Jordi Boggiano - * - * 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 - */ -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); - } - } -} diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php new file mode 100644 index 000000000..61c5a2823 --- /dev/null +++ b/src/Composer/Installer/PluginInstaller.php @@ -0,0 +1,81 @@ + + * Jordi Boggiano + * + * 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 + * @author Nils Adermann + */ +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); + } +} diff --git a/src/Composer/Plugin/CommandEvent.php b/src/Composer/Plugin/CommandEvent.php new file mode 100644 index 000000000..ac2ad2551 --- /dev/null +++ b/src/Composer/Plugin/CommandEvent.php @@ -0,0 +1,87 @@ + + * Jordi Boggiano + * + * 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 + */ +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; + } +} diff --git a/src/Composer/Plugin/PluginEvents.php b/src/Composer/Plugin/PluginEvents.php new file mode 100644 index 000000000..ce9efdef2 --- /dev/null +++ b/src/Composer/Plugin/PluginEvents.php @@ -0,0 +1,41 @@ + + * Jordi Boggiano + * + * 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 + */ +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'; +} diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php new file mode 100644 index 000000000..3a33672a6 --- /dev/null +++ b/src/Composer/Plugin/PluginInterface.php @@ -0,0 +1,39 @@ + + * Jordi Boggiano + * + * 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 + */ +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); +} diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php new file mode 100644 index 000000000..dacf9bd57 --- /dev/null +++ b/src/Composer/Plugin/PluginManager.php @@ -0,0 +1,259 @@ + + * Jordi Boggiano + * + * 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 + */ +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("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."); + } + + $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(); + } +} diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php new file mode 100644 index 000000000..847477e10 --- /dev/null +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -0,0 +1,80 @@ + + * Jordi Boggiano + * + * 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 + */ +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; + } +} diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 3f208aae0..7c4b72673 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -14,6 +14,7 @@ namespace Composer\Repository; use Composer\Package\CompletePackage; use Composer\Package\Version\VersionParser; +use Composer\Plugin\PluginInterface; /** * @author Jordi Boggiano @@ -28,6 +29,12 @@ class PlatformRepository extends ArrayRepository $versionParser = new VersionParser(); + $prettyVersion = PluginInterface::PLUGIN_API_VERSION; + $version = $versionParser->normalize($prettyVersion); + $composerPluginApi = new CompletePackage('composer-plugin-api', $version, $prettyVersion); + $composerPluginApi->setDescription('The Composer Plugin API'); + parent::addPackage($composerPluginApi); + try { $prettyVersion = PHP_VERSION; $version = $versionParser->normalize($prettyVersion); @@ -36,6 +43,7 @@ class PlatformRepository extends ArrayRepository $version = $versionParser->normalize($prettyVersion); } + $php = new CompletePackage('php', $version, $prettyVersion); $php->setDescription('The PHP interpreter'); parent::addPackage($php); diff --git a/src/Composer/Script/Event.php b/src/Composer/Script/Event.php index cafea2948..40b109b2d 100644 --- a/src/Composer/Script/Event.php +++ b/src/Composer/Script/Event.php @@ -16,17 +16,13 @@ use Composer\Composer; use Composer\IO\IOInterface; /** - * The base event class + * The script event class * * @author François Pluchino + * @author Nils Adermann */ -class Event +class Event extends \Composer\EventDispatcher\Event { - /** - * @var string This event's name - */ - private $name; - /** * @var Composer The composer instance */ @@ -52,22 +48,12 @@ class Event */ public function __construct($name, Composer $composer, IOInterface $io, $devMode = false) { - $this->name = $name; + parent::__construct($name); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; } - /** - * Returns the event's name. - * - * @return string The event name - */ - public function getName() - { - return $this->name; - } - /** * Returns the composer instance. * diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 3db55ab6d..7c26bd290 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -19,6 +19,7 @@ use Composer\Downloader\TransportException; /** * @author François Pluchino * @author Jordi Boggiano + * @author Nils Adermann */ class RemoteFilesystem { @@ -76,6 +77,16 @@ class RemoteFilesystem return $this->get($originUrl, $fileUrl, $options, null, $progress); } + /** + * Retrieve the options set in the constructor + * + * @return array Options + */ + public function getOptions() + { + return $this->options; + } + /** * Get file content or copy action. * diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 774276dda..1fe825197 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -72,7 +72,7 @@ class AutoloadGeneratorTest extends TestCase })); $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); - $this->eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $this->eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->disableOriginalConstructor() ->getMock(); @@ -626,12 +626,12 @@ EOF; { $this->eventDispatcher ->expects($this->at(0)) - ->method('dispatch') + ->method('dispatchScript') ->with(ScriptEvents::PRE_AUTOLOAD_DUMP, false); $this->eventDispatcher ->expects($this->at(1)) - ->method('dispatch') + ->method('dispatchScript') ->with(ScriptEvents::POST_AUTOLOAD_DUMP, false); $package = new Package('a', '1.0', '1.0'); diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index cc7d25df3..8e2b92044 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -23,7 +23,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase $config = $config ?: $this->getMock('Composer\Config'); $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); - return new FileDownloader($io, $config, null, $rfs, $filesystem); + return new FileDownloader($io, $config, null, null, $rfs, $filesystem); } /** diff --git a/tests/Composer/Test/Script/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php similarity index 83% rename from tests/Composer/Test/Script/EventDispatcherTest.php rename to tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index cd8f8e76f..7a15679d1 100644 --- a/tests/Composer/Test/Script/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -10,11 +10,12 @@ * file that was distributed with this source code. */ -namespace Composer\Test\Script; +namespace Composer\Test\EventDispatcher; +use Composer\EventDispatcher\Event; +use Composer\EventDispatcher\EventDispatcher; use Composer\Test\TestCase; -use Composer\Script\Event; -use Composer\Script\EventDispatcher; +use Composer\Script; use Composer\Util\ProcessExecutor; class EventDispatcherTest extends TestCase @@ -26,12 +27,12 @@ class EventDispatcherTest extends TestCase { $io = $this->getMock('Composer\IO\IOInterface'); $dispatcher = $this->getDispatcherStubForListenersTest(array( - "Composer\Test\Script\EventDispatcherTest::call" + "Composer\Test\EventDispatcher\EventDispatcherTest::call" ), $io); $io->expects($this->once()) ->method('write') - ->with('Script Composer\Test\Script\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'); + ->with('Script Composer\Test\EventDispatcher\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'); $dispatcher->dispatchCommandEvent("post-install-cmd", false); } @@ -43,7 +44,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherCanExecuteSingleCommandLineScript($command) { $process = $this->getMock('Composer\Util\ProcessExecutor'); - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -68,7 +69,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack() { $process = $this->getMock('Composer\Util\ProcessExecutor'); - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -86,7 +87,7 @@ class EventDispatcherTest extends TestCase $listeners = array( 'echo -n foo', - 'Composer\\Test\\Script\\EventDispatcherTest::someMethod', + 'Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod', 'echo -n bar', ); $dispatcher->expects($this->atLeastOnce()) @@ -95,7 +96,7 @@ class EventDispatcherTest extends TestCase $dispatcher->expects($this->once()) ->method('executeEventPhpScript') - ->with('Composer\Test\Script\EventDispatcherTest', 'someMethod') + ->with('Composer\Test\EventDispatcher\EventDispatcherTest', 'someMethod') ->will($this->returnValue(true)); $dispatcher->dispatchCommandEvent("post-install-cmd", false); @@ -103,7 +104,7 @@ class EventDispatcherTest extends TestCase private function getDispatcherStubForListenersTest($listeners, $io) { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $io, @@ -129,7 +130,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherOutputsCommands() { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $this->getMock('Composer\IO\IOInterface'), @@ -150,7 +151,7 @@ class EventDispatcherTest extends TestCase public function testDispatcherOutputsErrorOnFailedCommand() { - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher') + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), $io = $this->getMock('Composer\IO\IOInterface'), diff --git a/tests/Composer/Test/Fixtures/installer/custom-installers-are-installed-first.test b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test similarity index 90% rename from tests/Composer/Test/Fixtures/installer/custom-installers-are-installed-first.test rename to tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test index dd9d26d98..c57a36d35 100644 --- a/tests/Composer/Test/Fixtures/installer/custom-installers-are-installed-first.test +++ b/tests/Composer/Test/Fixtures/installer/plugins-are-installed-first.test @@ -8,8 +8,8 @@ Composer installers are installed first if they have no requirements "package": [ { "name": "pkg", "version": "1.0.0" }, { "name": "pkg2", "version": "1.0.0" }, - { "name": "inst", "version": "1.0.0", "type": "composer-installer" }, - { "name": "inst2", "version": "1.0.0", "type": "composer-installer", "require": { "pkg2": "*" } } + { "name": "inst", "version": "1.0.0", "type": "composer-plugin" }, + { "name": "inst2", "version": "1.0.0", "type": "composer-plugin", "require": { "pkg2": "*" } } ] } ], diff --git a/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php b/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php deleted file mode 100644 index bfad4a88a..000000000 --- a/tests/Composer/Test/Installer/Fixtures/installer-v1/Installer/Custom.php +++ /dev/null @@ -1,19 +0,0 @@ -getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); - $eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock(); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); @@ -189,7 +189,7 @@ class InstallerTest extends TestCase $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig))); $composer->setLocker($locker); - $eventDispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock(); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); $composer->setAutoloadGenerator($autoloadGenerator); $composer->setEventDispatcher($eventDispatcher); diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php new file mode 100644 index 000000000..f80acd325 --- /dev/null +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/Installer/Plugin.php @@ -0,0 +1,16 @@ +packages = array(); for ($i = 1; $i <= 4; $i++) { - $this->packages[] = $loader->load(__DIR__.'/Fixtures/installer-v'.$i.'/composer.json'); + $this->packages[] = $loader->load(__DIR__.'/Fixtures/plugin-v'.$i.'/composer.json'); } $dm = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->disableOriginalConstructor() ->getMock(); - $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager') - ->disableOriginalConstructor() - ->getMock(); - $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager') @@ -56,127 +54,97 @@ class InstallerInstallerTest extends \PHPUnit_Framework_TestCase $this->io = $this->getMock('Composer\IO\IOInterface'); - $dispatcher = $this->getMockBuilder('Composer\Script\EventDispatcher')->disableOriginalConstructor()->getMock(); + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $this->autoloadGenerator = new AutoloadGenerator($dispatcher); $this->composer = new Composer(); $config = new Config(); $this->composer->setConfig($config); $this->composer->setDownloadManager($dm); - $this->composer->setInstallationManager($this->im); $this->composer->setRepositoryManager($rm); $this->composer->setAutoloadGenerator($this->autoloadGenerator); + $this->pm = new PluginManager($this->composer, $this->io); + $this->composer->setPluginManager($this->pm); + $config->merge(array( 'config' => array( 'vendor-dir' => __DIR__.'/Fixtures/', + 'home' => __DIR__.'/Fixtures', 'bin-dir' => __DIR__.'/Fixtures/bin', ), )); } - public function testInstallNewInstaller() + public function testInstallNewPlugin() { $this->repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('getPackages') ->will($this->returnValue(array())); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v1', $installer->version); - })); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); $installer->install($this->repository, $this->packages[0]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('installer-v1', $plugins[0]->version); } - public function testInstallMultipleInstallers() + public function testInstallMultiplePlugins() { $this->repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('getPackages') ->will($this->returnValue(array())); - - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - - $this->im - ->expects($this->at(0)) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('custom1', $installer->name); - $test->assertEquals('installer-v4', $installer->version); - })); - - $this->im - ->expects($this->at(1)) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('custom2', $installer->name); - $test->assertEquals('installer-v4', $installer->version); - })); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); $installer->install($this->repository, $this->packages[3]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('plugin1', $plugins[0]->name); + $this->assertEquals('installer-v4', $plugins[0]->version); + $this->assertEquals('plugin2', $plugins[1]->name); + $this->assertEquals('installer-v4', $plugins[1]->version); } public function testUpgradeWithNewClassName() { $this->repository - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('getPackages') ->will($this->returnValue(array($this->packages[0]))); $this->repository ->expects($this->exactly(2)) ->method('hasPackage') ->will($this->onConsecutiveCalls(true, false)); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v2', $installer->version); - })); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); $installer->update($this->repository, $this->packages[0], $this->packages[1]); + + $plugins = $this->pm->getPlugins(); + $this->assertEquals('installer-v2', $plugins[1]->version); } public function testUpgradeWithSameClassName() { $this->repository - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('getPackages') ->will($this->returnValue(array($this->packages[1]))); $this->repository ->expects($this->exactly(2)) ->method('hasPackage') ->will($this->onConsecutiveCalls(true, false)); - $installer = new InstallerInstallerMock($this->io, $this->composer); - - $test = $this; - $this->im - ->expects($this->once()) - ->method('addInstaller') - ->will($this->returnCallback(function ($installer) use ($test) { - $test->assertEquals('installer-v3', $installer->version); - })); + $installer = new PluginInstaller($this->io, $this->composer); + $this->pm->loadInstalledPlugins(); $installer->update($this->repository, $this->packages[1], $this->packages[2]); + + $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].'/'; - } -}