Skip to content

Commit

Permalink
Issue #474684 by bnjmnm, dawehner, tedbow, pfrenssen, JohnAlbin, adem…
Browse files Browse the repository at this point in the history
…arco, kalpaitch, vdacosta@voidtek.com, rensingh99, markcarver, jungle, jhedstrom, RobLoach, almaudoh, kevineinarsson, shaal, dpagini, thedavidmeister, sreynen, Snugug, Miguel.kode, kamkejj, alexpott, Pol, sun, Wim Leers, lauriii, tim.plunkett, eaton: Allow themes to declare dependencies on modules
  • Loading branch information
catch committed Mar 19, 2020
1 parent 353773b commit 6e9d3de
Show file tree
Hide file tree
Showing 31 changed files with 1,119 additions and 67 deletions.
10 changes: 8 additions & 2 deletions core.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ services:
class: Drupal\Core\Extension\ModuleInstaller
tags:
- { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
arguments: ['@app.root', '@module_handler', '@kernel']
arguments: ['@app.root', '@module_handler', '@kernel', '@extension.list.theme']
lazy: true
extension.list.module:
class: Drupal\Core\Extension\ModuleExtensionList
Expand All @@ -552,12 +552,18 @@ services:
- { name: module_install.uninstall_validator }
arguments: ['@string_translation', '@extension.list.module']
lazy: true
module_required_by_themes_uninstall_validator:
class: Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme']
lazy: true
theme_handler:
class: Drupal\Core\Extension\ThemeHandler
arguments: ['@app.root', '@config.factory', '@extension.list.theme']
theme_installer:
class: Drupal\Core\Extension\ThemeInstaller
arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state']
arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@extension.list.module']
# @deprecated in Drupal 8.0.x and will be removed before 9.0.0. Use the other
# entity* services instead.
entity.manager:
Expand Down
23 changes: 20 additions & 3 deletions lib/Drupal/Core/Extension/ModuleInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
*/
protected $uninstallValidators;

/**
* The theme extension list.
*
* @var \Drupal\Core\Extension\ThemeExtensionList
*/
protected $themeExtensionList;

/**
* Constructs a new ModuleInstaller instance.
*
Expand All @@ -59,14 +66,21 @@ class ModuleInstaller implements ModuleInstallerInterface {
* The module handler.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The drupal kernel.
* @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
* The theme extension list.
*
* @see \Drupal\Core\DrupalKernel
* @see \Drupal\Core\CoreServiceProvider
*/
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, ThemeExtensionList $extension_list_theme = NULL) {
$this->root = $root;
$this->moduleHandler = $module_handler;
$this->kernel = $kernel;
if (is_null($extension_list_theme)) {
@trigger_error('The extension.list.theme service must be passed to ' . __NAMESPACE__ . '\ModuleInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
$extension_list_theme = \Drupal::service('extension.list.theme');
}
$this->themeExtensionList = $extension_list_theme;
}

/**
Expand Down Expand Up @@ -372,12 +386,14 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
}

if ($uninstall_dependents) {
$theme_list = $this->themeExtensionList->getList();

// Add dependent modules to the list. The new modules will be processed as
// the foreach loop continues.
foreach ($module_list as $module => $value) {
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
if (!isset($module_data[$dependent])) {
// The dependent module does not exist.
if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) {
// The dependent module or theme does not exist.
return FALSE;
}

Expand Down Expand Up @@ -578,6 +594,7 @@ protected function updateKernel($module_filenames) {
// After rebuilding the container we need to update the injected
// dependencies.
$container = $this->kernel->getContainer();
$this->themeExtensionList = $container->get('extension.list.theme');
$this->moduleHandler = $container->get('module_handler');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Drupal\Core\Extension;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;

/**
* Ensures modules cannot be uninstalled if enabled themes depend on them.
*/
class ModuleRequiredByThemesUninstallValidator implements ModuleUninstallValidatorInterface {

use StringTranslationTrait;

/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;

/**
* The theme extension list.
*
* @var \Drupal\Core\Extension\ThemeExtensionList
*/
protected $themeExtensionList;

/**
* Constructs a new ModuleRequiredByThemesUninstallValidator.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
* The module extension list.
* @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
* The theme extension list.
*/
public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, ThemeExtensionList $extension_list_theme) {
$this->stringTranslation = $string_translation;
$this->moduleExtensionList = $extension_list_module;
$this->themeExtensionList = $extension_list_theme;
}

/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];

$themes_depending_on_module = $this->getThemesDependingOnModule($module);
if (!empty($themes_depending_on_module)) {
$module_name = $this->moduleExtensionList->get($module)->info['name'];
$theme_names = implode(', ', $themes_depending_on_module);
$reasons[] = $this->formatPlural(count($themes_depending_on_module),
'Required by the theme: @theme_names',
'Required by the themes: @theme_names',
['@module_name' => $module_name, '@theme_names' => $theme_names]);
}

return $reasons;
}

/**
* Returns themes that depend on a module.
*
* @param string $module
* The module machine name.
*
* @return string[]
* An array of the names of themes that depend on $module.
*/
protected function getThemesDependingOnModule($module) {
$installed_themes = $this->themeExtensionList->getAllInstalledInfo();
$themes_depending_on_module = array_map(function ($theme) use ($module) {
if (in_array($module, $theme['dependencies'])) {
return $theme['name'];
}
}, $installed_themes);

return array_filter($themes_depending_on_module);
}

}
17 changes: 17 additions & 0 deletions lib/Drupal/Core/Extension/ThemeExtensionList.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ThemeExtensionList extends ExtensionList {
'libraries' => [],
'libraries_extend' => [],
'libraries_override' => [],
'dependencies' => [],
];

