PageRenderTime 64ms CodeModel.GetById 37ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Composer/Action/RequirePackage.php

https://gitlab.com/peterboorsma/fluor
PHP | 281 lines | 150 code | 43 blank | 88 comment | 14 complexity | 17183011b21298bb24d462782f1200c7 MD5 | raw file
  1. <?php
  2. namespace Bolt\Composer\Action;
  3. use Bolt\Exception\PackageManagerException;
  4. use Composer\Installer;
  5. use Composer\Json\JsonFile;
  6. use Composer\Json\JsonManipulator;
  7. use Composer\Package\Version\VersionParser;
  8. use Composer\Package\Version\VersionSelector;
  9. use Composer\Repository\CompositeRepository;
  10. use Composer\Repository\PlatformRepository;
  11. use Silex\Application;
  12. /**
  13. * Composer require package class.
  14. *
  15. * @author Gawain Lynch <gawain.lynch@gmail.com>
  16. */
  17. final class RequirePackage
  18. {
  19. /**
  20. * @var \Silex\Application
  21. */
  22. private $app;
  23. /**
  24. * @var \Composer\Package\Version\VersionSelector
  25. */
  26. private $versionSelector;
  27. /**
  28. * @var \Composer\Repository\RepositoryInterface
  29. */
  30. private $repos;
  31. /**
  32. * @param $app \Silex\Application
  33. */
  34. public function __construct(Application $app)
  35. {
  36. $this->app = $app;
  37. $this->versionSelector = new VersionSelector($this->app['extend.manager']->getPool());
  38. }
  39. /**
  40. * Require (install) a package.
  41. *
  42. * @param $package array Package names and version to require
  43. * - Format: array('name' => '', 'version' => '')
  44. *
  45. * @throws \Bolt\Exception\PackageManagerException
  46. *
  47. * @return int 0 on success or a positive error code on failure
  48. */
  49. public function execute(array $package)
  50. {
  51. /** @var $composer \Composer\Composer */
  52. $composer = $this->app['extend.manager']->getComposer();
  53. $io = $this->app['extend.manager']->getIO();
  54. $options = $this->app['extend.manager']->getOptions();
  55. $file = $options['composerjson'];
  56. $newlyCreated = !file_exists($file);
  57. if (!file_exists($file) && !file_put_contents($file, "{\n}\n")) {
  58. // JSON could not be created
  59. return 1;
  60. }
  61. if (!is_readable($file)) {
  62. // JSON is not readable
  63. return 1;
  64. }
  65. if (!is_writable($file)) {
  66. // JSON is not writable
  67. return 1;
  68. }
  69. // Get the Composer repos
  70. $repos = $composer->getRepositoryManager()->getRepositories();
  71. $this->repos = new CompositeRepository(
  72. array_merge(
  73. array(new PlatformRepository()),
  74. $repos
  75. )
  76. );
  77. // Format the package array
  78. $package = $this->formatRequirements($package);
  79. // Validate requirements format
  80. $versionParser = new VersionParser();
  81. foreach ($package as $constraint) {
  82. $versionParser->parseConstraints($constraint);
  83. }
  84. // Get the JSON object
  85. $json = new JsonFile($options['composerjson']);
  86. // Update our JSON file with the selected version until we reset Composer
  87. $composerBackup = $this->updateComposerJson($json, $options, $package, false);
  88. // Reload Composer config
  89. $composer = $this->app['extend.manager']->getFactory()->resetComposer();
  90. // Update our JSON file now with a contraint
  91. $this->updateComposerJson($json, $options, $package, true);
  92. // JSON file has been created/updated, if we're not installing, exit
  93. if ($options['noupdate']) {
  94. return 0;
  95. }
  96. /** @var $install \Composer\Installer */
  97. $install = Installer::create($io, $composer);
  98. try {
  99. $install
  100. ->setVerbose($options['verbose'])
  101. ->setPreferSource($options['prefersource'])
  102. ->setPreferDist($options['preferdist'])
  103. ->setDevMode(!$options['updatenodev'])
  104. ->setUpdate($options['update'])
  105. ->setUpdateWhitelist(array_keys($package))
  106. ->setWhitelistDependencies($options['updatewithdependencies'])
  107. ->setIgnorePlatformRequirements($options['ignoreplatformreqs']);
  108. $status = $install->run();
  109. if ($status !== 0) {
  110. if ($newlyCreated) {
  111. // Installation failed, deleting JSON
  112. unlink($json->getPath());
  113. } else {
  114. // Installation failed, reverting JSON to its original content
  115. file_put_contents($json->getPath(), $composerBackup);
  116. }
  117. }
  118. return $status;
  119. } catch (\Exception $e) {
  120. // Installation failed, reverting JSON to its original content
  121. file_put_contents($json->getPath(), $composerBackup);
  122. $msg = __CLASS__ . '::' . __FUNCTION__ . ' recieved an error from Composer: ' . $e->getMessage() . ' in ' . $e->getFile() . '::' . $e->getLine();
  123. $this->app['logger.system']->critical($msg, array('event' => 'exception', 'exception' => $e));
  124. throw new PackageManagerException($e->getMessage(), $e->getCode(), $e);
  125. }
  126. }
  127. /**
  128. * Update the JSON file.
  129. *
  130. * @param JsonFile $json
  131. * @param array $options
  132. * @param array $package
  133. * @param boolean $postreset
  134. *
  135. * @return string A back up of the current JSON file
  136. */
  137. private function updateComposerJson(JsonFile $json, array $options, array $package, $postreset)
  138. {
  139. $composerDefinition = $json->read();
  140. $composerBackup = file_get_contents($json->getPath());
  141. $sortPackages = $options['sortpackages'];
  142. $requireKey = $options['dev'] ? 'require-dev' : 'require';
  143. $removeKey = $options['dev'] ? 'require' : 'require-dev';
  144. $baseRequirements = array_key_exists($requireKey, $composerDefinition) ? $composerDefinition[$requireKey] : array();
  145. if (!$this->updateFileCleanly($json, $package, $requireKey, $removeKey, $sortPackages, $postreset)) {
  146. foreach ($package as $name => $version) {
  147. $baseRequirements[$name] = $version;
  148. if (isset($composerDefinition[$removeKey][$name])) {
  149. unset($composerDefinition[$removeKey][$name]);
  150. }
  151. }
  152. $composerDefinition[$requireKey] = $baseRequirements;
  153. $json->write($composerDefinition);
  154. }
  155. return $composerBackup;
  156. }
  157. /**
  158. * Cleanly update a Composer JSON file.
  159. *
  160. * @param JsonFile $json
  161. * @param array $new
  162. * @param string $requireKey
  163. * @param string $removeKey
  164. * @param boolean $sortPackages
  165. * @param boolean $postreset
  166. *
  167. * @return boolean
  168. */
  169. private function updateFileCleanly(JsonFile $json, array $new, $requireKey, $removeKey, $sortPackages, $postreset)
  170. {
  171. $contents = file_get_contents($json->getPath());
  172. $manipulator = new JsonManipulator($contents);
  173. foreach ($new as $package => $constraint) {
  174. if ($postreset) {
  175. $constraint = $this->findBestVersionForPackage($package);
  176. }
  177. if (!$manipulator->addLink($requireKey, $package, $constraint, $sortPackages)) {
  178. return false;
  179. }
  180. if (!$manipulator->removeSubNode($removeKey, $package)) {
  181. return false;
  182. }
  183. }
  184. file_put_contents($json->getPath(), $manipulator->getContents());
  185. return true;
  186. }
  187. /**
  188. * @param array $packages
  189. *
  190. * @return array
  191. */
  192. protected function formatRequirements(array $packages)
  193. {
  194. $requires = array();
  195. $packages = $this->normalizeRequirements($packages);
  196. foreach ($packages as $package) {
  197. $requires[$package['name']] = $package['version'];
  198. }
  199. return $requires;
  200. }
  201. /**
  202. * Parses a name/version pairs and returns an array of pairs.
  203. *
  204. * @param array $packages a set of package/version pairs separated by ":", "=" or " "
  205. *
  206. * @return array[] An array of arrays containing a name and (if provided) a version
  207. */
  208. protected function normalizeRequirements(array $packages)
  209. {
  210. $parser = new VersionParser();
  211. return $parser->parseNameVersionPairs($packages);
  212. }
  213. /**
  214. * Given a package name, this determines the best version to use in the require key.
  215. *
  216. * This returns a version with the ~ operator prefixed when possible.
  217. *
  218. * @param string $name
  219. *
  220. * @throws \InvalidArgumentException
  221. *
  222. * @return string
  223. */
  224. private function findBestVersionForPackage($name)
  225. {
  226. $package = $this->versionSelector->findBestCandidate($name);
  227. if (!$package) {
  228. throw new \InvalidArgumentException(
  229. sprintf(
  230. 'Could not find package %s at any version for your minimum-stability (%s). Check the package spelling or your minimum-stability',
  231. $name,
  232. $this->app['extend.manager']->getMinimumStability()
  233. )
  234. );
  235. }
  236. return $this->versionSelector->findRecommendedRequireVersion($package);
  237. }
  238. }