PageRenderTime 41ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/setup/src/Magento/Setup/Model/PackagesData.php

https://gitlab.com/crazybutterfly815/magento2
PHP | 452 lines | 278 code | 42 blank | 132 comment | 26 complexity | 1afbceb8c21f6f000f0185ea1d526645 MD5 | raw file
  1. <?php
  2. /**
  3. * Copyright © 2016 Magento. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Setup\Model;
  7. use Magento\Framework\Composer\ComposerInformation;
  8. /**
  9. * Class PackagesData returns system packages and available for update versions
  10. */
  11. class PackagesData
  12. {
  13. /**#@+
  14. * Composer command params and options
  15. */
  16. const COMPOSER_SHOW = 'show';
  17. const PARAM_COMMAND = 'command';
  18. const PARAM_PACKAGE = 'package';
  19. const PARAM_AVAILABLE = '--available';
  20. /**#@-*/
  21. /**
  22. * @var ComposerInformation
  23. */
  24. private $composerInformation;
  25. /**
  26. * @var string
  27. */
  28. protected $urlPrefix = 'https://';
  29. /**
  30. * @var array
  31. */
  32. private $packagesJson;
  33. /**
  34. * @var \Magento\Framework\Filesystem
  35. */
  36. private $filesystem;
  37. /**
  38. * @var \Magento\Setup\Model\PackagesAuth
  39. */
  40. private $packagesAuth;
  41. /**
  42. * @var \Magento\Setup\Model\DateTime\TimeZoneProvider
  43. */
  44. private $timeZoneProvider;
  45. /**
  46. * @var \Magento\Setup\Model\ObjectManagerProvider
  47. */
  48. private $objectManagerProvider;
  49. /**
  50. * @var array
  51. */
  52. private $metapackagesMap;
  53. /**
  54. * PackagesData constructor.
  55. *
  56. * @param ComposerInformation $composerInformation,
  57. * @param \Magento\Setup\Model\DateTime\TimeZoneProvider $timeZoneProvider,
  58. * @param \Magento\Setup\Model\PackagesAuth $packagesAuth,
  59. * @param \Magento\Framework\Filesystem $filesystem,
  60. * @param \Magento\Setup\Model\ObjectManagerProvider $objectManagerProvider
  61. */
  62. public function __construct(
  63. ComposerInformation $composerInformation,
  64. \Magento\Setup\Model\DateTime\TimeZoneProvider $timeZoneProvider,
  65. \Magento\Setup\Model\PackagesAuth $packagesAuth,
  66. \Magento\Framework\Filesystem $filesystem,
  67. \Magento\Setup\Model\ObjectManagerProvider $objectManagerProvider
  68. ) {
  69. $this->objectManagerProvider = $objectManagerProvider;
  70. $this->composerInformation = $composerInformation;
  71. $this->timeZoneProvider = $timeZoneProvider;
  72. $this->packagesAuth = $packagesAuth;
  73. $this->filesystem = $filesystem;
  74. }
  75. /**
  76. * @return array|bool|mixed
  77. * @throws \RuntimeException
  78. */
  79. public function syncPackagesData()
  80. {
  81. try {
  82. $lastSyncData = [];
  83. $lastSyncData['lastSyncDate'] = $this->getLastSyncDate();
  84. $lastSyncData['packages'] = $this->getPackagesForUpdate();
  85. $packagesForInstall = $this->syncPackagesForInstall();
  86. $lastSyncData = $this->formatLastSyncData($packagesForInstall, $lastSyncData);
  87. return $lastSyncData;
  88. } catch (\Exception $e) {
  89. throw new \RuntimeException($e->getMessage());
  90. }
  91. }
  92. /**
  93. * Gets last sync date
  94. *
  95. * @return string
  96. */
  97. private function getLastSyncDate()
  98. {
  99. $directory = $this->filesystem->getDirectoryRead(
  100. \Magento\Framework\App\Filesystem\DirectoryList::COMPOSER_HOME
  101. );
  102. if ($directory->isExist(PackagesAuth::PATH_TO_PACKAGES_FILE)) {
  103. $fileData = $directory->stat(PackagesAuth::PATH_TO_PACKAGES_FILE);
  104. return $fileData['mtime'];
  105. }
  106. return '';
  107. }
  108. /**
  109. * Format the lastSyncData for use on frontend
  110. *
  111. * @param array $packagesForInstall
  112. * @param array $lastSyncData
  113. * @return mixed
  114. */
  115. private function formatLastSyncData($packagesForInstall, $lastSyncData)
  116. {
  117. $lastSyncData['countOfInstall']
  118. = isset($packagesForInstall['packages']) ? count($packagesForInstall['packages']) : 0;
  119. $lastSyncData['countOfUpdate'] = isset($lastSyncData['packages']) ? count($lastSyncData['packages']) : 0;
  120. $lastSyncData['installPackages'] = $packagesForInstall['packages'];
  121. if (isset($lastSyncData['lastSyncDate'])) {
  122. $lastSyncData['lastSyncDate'] = $this->formatSyncDate($lastSyncData['lastSyncDate']);
  123. }
  124. return $lastSyncData;
  125. }
  126. /**
  127. * Format a UTC timestamp (seconds since epoch) to structure expected by frontend
  128. *
  129. * @param string $syncDate seconds since epoch
  130. * @return array
  131. */
  132. private function formatSyncDate($syncDate)
  133. {
  134. $timezone = $this->timeZoneProvider->get();
  135. return [
  136. 'date' => $timezone->formatDateTime(
  137. new \DateTime('@'.$syncDate),
  138. \IntlDateFormatter::MEDIUM,
  139. \IntlDateFormatter::NONE,
  140. null,
  141. null,
  142. 'd MMM Y'
  143. ),
  144. 'time' => $timezone->formatDateTime(
  145. new \DateTime('@'.$syncDate),
  146. \IntlDateFormatter::NONE,
  147. \IntlDateFormatter::MEDIUM,
  148. null,
  149. null,
  150. 'hh:mma'
  151. ),
  152. ];
  153. }
  154. /**
  155. * Get list of manually installed package
  156. *
  157. * @return array
  158. */
  159. public function getInstalledPackages()
  160. {
  161. return array_intersect_key(
  162. $this->composerInformation->getInstalledMagentoPackages(),
  163. $this->composerInformation->getRootPackage()->getRequires()
  164. );
  165. }
  166. /**
  167. * Get packages that need updates
  168. *
  169. * @return array
  170. */
  171. public function getPackagesForUpdate()
  172. {
  173. $packagesForUpdate = [];
  174. $packages = $this->getInstalledPackages();
  175. foreach ($packages as $package) {
  176. $latestProductVersion = $this->getLatestNonDevVersion($package['name']);
  177. if ($latestProductVersion && version_compare($latestProductVersion, $package['version'], '>')) {
  178. $availableVersions = $this->getPackageAvailableVersions($package['name']);
  179. $package['latestVersion'] = $latestProductVersion;
  180. $package['versions'] = array_filter($availableVersions, function ($version) use ($package) {
  181. return version_compare($version, $package['version'], '>');
  182. });
  183. $packagesForUpdate[$package['name']] = $package;
  184. }
  185. }
  186. return $packagesForUpdate;
  187. }
  188. /**
  189. * Retrieve the latest available stable version for a package
  190. *
  191. * @param string $package
  192. * @return string
  193. */
  194. private function getLatestNonDevVersion($package)
  195. {
  196. $versionParser = new \Composer\Package\Version\VersionParser();
  197. foreach ($this->getPackageAvailableVersions($package) as $version) {
  198. if ($versionParser->parseStability($version) != 'dev') {
  199. return $version;
  200. }
  201. }
  202. return '';
  203. }
  204. /**
  205. * Gets array of packages from packages.json
  206. *
  207. * @return array
  208. * @throws \RuntimeException
  209. */
  210. private function getPackagesJson()
  211. {
  212. if ($this->packagesJson !== null) {
  213. return $this->packagesJson;
  214. }
  215. try {
  216. $jsonData = '';
  217. $directory = $this->filesystem->getDirectoryRead(
  218. \Magento\Framework\App\Filesystem\DirectoryList::COMPOSER_HOME
  219. );
  220. if ($directory->isExist(PackagesAuth::PATH_TO_PACKAGES_FILE)) {
  221. $jsonData = $directory->readFile(PackagesAuth::PATH_TO_PACKAGES_FILE);
  222. }
  223. $packagesData = json_decode($jsonData, true);
  224. $this->packagesJson = isset($packagesData['packages']) ?
  225. $packagesData['packages'] :
  226. [];
  227. return $this->packagesJson;
  228. } catch (\Exception $e) {
  229. throw new \RuntimeException('Error in reading packages.json');
  230. }
  231. }
  232. /**
  233. * Sync packages for install
  234. *
  235. * @return array
  236. * @throws \RuntimeException
  237. */
  238. private function syncPackagesForInstall()
  239. {
  240. try {
  241. $packagesJson = $this->getPackagesJson();
  242. $packages = $this->composerInformation->getInstalledMagentoPackages();
  243. $packageNames = array_column($packages, 'name');
  244. $installPackages = [];
  245. foreach ($packagesJson as $packageName => $package) {
  246. if (!empty($package) && isset($package) && is_array($package)) {
  247. $package = $this->unsetDevVersions($package);
  248. ksort($package);
  249. $packageValues = array_values($package);
  250. if ($this->isNewUserPackage($packageValues[0], $packageNames)) {
  251. $versions = array_reverse(array_keys($package));
  252. $installPackage = $packageValues[0];
  253. $installPackage['versions'] = $versions;
  254. $installPackage['name'] = $packageName;
  255. $installPackage['vendor'] = explode('/', $packageName)[0];
  256. $installPackages[$packageName] = $installPackage;
  257. }
  258. }
  259. }
  260. $packagesForInstall['packages'] = $installPackages;
  261. return $packagesForInstall;
  262. } catch (\Exception $e) {
  263. throw new \RuntimeException('Error in syncing packages for Install');
  264. }
  265. }
  266. /**
  267. * Check if this new user package
  268. *
  269. * @param array $package
  270. * @param array $packageNames
  271. * @return bool
  272. */
  273. protected function isNewUserPackage($package, $packageNames)
  274. {
  275. if (!in_array($package['name'], $packageNames) &&
  276. in_array($package['type'], $this->composerInformation->getPackagesTypes()) &&
  277. strpos($package['name'], 'magento/product-') === false &&
  278. strpos($package['name'], 'magento/project-') === false
  279. ) {
  280. return true;
  281. }
  282. return false;
  283. }
  284. /**
  285. * Unset dev versions
  286. *
  287. * @param array $package
  288. * @return array
  289. */
  290. protected function unsetDevVersions($package)
  291. {
  292. foreach ($package as $key => $version) {
  293. if (strpos($key, 'dev') !== false) {
  294. unset($package[$key]);
  295. }
  296. }
  297. unset($version);
  298. return $package;
  299. }
  300. /**
  301. * Sync list of available for install versions for packages
  302. *
  303. * @return array
  304. * @throws \RuntimeException
  305. */
  306. public function getPackagesForInstall()
  307. {
  308. $actualInstallPackages = [];
  309. try {
  310. $installPackages = $this->syncPackagesForInstall()['packages'];
  311. $metaPackageByPackage = $this->getMetaPackageForPackage($installPackages);
  312. foreach ($installPackages as $package) {
  313. $package['metapackage'] =
  314. isset($metaPackageByPackage[$package['name']]) ? $metaPackageByPackage[$package['name']] : '';
  315. $actualInstallPackages[$package['name']] = $package;
  316. $actualInstallPackages[$package['name']]['version'] = $package['versions'][0];
  317. }
  318. $installPackagesInfo['packages'] = $actualInstallPackages;
  319. return $installPackagesInfo;
  320. } catch (\Exception $e) {
  321. throw new \RuntimeException('Error in getting new packages to install');
  322. }
  323. }
  324. /**
  325. *
  326. * @param array $packages
  327. * @return array
  328. */
  329. private function getMetaPackageForPackage($packages)
  330. {
  331. $result = [];
  332. foreach ($packages as $package) {
  333. if ($package['type'] == ComposerInformation::METAPACKAGE_PACKAGE_TYPE) {
  334. if (isset($package['require'])) {
  335. foreach ($package['require'] as $key => $requirePackage) {
  336. $result[$key] = $package['name'];
  337. }
  338. }
  339. }
  340. }
  341. unset($requirePackage);
  342. return $result;
  343. }
  344. /**
  345. * Get all metapackages
  346. *
  347. * @return array
  348. */
  349. public function getMetaPackagesMap()
  350. {
  351. if ($this->metapackagesMap === null) {
  352. $packages = $this->getPackagesJson();
  353. array_walk($packages, function ($packageVersions) {
  354. $package = array_shift($packageVersions);
  355. if ($package['type'] == ComposerInformation::METAPACKAGE_PACKAGE_TYPE
  356. && isset($package['require'])
  357. ) {
  358. foreach (array_keys($package['require']) as $key) {
  359. $this->metapackagesMap[$key] = $package['name'];
  360. }
  361. }
  362. });
  363. }
  364. return $this->metapackagesMap;
  365. }
  366. /**
  367. * Retrieve all available versions for a package
  368. *
  369. * @param string $package
  370. * @return array
  371. * @throws \RuntimeException
  372. */
  373. private function getPackageAvailableVersions($package)
  374. {
  375. $magentoRepositories = $this->composerInformation->getRootRepositories();
  376. // Check we have only one repo.magento.com repository
  377. if (
  378. count($magentoRepositories) === 1
  379. && strpos($magentoRepositories[0], $this->packagesAuth->getCredentialBaseUrl())
  380. ) {
  381. $packagesJson = $this->getPackagesJson();
  382. if (isset($packagesJson[$package])) {
  383. $packageVersions = $packagesJson[$package];
  384. uksort($packageVersions, 'version_compare');
  385. $packageVersions = array_reverse($packageVersions);
  386. return array_keys($packageVersions);
  387. }
  388. } else {
  389. $versionsPattern = '/^versions\s*\:\s(.+)$/m';
  390. $commandParams = [
  391. self::PARAM_COMMAND => self::COMPOSER_SHOW,
  392. self::PARAM_PACKAGE => $package,
  393. self::PARAM_AVAILABLE => true
  394. ];
  395. $applicationFactory = $this->objectManagerProvider->get()
  396. ->get(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class);
  397. /** @var \Magento\Composer\MagentoComposerApplication $application */
  398. $application = $applicationFactory->create();
  399. $result = $application->runComposerCommand($commandParams);
  400. $matches = [];
  401. preg_match($versionsPattern, $result, $matches);
  402. if (isset($matches[1])) {
  403. return explode(', ', $matches[1]);
  404. }
  405. }
  406. throw new \RuntimeException(
  407. sprintf('Couldn\'t get available versions for package %s', $package)
  408. );
  409. }
  410. }