ThemeUninstallCommand.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Theme\Console\Command;
  7. use Magento\Framework\App\Cache;
  8. use Magento\Framework\App\Console\MaintenanceModeEnabler;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\Framework\App\MaintenanceMode;
  11. use Magento\Framework\App\State\CleanupFiles;
  12. use Magento\Framework\Composer\ComposerInformation;
  13. use Magento\Framework\Composer\DependencyChecker;
  14. use Magento\Theme\Model\Theme\Data\Collection;
  15. use Magento\Theme\Model\Theme\ThemePackageInfo;
  16. use Magento\Theme\Model\Theme\ThemeUninstaller;
  17. use Magento\Theme\Model\Theme\ThemeDependencyChecker;
  18. use Symfony\Component\Console\Command\Command;
  19. use Symfony\Component\Console\Input\InputInterface;
  20. use Symfony\Component\Console\Output\OutputInterface;
  21. use Symfony\Component\Console\Input\InputOption;
  22. use Symfony\Component\Console\Input\InputArgument;
  23. use Magento\Framework\Setup\BackupRollbackFactory;
  24. use Magento\Theme\Model\ThemeValidator;
  25. /**
  26. * Command for uninstalling theme and backup-code feature
  27. *
  28. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  29. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  30. */
  31. class ThemeUninstallCommand extends Command
  32. {
  33. /**
  34. * Names of input arguments or options
  35. */
  36. const INPUT_KEY_BACKUP_CODE = 'backup-code';
  37. const INPUT_KEY_THEMES = 'theme';
  38. const INPUT_KEY_CLEAR_STATIC_CONTENT = 'clear-static-content';
  39. /**
  40. * Composer general dependency checker
  41. *
  42. * @var DependencyChecker
  43. */
  44. private $dependencyChecker;
  45. /**
  46. * Root composer.json information
  47. *
  48. * @var ComposerInformation
  49. */
  50. private $composer;
  51. /**
  52. * Theme collection in filesystem
  53. *
  54. * @var Collection
  55. */
  56. private $themeCollection;
  57. /**
  58. * System cache model
  59. *
  60. * @var Cache
  61. */
  62. private $cache;
  63. /**
  64. * Cleaning up application state service
  65. *
  66. * @var CleanupFiles
  67. */
  68. private $cleanupFiles;
  69. /**
  70. * BackupRollback factory
  71. *
  72. * @var BackupRollbackFactory
  73. */
  74. private $backupRollbackFactory;
  75. /**
  76. * Theme Validator
  77. *
  78. * @var ThemeValidator
  79. */
  80. private $themeValidator;
  81. /**
  82. * Package name finder
  83. *
  84. * @var ThemePackageInfo
  85. */
  86. private $themePackageInfo;
  87. /**
  88. * Theme Uninstaller
  89. *
  90. * @var ThemeUninstaller
  91. */
  92. private $themeUninstaller;
  93. /**
  94. * Theme Dependency Checker
  95. *
  96. * @var ThemeDependencyChecker
  97. */
  98. private $themeDependencyChecker;
  99. /**
  100. * @var MaintenanceModeEnabler
  101. */
  102. private $maintenanceModeEnabler;
  103. /**
  104. * Constructor
  105. *
  106. * @param Cache $cache
  107. * @param CleanupFiles $cleanupFiles
  108. * @param ComposerInformation $composer
  109. * @param MaintenanceMode $maintenanceMode deprecated, use $maintenanceModeEnabler instead
  110. * @param DependencyChecker $dependencyChecker
  111. * @param Collection $themeCollection
  112. * @param BackupRollbackFactory $backupRollbackFactory
  113. * @param ThemeValidator $themeValidator
  114. * @param ThemePackageInfo $themePackageInfo
  115. * @param ThemeUninstaller $themeUninstaller
  116. * @param ThemeDependencyChecker $themeDependencyChecker
  117. * @param MaintenanceModeEnabler $maintenanceModeEnabler
  118. *
  119. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  120. */
  121. public function __construct(
  122. Cache $cache,
  123. CleanupFiles $cleanupFiles,
  124. ComposerInformation $composer,
  125. MaintenanceMode $maintenanceMode,
  126. DependencyChecker $dependencyChecker,
  127. Collection $themeCollection,
  128. BackupRollbackFactory $backupRollbackFactory,
  129. ThemeValidator $themeValidator,
  130. ThemePackageInfo $themePackageInfo,
  131. ThemeUninstaller $themeUninstaller,
  132. ThemeDependencyChecker $themeDependencyChecker,
  133. MaintenanceModeEnabler $maintenanceModeEnabler = null
  134. ) {
  135. $this->cache = $cache;
  136. $this->cleanupFiles = $cleanupFiles;
  137. $this->composer = $composer;
  138. $this->dependencyChecker = $dependencyChecker;
  139. $this->themeCollection = $themeCollection;
  140. $this->backupRollbackFactory = $backupRollbackFactory;
  141. $this->themeValidator = $themeValidator;
  142. $this->themePackageInfo = $themePackageInfo;
  143. $this->themeUninstaller = $themeUninstaller;
  144. $this->themeDependencyChecker = $themeDependencyChecker;
  145. $this->maintenanceModeEnabler =
  146. $maintenanceModeEnabler ?: ObjectManager::getInstance()->get(MaintenanceModeEnabler::class);
  147. parent::__construct();
  148. }
  149. /**
  150. * {@inheritdoc}
  151. */
  152. protected function configure()
  153. {
  154. $this->setName('theme:uninstall');
  155. $this->setDescription('Uninstalls theme');
  156. $this->addOption(
  157. self::INPUT_KEY_BACKUP_CODE,
  158. null,
  159. InputOption::VALUE_NONE,
  160. 'Take code backup (excluding temporary files)'
  161. );
  162. $this->addArgument(
  163. self::INPUT_KEY_THEMES,
  164. InputArgument::IS_ARRAY | InputArgument::REQUIRED,
  165. 'Path of the theme. Theme path should be specified as full path which is area/vendor/name.'
  166. . ' For example, frontend/Magento/blank'
  167. );
  168. $this->addOption(
  169. self::INPUT_KEY_CLEAR_STATIC_CONTENT,
  170. 'c',
  171. InputOption::VALUE_NONE,
  172. 'Clear generated static view files.'
  173. );
  174. parent::configure();
  175. }
  176. /**
  177. * {@inheritdoc}
  178. */
  179. protected function execute(InputInterface $input, OutputInterface $output)
  180. {
  181. $messages = [];
  182. $themePaths = $input->getArgument(self::INPUT_KEY_THEMES);
  183. $messages = array_merge($messages, $this->validate($themePaths));
  184. if (!empty($messages)) {
  185. $output->writeln($messages);
  186. // we must have an exit code higher than zero to indicate something was wrong
  187. return \Magento\Framework\Console\Cli::RETURN_FAILURE;
  188. }
  189. $messages = array_merge(
  190. $messages,
  191. $this->themeValidator->validateIsThemeInUse($themePaths),
  192. $this->themeDependencyChecker->checkChildTheme($themePaths),
  193. $this->checkDependencies($themePaths)
  194. );
  195. if (!empty($messages)) {
  196. $output->writeln(
  197. '<error>Unable to uninstall. Please resolve the following issues:</error>'
  198. . PHP_EOL . implode(PHP_EOL, $messages)
  199. );
  200. // we must have an exit code higher than zero to indicate something was wrong
  201. return \Magento\Framework\Console\Cli::RETURN_FAILURE;
  202. }
  203. $result = $this->maintenanceModeEnabler->executeInMaintenanceMode(
  204. function () use ($input, $output, $themePaths) {
  205. try {
  206. if ($input->getOption(self::INPUT_KEY_BACKUP_CODE)) {
  207. $time = time();
  208. $codeBackup = $this->backupRollbackFactory->create($output);
  209. $codeBackup->codeBackup($time);
  210. }
  211. $this->themeUninstaller->uninstallRegistry($output, $themePaths);
  212. $this->themeUninstaller->uninstallCode($output, $themePaths);
  213. $this->cleanup($input, $output);
  214. return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
  215. } catch (\Exception $e) {
  216. $output->writeln('<error>' . $e->getMessage() . '</error>');
  217. $output->writeln('<error>Please disable maintenance mode after you resolved above issues</error>');
  218. // we must have an exit code higher than zero to indicate something was wrong
  219. return \Magento\Framework\Console\Cli::RETURN_FAILURE;
  220. }
  221. },
  222. $output,
  223. true
  224. );
  225. return $result;
  226. }
  227. /**
  228. * Validate given full theme paths
  229. *
  230. * @param string[] $themePaths
  231. * @return string[]
  232. */
  233. private function validate($themePaths)
  234. {
  235. $messages = [];
  236. $incorrectThemes = $this->getIncorrectThemes($themePaths);
  237. if (!empty($incorrectThemes)) {
  238. $text = 'Theme path should be specified as full path which is area/vendor/name.';
  239. $messages[] = '<error>Incorrect theme(s) format: ' . implode(', ', $incorrectThemes)
  240. . '. ' . $text . '</error>';
  241. return $messages;
  242. }
  243. $unknownPackages = $this->getUnknownPackages($themePaths);
  244. $unknownThemes = $this->getUnknownThemes($themePaths);
  245. $unknownPackages = array_diff($unknownPackages, $unknownThemes);
  246. if (!empty($unknownPackages)) {
  247. $text = count($unknownPackages) > 1 ?
  248. ' are not installed Composer packages' : ' is not an installed Composer package';
  249. $messages[] = '<error>' . implode(', ', $unknownPackages) . $text . '</error>';
  250. }
  251. if (!empty($unknownThemes)) {
  252. $messages[] = '<error>Unknown theme(s): ' . implode(', ', $unknownThemes) . '</error>';
  253. }
  254. return $messages;
  255. }
  256. /**
  257. * Retrieve list of themes with wrong name format
  258. *
  259. * @param string[] $themePaths
  260. * @return string[]
  261. */
  262. protected function getIncorrectThemes($themePaths)
  263. {
  264. $result = [];
  265. foreach ($themePaths as $themePath) {
  266. if (!preg_match('/^[^\/]+\/[^\/]+\/[^\/]+$/', $themePath)) {
  267. $result[] = $themePath;
  268. continue;
  269. }
  270. }
  271. return $result;
  272. }
  273. /**
  274. * Retrieve list of unknown packages
  275. *
  276. * @param string[] $themePaths
  277. * @return string[]
  278. */
  279. protected function getUnknownPackages($themePaths)
  280. {
  281. $installedPackages = $this->composer->getRootRequiredPackages();
  282. $result = [];
  283. foreach ($themePaths as $themePath) {
  284. if (array_search($this->themePackageInfo->getPackageName($themePath), $installedPackages) === false) {
  285. $result[] = $themePath;
  286. }
  287. }
  288. return $result;
  289. }
  290. /**
  291. * Retrieve list of unknown themes
  292. *
  293. * @param string[] $themePaths
  294. * @return string[]
  295. */
  296. protected function getUnknownThemes($themePaths)
  297. {
  298. $result = [];
  299. foreach ($themePaths as $themePath) {
  300. if (!$this->themeCollection->hasTheme($this->themeCollection->getThemeByFullPath($themePath))) {
  301. $result[] = $themePath;
  302. }
  303. }
  304. return $result;
  305. }
  306. /**
  307. * Check dependencies to given full theme paths
  308. *
  309. * @param string[] $themePaths
  310. * @return string[]
  311. */
  312. private function checkDependencies($themePaths)
  313. {
  314. $messages = [];
  315. $packageToPath = [];
  316. foreach ($themePaths as $themePath) {
  317. $packageToPath[$this->themePackageInfo->getPackageName($themePath)] = $themePath;
  318. }
  319. $dependencies = $this->dependencyChecker->checkDependencies(array_keys($packageToPath), true);
  320. foreach ($dependencies as $package => $dependingPackages) {
  321. if (!empty($dependingPackages)) {
  322. $messages[] =
  323. '<error>' . $packageToPath[$package] .
  324. " has the following dependent package(s):</error>" .
  325. PHP_EOL . "\t<error>" . implode('</error>' . PHP_EOL . "\t<error>", $dependingPackages)
  326. . "</error>";
  327. }
  328. }
  329. return $messages;
  330. }
  331. /**
  332. * Cleanup after updated modules status
  333. *
  334. * @param InputInterface $input
  335. * @param OutputInterface $output
  336. * @return void
  337. */
  338. private function cleanup(InputInterface $input, OutputInterface $output)
  339. {
  340. $this->cache->clean();
  341. $output->writeln('<info>Cache cleared successfully.</info>');
  342. if ($input->getOption(self::INPUT_KEY_CLEAR_STATIC_CONTENT)) {
  343. $this->cleanupFiles->clearMaterializedViewFiles();
  344. $output->writeln('<info>Generated static view files cleared successfully.</info>');
  345. } else {
  346. $output->writeln(
  347. '<error>Alert: Generated static view files were not cleared.'
  348. . ' You can clear them using the --' . self::INPUT_KEY_CLEAR_STATIC_CONTENT . ' option.'
  349. . ' Failure to clear static view files might cause display issues in the Admin and storefront.</error>'
  350. );
  351. }
  352. }
  353. }