PluginManager.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Plugin;
  12. use Composer\Composer;
  13. use Composer\EventDispatcher\EventSubscriberInterface;
  14. use Composer\IO\IOInterface;
  15. use Composer\Package\Package;
  16. use Composer\Semver\VersionParser;
  17. use Composer\Repository\RepositoryInterface;
  18. use Composer\Package\AliasPackage;
  19. use Composer\Package\PackageInterface;
  20. use Composer\Package\Link;
  21. use Composer\Semver\Constraint\Constraint;
  22. use Composer\DependencyResolver\Pool;
  23. /**
  24. * Plugin manager
  25. *
  26. * @author Nils Adermann <naderman@naderman.de>
  27. * @author Jordi Boggiano <j.boggiano@seld.be>
  28. */
  29. class PluginManager
  30. {
  31. protected $composer;
  32. protected $io;
  33. protected $globalComposer;
  34. protected $versionParser;
  35. protected $disablePlugins = false;
  36. protected $plugins = array();
  37. protected $registeredPlugins = array();
  38. private static $classCounter = 0;
  39. /**
  40. * Initializes plugin manager
  41. *
  42. * @param IOInterface $io
  43. * @param Composer $composer
  44. * @param Composer $globalComposer
  45. * @param bool $disablePlugins
  46. */
  47. public function __construct(IOInterface $io, Composer $composer, Composer $globalComposer = null, $disablePlugins = false)
  48. {
  49. $this->io = $io;
  50. $this->composer = $composer;
  51. $this->globalComposer = $globalComposer;
  52. $this->versionParser = new VersionParser();
  53. $this->disablePlugins = $disablePlugins;
  54. }
  55. /**
  56. * Loads all plugins from currently installed plugin packages
  57. */
  58. public function loadInstalledPlugins()
  59. {
  60. if ($this->disablePlugins) {
  61. return;
  62. }
  63. $repo = $this->composer->getRepositoryManager()->getLocalRepository();
  64. $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
  65. if ($repo) {
  66. $this->loadRepository($repo);
  67. }
  68. if ($globalRepo) {
  69. $this->loadRepository($globalRepo);
  70. }
  71. }
  72. /**
  73. * Gets all currently active plugin instances
  74. *
  75. * @return array plugins
  76. */
  77. public function getPlugins()
  78. {
  79. return $this->plugins;
  80. }
  81. /**
  82. * Register a plugin package, activate it etc.
  83. *
  84. * If it's of type composer-installer it is registered as an installer
  85. * instead for BC
  86. *
  87. * @param PackageInterface $package
  88. * @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception
  89. *
  90. * @throws \UnexpectedValueException
  91. */
  92. public function registerPackage(PackageInterface $package, $failOnMissingClasses = false)
  93. {
  94. if ($this->disablePlugins) {
  95. return;
  96. }
  97. $requiresComposer = null;
  98. foreach ($package->getRequires() as $link) { /** @var Link $link */
  99. if ('composer-plugin-api' === $link->getTarget()) {
  100. $requiresComposer = $link->getConstraint();
  101. break;
  102. }
  103. }
  104. if (!$requiresComposer) {
  105. throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package.");
  106. }
  107. $currentPluginApiVersion = $this->getPluginApiVersion();
  108. $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion));
  109. if (!$requiresComposer->matches($currentPluginApiConstraint)) {
  110. $this->io->writeError('<warning>The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.</warning>');
  111. return;
  112. }
  113. $oldInstallerPlugin = ($package->getType() === 'composer-installer');
  114. if (in_array($package->getName(), $this->registeredPlugins)) {
  115. return;
  116. }
  117. $extra = $package->getExtra();
  118. if (empty($extra['class'])) {
  119. throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
  120. }
  121. $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']);
  122. $localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
  123. $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null;
  124. $pool = new Pool('dev');
  125. $pool->addRepository($localRepo);
  126. if ($globalRepo) {
  127. $pool->addRepository($globalRepo);
  128. }
  129. $autoloadPackages = array($package->getName() => $package);
  130. $autoloadPackages = $this->collectDependencies($pool, $autoloadPackages, $package);
  131. $generator = $this->composer->getAutoloadGenerator();
  132. $autoloads = array();
  133. foreach ($autoloadPackages as $autoloadPackage) {
  134. $downloadPath = $this->getInstallPath($autoloadPackage, ($globalRepo && $globalRepo->hasPackage($autoloadPackage)));
  135. $autoloads[] = array($autoloadPackage, $downloadPath);
  136. }
  137. $map = $generator->parseAutoloads($autoloads, new Package('dummy', '1.0.0.0', '1.0.0'));
  138. $classLoader = $generator->createLoader($map);
  139. $classLoader->register();
  140. foreach ($classes as $class) {
  141. if (class_exists($class, false)) {
  142. $code = file_get_contents($classLoader->findFile($class));
  143. $code = preg_replace('{^((?:final\s+)?(?:\s*))class\s+(\S+)}mi', '$1class $2_composer_tmp'.self::$classCounter, $code);
  144. eval('?>'.$code);
  145. $class .= '_composer_tmp'.self::$classCounter;
  146. self::$classCounter++;
  147. }
  148. if ($oldInstallerPlugin) {
  149. $installer = new $class($this->io, $this->composer);
  150. $this->composer->getInstallationManager()->addInstaller($installer);
  151. } elseif (class_exists($class)) {
  152. $plugin = new $class();
  153. $this->addPlugin($plugin);
  154. $this->registeredPlugins[] = $package->getName();
  155. } elseif ($failOnMissingClasses) {
  156. throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class);
  157. }
  158. }
  159. }
  160. /**
  161. * Returns the version of the internal composer-plugin-api package.
  162. *
  163. * @return string
  164. */
  165. protected function getPluginApiVersion()
  166. {
  167. return PluginInterface::PLUGIN_API_VERSION;
  168. }
  169. /**
  170. * Adds a plugin, activates it and registers it with the event dispatcher
  171. *
  172. * @param PluginInterface $plugin plugin instance
  173. */
  174. private function addPlugin(PluginInterface $plugin)
  175. {
  176. if ($this->io->isDebug()) {
  177. $this->io->writeError('Loading plugin '.get_class($plugin));
  178. }
  179. $this->plugins[] = $plugin;
  180. $plugin->activate($this->composer, $this->io);
  181. if ($plugin instanceof EventSubscriberInterface) {
  182. $this->composer->getEventDispatcher()->addSubscriber($plugin);
  183. }
  184. }
  185. /**
  186. * Load all plugins and installers from a repository
  187. *
  188. * Note that plugins in the specified repository that rely on events that
  189. * have fired prior to loading will be missed. This means you likely want to
  190. * call this method as early as possible.
  191. *
  192. * @param RepositoryInterface $repo Repository to scan for plugins to install
  193. *
  194. * @throws \RuntimeException
  195. */
  196. private function loadRepository(RepositoryInterface $repo)
  197. {
  198. foreach ($repo->getPackages() as $package) { /** @var PackageInterface $package */
  199. if ($package instanceof AliasPackage) {
  200. continue;
  201. }
  202. if ('composer-plugin' === $package->getType()) {
  203. $this->registerPackage($package);
  204. // Backward compatibility
  205. } elseif ('composer-installer' === $package->getType()) {
  206. $this->registerPackage($package);
  207. }
  208. }
  209. }
  210. /**
  211. * Recursively generates a map of package names to packages for all deps
  212. *
  213. * @param Pool $pool Package pool of installed packages
  214. * @param array $collected Current state of the map for recursion
  215. * @param PackageInterface $package The package to analyze
  216. *
  217. * @return array Map of package names to packages
  218. */
  219. private function collectDependencies(Pool $pool, array $collected, PackageInterface $package)
  220. {
  221. $requires = array_merge(
  222. $package->getRequires(),
  223. $package->getDevRequires()
  224. );
  225. foreach ($requires as $requireLink) {
  226. $requiredPackage = $this->lookupInstalledPackage($pool, $requireLink);
  227. if ($requiredPackage && !isset($collected[$requiredPackage->getName()])) {
  228. $collected[$requiredPackage->getName()] = $requiredPackage;
  229. $collected = $this->collectDependencies($pool, $collected, $requiredPackage);
  230. }
  231. }
  232. return $collected;
  233. }
  234. /**
  235. * Resolves a package link to a package in the installed pool
  236. *
  237. * Since dependencies are already installed this should always find one.
  238. *
  239. * @param Pool $pool Pool of installed packages only
  240. * @param Link $link Package link to look up
  241. *
  242. * @return PackageInterface|null The found package
  243. */
  244. private function lookupInstalledPackage(Pool $pool, Link $link)
  245. {
  246. $packages = $pool->whatProvides($link->getTarget(), $link->getConstraint());
  247. return (!empty($packages)) ? $packages[0] : null;
  248. }
  249. /**
  250. * Retrieves the path a package is installed to.
  251. *
  252. * @param PackageInterface $package
  253. * @param bool $global Whether this is a global package
  254. *
  255. * @return string Install path
  256. */
  257. private function getInstallPath(PackageInterface $package, $global = false)
  258. {
  259. if (!$global) {
  260. return $this->composer->getInstallationManager()->getInstallPath($package);
  261. }
  262. return $this->globalComposer->getInstallationManager()->getInstallPath($package);
  263. }
  264. }