/**
Expand Down Expand Up @@ -140,6 +141,22 @@ protected function doList() {
// sub-themes.
$this->fillInSubThemeData($themes, $sub_themes);

foreach ($themes as $key => $theme) {
// After $theme is processed by buildModuleDependencies(), there can be a
// `$theme->requires` array containing both module and base theme
// dependencies. The module dependencies are copied to their own property
// so they are available to operations specific to module dependencies.
if (isset($theme->requires)) {
$theme->module_dependencies = array_diff_key($theme->requires, $themes);
}
else {
// Even if no requirements are specified, the theme installation process
// expects the presence of the `requires` and `module_dependencies`
// properties, so they should be initialized here as empty arrays.
$theme->requires = [];
$theme->module_dependencies = [];
}
}
return $themes;
}

Expand Down
60 changes: 52 additions & 8 deletions lib/Drupal/Core/Extension/ThemeInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Drupal\Core\Extension;

use Drupal\Component\Utility\Html;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
Expand All @@ -10,13 +11,18 @@
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\system\ModuleDependencyMessageTrait;
use Psr\Log\LoggerInterface;

/**
* Manages theme installation/uninstallation.
*/
class ThemeInstaller implements ThemeInstallerInterface {

use ModuleDependencyMessageTrait;
use StringTranslationTrait;

/**
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
Expand Down Expand Up @@ -62,6 +68,13 @@ class ThemeInstaller implements ThemeInstallerInterface {
*/
protected $logger;

/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;

/**
* Constructs a new ThemeInstaller.
*
Expand All @@ -86,8 +99,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
* A logger instance.
* @param \Drupal\Core\State\StateInterface $state
* The state store.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
* The module extension list.
*/
public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state, ModuleExtensionList $module_extension_list = NULL) {
$this->themeHandler = $theme_handler;
$this->configFactory = $config_factory;
$this->configInstaller = $config_installer;
Expand All @@ -97,6 +112,11 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI
$this->routeBuilder = $route_builder;
$this->logger = $logger;
$this->state = $state;
if ($module_extension_list === NULL) {
@trigger_error('The extension.list.module service must be passed to ' . __NAMESPACE__ . '\ThemeInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
$this->moduleExtensionList = $module_extension_list;
}

/**
Expand All @@ -106,6 +126,8 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
$extension_config = $this->configFactory->getEditable('core.extension');

$theme_data = $this->themeHandler->rebuildThemeData();
$installed_themes = $extension_config->get('theme') ?: [];
$installed_modules = $extension_config->get('module') ?: [];

if ($install_dependencies) {
$theme_list = array_combine($theme_list, $theme_list);
Expand All @@ -116,16 +138,41 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
}

// Only process themes that are not installed currently.
$installed_themes = $extension_config->get('theme') ?: [];
if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
// Nothing to do. All themes already installed.
return TRUE;
}

$module_list = $this->moduleExtensionList->getList();
foreach ($theme_list as $theme => $value) {
// Add dependencies to the list. The new themes will be processed as
// the parent foreach loop continues.
foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
$module_dependencies = $theme_data[$theme]->module_dependencies;
// $theme_data[$theme]->requires contains both theme and module
// dependencies keyed by the extension machine names and
// $theme_data[$theme]->module_dependencies contains only modules keyed
// by the module extension machine name. Therefore we can find the theme
// dependencies by finding array keys for 'requires' that are not in
// $module_dependencies.
$theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
// We can find the unmet module dependencies by finding the module
// machine names keys that are not in $installed_modules keys.
$unmet_module_dependencies = array_diff_key($module_dependencies, $installed_modules);

// Prevent themes with unmet module dependencies from being installed.
if (!empty($unmet_module_dependencies)) {
$unmet_module_dependencies_list = implode(', ', array_keys($unmet_module_dependencies));
throw new MissingDependencyException("Unable to install theme: '$theme' due to unmet module dependencies: '$unmet_module_dependencies_list'.");
}

foreach ($module_dependencies as $dependency => $dependency_object) {
if ($incompatible = $this->checkDependencyMessage($module_list, $dependency, $dependency_object)) {
$sanitized_message = Html::decodeEntities(strip_tags($incompatible));
throw new MissingDependencyException("Unable to install theme: $sanitized_message");
}
}

// Add dependencies to the list of themes to install. The new themes
// will be processed as the parent foreach loop continues.
foreach (array_keys($theme_dependencies) as $dependency) {
if (!isset($theme_data[$dependency])) {
// The dependency does not exist.
return FALSE;
Expand All @@ -147,9 +194,6 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
arsort($theme_list);
$theme_list = array_keys($theme_list);
}
else {
$installed_themes = $extension_config->get('theme') ?: [];
}

$themes_installed = [];
foreach ($theme_list as $key) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Drupal/Core/Extension/ThemeInstallerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ interface ThemeInstallerInterface {
*
* @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
* Thrown when the theme does not exist.
*
* @throws \Drupal\Core\Extension\MissingDependencyException
* Thrown when a requested dependency can't be found.
*/
public function install(array $theme_list, $install_dependencies = TRUE);

Expand Down

0 comments on commit 6e9d3de

Please sign in to comment.