Loading core/lib/Drupal/Core/Extension/Command/ExtensionInstallCommand.php +1 −1 Changes for core/lib/Drupal/Core/Extension/Command/ExtensionInstallCommand.php: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -102,7 +102,7 @@ public function __invoke( return Command::FAILURE; } // Installing modules and themes together would require interleaving two // Installing modules and themes together would require combining two // different dependency-resolution and install processes (a theme may even // depend on a module being installed first). Keep each invocation to a // single extension type rather than guessing at the correct ordering. Loading core/lib/Drupal/Core/Extension/Command/ExtensionUninstallCommand.php 0 → 100644 +231 −0 Changes for core/lib/Drupal/Core/Extension/Command/ExtensionUninstallCommand.php: 231 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Extension\Command; use Drupal\Core\Command\Exception\UserAbortException; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Core\Extension\ThemeInstallerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Uninstalls modules or themes, resolving and confirming their dependencies. * * @internal */ #[AsCommand( name: 'ex:uninstall', description: 'Uninstalls one or more modules or themes.', aliases: ['exun', 'pm:uninstall', 'pmu'], usages: ['views,views_ui', 'claro'] )] class ExtensionUninstallCommand { use StringTranslationTrait; public function __construct( protected readonly ModuleInstallerInterface $moduleInstaller, protected readonly ThemeInstallerInterface $themeInstaller, protected readonly ModuleExtensionList $moduleExtensionList, protected readonly ThemeExtensionList $themeExtensionList, protected readonly MessengerInterface $messenger, ) {} /** * Uninstalls one or more modules or themes. */ public function __invoke( InputInterface $input, OutputInterface $output, #[Argument(description: 'A comma-separated list of module or theme machine names to uninstall.')] string $extensions, #[Option(description: 'Proceed without prompting for confirmation when additional dependencies will be uninstalled.', shortcut: 'y')] bool $yes = FALSE, #[Option(description: 'Show the operations that would be performed without uninstalling anything.')] bool $dryRun = FALSE, ): int { $io = new SymfonyStyle($input, $output); // Parse the comma-delimited list of extension machine names, dropping any // empty entries left by stray or trailing commas. $requested = array_values(array_filter(explode(',', $extensions))); if (empty($requested)) { $io->error((string) $this->t('No modules or themes were specified.')); return Command::INVALID; } // Classify each requested extension as a module or a theme. The extension // lists are reset so that any extensions added directly to the filesystem // are picked up. $module_data = $this->moduleExtensionList->reset()->getList(); $theme_data = $this->themeExtensionList->reset()->getList(); $modules = []; $themes = []; $unknown = []; foreach ($requested as $name) { if (isset($module_data[$name])) { $modules[] = $name; continue; } if (isset($theme_data[$name])) { $themes[] = $name; continue; } $unknown[] = $name; } if (!empty($unknown)) { $io->error((string) $this->formatPlural( count($unknown), 'The following could not be found as a module or theme: @names.', 'The following could not be found as modules or themes: @names.', ['@names' => implode(', ', $unknown)], )); return Command::FAILURE; } // Keep the uninstall to a single extension type because it would require // combining two different dependency-resolution and uninstall processes // e.g. a theme may depend on a module being uninstalled first. if (!empty($modules) && !empty($themes)) { $io->error((string) $this->t('Cannot uninstall modules (@modules) and themes (@themes) in the same command. Run "ex:uninstall" separately for each.', [ '@modules' => implode(', ', $modules), '@themes' => implode(', ', $themes), ])); return Command::FAILURE; } $is_theme = !empty($themes); $type = $is_theme ? 'theme' : 'module'; $names = $is_theme ? $themes : $modules; $data = $is_theme ? $theme_data : $module_data; // Evaluate the full set of extensions that will be uninstalled, including // dependencies that are pulled in automatically. This validates the // request and throws on missing/incompatible/obsolete extensions. try { $resolved = $is_theme ? $this->themeInstaller->getThemesToUninstall($names) : $this->moduleInstaller->getModulesToUninstall($names); } catch (UnknownExtensionException | \InvalidArgumentException $e) { $io->error($e->getMessage()); return Command::FAILURE; } if (empty($resolved)) { $io->success((string) $this->formatPlural( count($names), 'Nothing to do: the requested @type is already uninstalled.', 'Nothing to do: the requested @types are already uninstalled.', ['@type' => $type], )); return Command::SUCCESS; } // Split the resolved set into the extensions that were explicitly // requested and the additional dependencies that will be uninstalled too. $additional = array_diff($resolved, $names); $verb = $dryRun ? $this->t('would be uninstalled') : $this->t('will be uninstalled'); $requested_labels = []; $additional_labels = []; foreach ($resolved as $machine_name) { $label = sprintf('%s (%s)', $data[$machine_name]->info['name'], $machine_name); if (in_array($machine_name, $names, TRUE)) { $requested_labels[] = $label; } else { $additional_labels[] = $label; } } $io->writeln((string) $this->formatPlural( count($requested_labels), 'The following @type @verb:', 'The following @types @verb:', ['@type' => $type, '@verb' => $verb], )); $io->listing($requested_labels); if ($additional_labels) { $io->writeln((string) $this->formatPlural( count($additional_labels), 'The following additional @type @verb as a dependency:', 'The following additional @types @verb as dependencies:', ['@type' => $type, '@verb' => $verb], )); $io->listing($additional_labels); } // Perform the same checks as the modules uninstall form, so a module that // an uninstall validator blocks is reported before anything happens. if (!$is_theme && $reasons = $this->moduleInstaller->validateUninstall($resolved)) { foreach ($reasons as $machine_name => $module_reasons) { $io->error((string) $this->t('@module cannot be uninstalled: @reasons', [ '@module' => sprintf('%s (%s)', $data[$machine_name]->info['name'], $machine_name), '@reasons' => implode('; ', $module_reasons), ])); } return Command::FAILURE; } if ($dryRun) { $io->note((string) $this->t('Dry run: no changes were made.')); return Command::SUCCESS; } // Prompt before pulling in dependencies the user did not request. In // non-interactive mode there is nobody to answer the prompt, so require // --yes to be explicit rather than silently uninstalling the dependencies. if (!empty($additional) && !$yes) { if (!$input->isInteractive()) { throw new UserAbortException(sprintf('Uninstalling the requested %s(s) requires additional dependencies. Re-run with --yes to uninstall them.', $type)); } $question = (string) $this->formatPlural( count($additional), 'Uninstall @count additional @type as a dependency?', 'Uninstall @count additional @types as dependencies?', ['@type' => $type], ); if (!$io->confirm($question, TRUE)) { throw new UserAbortException(); } } try { if ($is_theme) { $this->themeInstaller->uninstall($resolved); } else { $this->moduleInstaller->uninstall($resolved); } } catch (\Exception $e) { $io->error($e->getMessage()); return Command::FAILURE; } $io->success((string) $this->formatPlural( count($resolved), 'Successfully uninstalled @count @type.', 'Successfully uninstalled @count @types.', ['@type' => $type], )); return Command::SUCCESS; } } core/lib/Drupal/Core/Extension/ModuleInstaller.php +41 −27 Changes for core/lib/Drupal/Core/Extension/ModuleInstaller.php: 41 added lines, 27 removed lines. Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Extension\Exception\ObsoleteExtensionException; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Serialization\Yaml; use Drupal\Core\Update\Attribute\MarkFutureUpdateEquivalent; Loading Loading @@ -514,58 +515,71 @@ private function doInstall(array $module_list, array $installed_modules, bool $s /** * {@inheritdoc} */ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Get all module data so we can find dependencies and sort. public function getModulesToUninstall(array $module_list, bool $uninstall_dependents = TRUE): array { // Get all module data so we can find dependents and sort. $module_data = \Drupal::service('extension.list.module')->getList(); $sync_status = \Drupal::service('config.installer')->isSyncing(); $module_list = $module_list ? array_combine($module_list, $module_list) : []; if (array_diff_key($module_list, $module_data)) { // One or more of the given modules doesn't exist. return FALSE; if ($missing = array_diff_key($module_list, $module_data)) { throw new UnknownExtensionException(sprintf('Unable to uninstall modules %s due to unknown modules %s.', implode(', ', $module_list), implode(', ', $missing))); } $extension_config = \Drupal::configFactory()->getEditable('core.extension'); $installed_modules = $extension_config->get('module') ?: []; // Only process currently installed modules. $installed_modules = \Drupal::config('core.extension')->get('module') ?: []; if (!$module_list = array_intersect_key($module_list, $installed_modules)) { // Nothing to do. All modules already uninstalled. return TRUE; return []; } if ($uninstall_dependents) { $theme_list = \Drupal::service('extension.list.theme')->getList(); // Add dependent modules to the list. The new modules will be processed as // the foreach loop continues. foreach ($module_list as $module => $value) { // Add installed dependent modules to the list. Themes that depend on a // module are left to the uninstall validators. foreach ($module_list as $module) { foreach (array_keys($module_data[$module]->required_by) as $dependent) { if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) { // The dependent module or theme does not exist. return FALSE; throw new UnknownExtensionException("Unable to uninstall module '$module' due to unknown dependent '$dependent'."); } // Skip already uninstalled modules. if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) { if (isset($installed_modules[$dependent])) { $module_list[$dependent] = $dependent; } } } } // Use the validators and throw an exception with the reasons. if ($reasons = $this->validateUninstall($module_list)) { // Sort the module list by their weights. $module_list = array_map(fn ($module) => $module_data[$module]->sort, $module_list); asort($module_list); return array_keys($module_list); } /** * {@inheritdoc} */ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { $requested = $module_list; try { $module_list = $this->getModulesToUninstall($module_list, $uninstall_dependents); } catch (UnknownExtensionException) { // One or more of the given modules, or a dependent, doesn't exist. return FALSE; } if (empty($module_list)) { // Nothing to do. All modules already uninstalled. return TRUE; } // Use the validators and throw an exception with the reasons, reported in // the requested order followed by any added dependents. $to_validate = array_merge(array_intersect($requested, $module_list), array_diff($module_list, $requested)); if ($reasons = $this->validateUninstall($to_validate)) { foreach ($reasons as $reason) { $reason_message[] = implode(', ', $reason); } throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message)); } // Set the actual module weights. $module_list = array_map(function ($module) use ($module_data) { return $module_data[$module]->sort; }, $module_list); // Sort the module list by their weights. asort($module_list); $module_list = array_keys($module_list); $sync_status = \Drupal::service('config.installer')->isSyncing(); // Only process modules that are enabled. A module is only enabled if it is // configured as enabled. Custom or overridden module handlers might contain Loading core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php +20 −0 Changes for core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -96,6 +96,26 @@ public function getModulesToInstall(array $module_list, bool $enable_dependencie */ public function uninstall(array $module_list, $uninstall_dependents = TRUE); /** * Determines the full set of modules to uninstall for the given list. * * @param string[] $module_list * An array of module machine names. * @param bool $uninstall_dependents * (optional) If TRUE, installed modules that depend on the given modules * are added to the returned list. If FALSE, only the given modules that * are currently installed are returned. * * @return string[] * The machine names of the installed modules to uninstall, including any * dependents, ordered the way ::uninstall() would uninstall them. Modules * that are not installed are omitted. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * Thrown when a requested module, or a dependent of one, does not exist. */ public function getModulesToUninstall(array $module_list, bool $uninstall_dependents = TRUE): array; /** * Determines whether a list of modules can be uninstalled. * Loading core/lib/Drupal/Core/Extension/ThemeInstaller.php +23 −10 Changes for core/lib/Drupal/Core/Extension/ThemeInstaller.php: 23 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -212,12 +212,11 @@ public function install(array $theme_list, $install_dependencies = TRUE) { /** * {@inheritdoc} */ public function uninstall(array $theme_list) { $extension_config = $this->configFactory->getEditable('core.extension'); $theme_config = $this->configFactory->getEditable('system.theme'); public function getThemesToUninstall(array $theme_list, bool $uninstall_dependents = TRUE): array { $extension_config = $this->configFactory->get('core.extension'); $theme_config = $this->configFactory->get('system.theme'); $list = $this->themeHandler->listInfo(); $installed_themes = $extension_config->get('theme') ?: []; $theme_data = $this->themeExtensionList->reset()->getList(); $theme_list = array_combine(array_values($theme_list), array_values($theme_list)); foreach ($theme_list as $key) { if ($extension_config->get("theme.$key") === NULL) { throw new UnknownExtensionException("Unknown theme: $key."); Loading @@ -228,16 +227,30 @@ public function uninstall(array $theme_list) { if ($key === $theme_config->get('admin')) { throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled."); } // Base themes cannot be uninstalled if sub themes are installed, and if // they are not uninstalled at the same time. if (isset($list[$key]) && !empty($list[$key]->sub_themes)) { foreach ($list[$key]->sub_themes as $sub_key => $sub_label) { if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) { // Base themes cannot be uninstalled if sub themes are installed, unless // they are uninstalled at the same time. foreach (array_keys($list[$key]->sub_themes ?? []) as $sub_key) { if (isset($list[$sub_key]) && !isset($theme_list[$sub_key])) { if (!$uninstall_dependents) { throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it."); } $theme_list[$sub_key] = $sub_key; } } } return array_values($theme_list); } /** * {@inheritdoc} */ public function uninstall(array $theme_list) { // Validate the request without adding sub-themes: callers must list them // explicitly, see ::getThemesToUninstall() to resolve them beforehand. $theme_list = $this->getThemesToUninstall($theme_list, FALSE); $extension_config = $this->configFactory->getEditable('core.extension'); $installed_themes = $extension_config->get('theme') ?: []; $theme_data = $this->themeExtensionList->reset()->getList(); $this->cssCollectionOptimizer->deleteAll(); foreach ($theme_list as $key) { Loading Loading
core/lib/Drupal/Core/Extension/Command/ExtensionInstallCommand.php +1 −1 Changes for core/lib/Drupal/Core/Extension/Command/ExtensionInstallCommand.php: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -102,7 +102,7 @@ public function __invoke( return Command::FAILURE; } // Installing modules and themes together would require interleaving two // Installing modules and themes together would require combining two // different dependency-resolution and install processes (a theme may even // depend on a module being installed first). Keep each invocation to a // single extension type rather than guessing at the correct ordering. Loading
core/lib/Drupal/Core/Extension/Command/ExtensionUninstallCommand.php 0 → 100644 +231 −0 Changes for core/lib/Drupal/Core/Extension/Command/ExtensionUninstallCommand.php: 231 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Extension\Command; use Drupal\Core\Command\Exception\UserAbortException; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Core\Extension\ThemeInstallerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Uninstalls modules or themes, resolving and confirming their dependencies. * * @internal */ #[AsCommand( name: 'ex:uninstall', description: 'Uninstalls one or more modules or themes.', aliases: ['exun', 'pm:uninstall', 'pmu'], usages: ['views,views_ui', 'claro'] )] class ExtensionUninstallCommand { use StringTranslationTrait; public function __construct( protected readonly ModuleInstallerInterface $moduleInstaller, protected readonly ThemeInstallerInterface $themeInstaller, protected readonly ModuleExtensionList $moduleExtensionList, protected readonly ThemeExtensionList $themeExtensionList, protected readonly MessengerInterface $messenger, ) {} /** * Uninstalls one or more modules or themes. */ public function __invoke( InputInterface $input, OutputInterface $output, #[Argument(description: 'A comma-separated list of module or theme machine names to uninstall.')] string $extensions, #[Option(description: 'Proceed without prompting for confirmation when additional dependencies will be uninstalled.', shortcut: 'y')] bool $yes = FALSE, #[Option(description: 'Show the operations that would be performed without uninstalling anything.')] bool $dryRun = FALSE, ): int { $io = new SymfonyStyle($input, $output); // Parse the comma-delimited list of extension machine names, dropping any // empty entries left by stray or trailing commas. $requested = array_values(array_filter(explode(',', $extensions))); if (empty($requested)) { $io->error((string) $this->t('No modules or themes were specified.')); return Command::INVALID; } // Classify each requested extension as a module or a theme. The extension // lists are reset so that any extensions added directly to the filesystem // are picked up. $module_data = $this->moduleExtensionList->reset()->getList(); $theme_data = $this->themeExtensionList->reset()->getList(); $modules = []; $themes = []; $unknown = []; foreach ($requested as $name) { if (isset($module_data[$name])) { $modules[] = $name; continue; } if (isset($theme_data[$name])) { $themes[] = $name; continue; } $unknown[] = $name; } if (!empty($unknown)) { $io->error((string) $this->formatPlural( count($unknown), 'The following could not be found as a module or theme: @names.', 'The following could not be found as modules or themes: @names.', ['@names' => implode(', ', $unknown)], )); return Command::FAILURE; } // Keep the uninstall to a single extension type because it would require // combining two different dependency-resolution and uninstall processes // e.g. a theme may depend on a module being uninstalled first. if (!empty($modules) && !empty($themes)) { $io->error((string) $this->t('Cannot uninstall modules (@modules) and themes (@themes) in the same command. Run "ex:uninstall" separately for each.', [ '@modules' => implode(', ', $modules), '@themes' => implode(', ', $themes), ])); return Command::FAILURE; } $is_theme = !empty($themes); $type = $is_theme ? 'theme' : 'module'; $names = $is_theme ? $themes : $modules; $data = $is_theme ? $theme_data : $module_data; // Evaluate the full set of extensions that will be uninstalled, including // dependencies that are pulled in automatically. This validates the // request and throws on missing/incompatible/obsolete extensions. try { $resolved = $is_theme ? $this->themeInstaller->getThemesToUninstall($names) : $this->moduleInstaller->getModulesToUninstall($names); } catch (UnknownExtensionException | \InvalidArgumentException $e) { $io->error($e->getMessage()); return Command::FAILURE; } if (empty($resolved)) { $io->success((string) $this->formatPlural( count($names), 'Nothing to do: the requested @type is already uninstalled.', 'Nothing to do: the requested @types are already uninstalled.', ['@type' => $type], )); return Command::SUCCESS; } // Split the resolved set into the extensions that were explicitly // requested and the additional dependencies that will be uninstalled too. $additional = array_diff($resolved, $names); $verb = $dryRun ? $this->t('would be uninstalled') : $this->t('will be uninstalled'); $requested_labels = []; $additional_labels = []; foreach ($resolved as $machine_name) { $label = sprintf('%s (%s)', $data[$machine_name]->info['name'], $machine_name); if (in_array($machine_name, $names, TRUE)) { $requested_labels[] = $label; } else { $additional_labels[] = $label; } } $io->writeln((string) $this->formatPlural( count($requested_labels), 'The following @type @verb:', 'The following @types @verb:', ['@type' => $type, '@verb' => $verb], )); $io->listing($requested_labels); if ($additional_labels) { $io->writeln((string) $this->formatPlural( count($additional_labels), 'The following additional @type @verb as a dependency:', 'The following additional @types @verb as dependencies:', ['@type' => $type, '@verb' => $verb], )); $io->listing($additional_labels); } // Perform the same checks as the modules uninstall form, so a module that // an uninstall validator blocks is reported before anything happens. if (!$is_theme && $reasons = $this->moduleInstaller->validateUninstall($resolved)) { foreach ($reasons as $machine_name => $module_reasons) { $io->error((string) $this->t('@module cannot be uninstalled: @reasons', [ '@module' => sprintf('%s (%s)', $data[$machine_name]->info['name'], $machine_name), '@reasons' => implode('; ', $module_reasons), ])); } return Command::FAILURE; } if ($dryRun) { $io->note((string) $this->t('Dry run: no changes were made.')); return Command::SUCCESS; } // Prompt before pulling in dependencies the user did not request. In // non-interactive mode there is nobody to answer the prompt, so require // --yes to be explicit rather than silently uninstalling the dependencies. if (!empty($additional) && !$yes) { if (!$input->isInteractive()) { throw new UserAbortException(sprintf('Uninstalling the requested %s(s) requires additional dependencies. Re-run with --yes to uninstall them.', $type)); } $question = (string) $this->formatPlural( count($additional), 'Uninstall @count additional @type as a dependency?', 'Uninstall @count additional @types as dependencies?', ['@type' => $type], ); if (!$io->confirm($question, TRUE)) { throw new UserAbortException(); } } try { if ($is_theme) { $this->themeInstaller->uninstall($resolved); } else { $this->moduleInstaller->uninstall($resolved); } } catch (\Exception $e) { $io->error($e->getMessage()); return Command::FAILURE; } $io->success((string) $this->formatPlural( count($resolved), 'Successfully uninstalled @count @type.', 'Successfully uninstalled @count @types.', ['@type' => $type], )); return Command::SUCCESS; } }
core/lib/Drupal/Core/Extension/ModuleInstaller.php +41 −27 Changes for core/lib/Drupal/Core/Extension/ModuleInstaller.php: 41 added lines, 27 removed lines. Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Extension\Exception\ObsoleteExtensionException; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Serialization\Yaml; use Drupal\Core\Update\Attribute\MarkFutureUpdateEquivalent; Loading Loading @@ -514,58 +515,71 @@ private function doInstall(array $module_list, array $installed_modules, bool $s /** * {@inheritdoc} */ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Get all module data so we can find dependencies and sort. public function getModulesToUninstall(array $module_list, bool $uninstall_dependents = TRUE): array { // Get all module data so we can find dependents and sort. $module_data = \Drupal::service('extension.list.module')->getList(); $sync_status = \Drupal::service('config.installer')->isSyncing(); $module_list = $module_list ? array_combine($module_list, $module_list) : []; if (array_diff_key($module_list, $module_data)) { // One or more of the given modules doesn't exist. return FALSE; if ($missing = array_diff_key($module_list, $module_data)) { throw new UnknownExtensionException(sprintf('Unable to uninstall modules %s due to unknown modules %s.', implode(', ', $module_list), implode(', ', $missing))); } $extension_config = \Drupal::configFactory()->getEditable('core.extension'); $installed_modules = $extension_config->get('module') ?: []; // Only process currently installed modules. $installed_modules = \Drupal::config('core.extension')->get('module') ?: []; if (!$module_list = array_intersect_key($module_list, $installed_modules)) { // Nothing to do. All modules already uninstalled. return TRUE; return []; } if ($uninstall_dependents) { $theme_list = \Drupal::service('extension.list.theme')->getList(); // Add dependent modules to the list. The new modules will be processed as // the foreach loop continues. foreach ($module_list as $module => $value) { // Add installed dependent modules to the list. Themes that depend on a // module are left to the uninstall validators. foreach ($module_list as $module) { foreach (array_keys($module_data[$module]->required_by) as $dependent) { if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) { // The dependent module or theme does not exist. return FALSE; throw new UnknownExtensionException("Unable to uninstall module '$module' due to unknown dependent '$dependent'."); } // Skip already uninstalled modules. if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) { if (isset($installed_modules[$dependent])) { $module_list[$dependent] = $dependent; } } } } // Use the validators and throw an exception with the reasons. if ($reasons = $this->validateUninstall($module_list)) { // Sort the module list by their weights. $module_list = array_map(fn ($module) => $module_data[$module]->sort, $module_list); asort($module_list); return array_keys($module_list); } /** * {@inheritdoc} */ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { $requested = $module_list; try { $module_list = $this->getModulesToUninstall($module_list, $uninstall_dependents); } catch (UnknownExtensionException) { // One or more of the given modules, or a dependent, doesn't exist. return FALSE; } if (empty($module_list)) { // Nothing to do. All modules already uninstalled. return TRUE; } // Use the validators and throw an exception with the reasons, reported in // the requested order followed by any added dependents. $to_validate = array_merge(array_intersect($requested, $module_list), array_diff($module_list, $requested)); if ($reasons = $this->validateUninstall($to_validate)) { foreach ($reasons as $reason) { $reason_message[] = implode(', ', $reason); } throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message)); } // Set the actual module weights. $module_list = array_map(function ($module) use ($module_data) { return $module_data[$module]->sort; }, $module_list); // Sort the module list by their weights. asort($module_list); $module_list = array_keys($module_list); $sync_status = \Drupal::service('config.installer')->isSyncing(); // Only process modules that are enabled. A module is only enabled if it is // configured as enabled. Custom or overridden module handlers might contain Loading
core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php +20 −0 Changes for core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -96,6 +96,26 @@ public function getModulesToInstall(array $module_list, bool $enable_dependencie */ public function uninstall(array $module_list, $uninstall_dependents = TRUE); /** * Determines the full set of modules to uninstall for the given list. * * @param string[] $module_list * An array of module machine names. * @param bool $uninstall_dependents * (optional) If TRUE, installed modules that depend on the given modules * are added to the returned list. If FALSE, only the given modules that * are currently installed are returned. * * @return string[] * The machine names of the installed modules to uninstall, including any * dependents, ordered the way ::uninstall() would uninstall them. Modules * that are not installed are omitted. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * Thrown when a requested module, or a dependent of one, does not exist. */ public function getModulesToUninstall(array $module_list, bool $uninstall_dependents = TRUE): array; /** * Determines whether a list of modules can be uninstalled. * Loading
core/lib/Drupal/Core/Extension/ThemeInstaller.php +23 −10 Changes for core/lib/Drupal/Core/Extension/ThemeInstaller.php: 23 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -212,12 +212,11 @@ public function install(array $theme_list, $install_dependencies = TRUE) { /** * {@inheritdoc} */ public function uninstall(array $theme_list) { $extension_config = $this->configFactory->getEditable('core.extension'); $theme_config = $this->configFactory->getEditable('system.theme'); public function getThemesToUninstall(array $theme_list, bool $uninstall_dependents = TRUE): array { $extension_config = $this->configFactory->get('core.extension'); $theme_config = $this->configFactory->get('system.theme'); $list = $this->themeHandler->listInfo(); $installed_themes = $extension_config->get('theme') ?: []; $theme_data = $this->themeExtensionList->reset()->getList(); $theme_list = array_combine(array_values($theme_list), array_values($theme_list)); foreach ($theme_list as $key) { if ($extension_config->get("theme.$key") === NULL) { throw new UnknownExtensionException("Unknown theme: $key."); Loading @@ -228,16 +227,30 @@ public function uninstall(array $theme_list) { if ($key === $theme_config->get('admin')) { throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled."); } // Base themes cannot be uninstalled if sub themes are installed, and if // they are not uninstalled at the same time. if (isset($list[$key]) && !empty($list[$key]->sub_themes)) { foreach ($list[$key]->sub_themes as $sub_key => $sub_label) { if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) { // Base themes cannot be uninstalled if sub themes are installed, unless // they are uninstalled at the same time. foreach (array_keys($list[$key]->sub_themes ?? []) as $sub_key) { if (isset($list[$sub_key]) && !isset($theme_list[$sub_key])) { if (!$uninstall_dependents) { throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it."); } $theme_list[$sub_key] = $sub_key; } } } return array_values($theme_list); } /** * {@inheritdoc} */ public function uninstall(array $theme_list) { // Validate the request without adding sub-themes: callers must list them // explicitly, see ::getThemesToUninstall() to resolve them beforehand. $theme_list = $this->getThemesToUninstall($theme_list, FALSE); $extension_config = $this->configFactory->getEditable('core.extension'); $installed_themes = $extension_config->get('theme') ?: []; $theme_data = $this->themeExtensionList->reset()->getList(); $this->cssCollectionOptimizer->deleteAll(); foreach ($theme_list as $key) { Loading