diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 3363efc..8231668 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -272,6 +272,7 @@ public function rebuildThemeData() { 'screenshot' => 'screenshot.png', 'php' => DRUPAL_MINIMUM_PHP, 'libraries' => array(), + 'dependencies' => array(), ); $sub_themes = array(); @@ -358,6 +359,17 @@ public function rebuildThemeData() { } } + foreach ($themes as $key => $theme) { + // buildModuleDependencies() adds a theme->requires array that contains + // both module and base theme dependencies, if they are specified. Ensure + // that every theme stores the list of module dependencies separately + // from the full requires list. + if (!isset($theme->requires)) { + $theme->requires = []; + } + $themes[$key]->module_dependencies = isset($theme->base_themes) ? array_diff_key($theme->requires, $theme->base_themes) : $theme->requires; + } + return $themes; } diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php index 99de386..e12ff0b 100644 --- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php +++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php @@ -7,6 +7,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigInstallerInterface; use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Routing\RouteBuilderInterface; use Drupal\Core\State\StateInterface; use Psr\Log\LoggerInterface; @@ -56,6 +57,11 @@ class ThemeInstaller implements ThemeInstallerInterface { */ protected $logger; + /** + * @var \Drupal\Core\Extension\ModuleInstallerInterface|NULL + */ + protected $moduleInstaller; + /** * Constructs a new ThemeInstaller. @@ -81,8 +87,10 @@ class ThemeInstaller implements ThemeInstallerInterface { * A logger instance. * @param \Drupal\Core\State\StateInterface $state * The state store. + * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer + * The module installer. */ - 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, ModuleInstallerInterface $module_installer = NULL) { $this->themeHandler = $theme_handler; $this->configFactory = $config_factory; $this->configInstaller = $config_installer; @@ -92,6 +100,17 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI $this->routeBuilder = $route_builder; $this->logger = $logger; $this->state = $state; + $this->moduleInstaller = $module_installer; + } + + /** + * @return \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected function getModuleInstaller() { + if (!isset($this->moduleInstaller)) { + $this->moduleInstaller = \Drupal::service('module_installer'); + } + return $this->moduleInstaller; } /** @@ -118,9 +137,15 @@ public function install(array $theme_list, $install_dependencies = TRUE) { } while (list($theme) = each($theme_list)) { - // Add dependencies to the list. The new themes will be processed as - // the while loop continues. - foreach (array_keys($theme_data[$theme]->requires) as $dependency) { + $module_dependencies = $theme_data[$theme]->module_dependencies; + $theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies); + + // Install the module dependencies. + $this->getModuleInstaller()->install(array_keys($module_dependencies)); + + // Add dependencies to the list of themes to install. The new themes + // will be processed as the while loop continues. + foreach (array_keys($theme_dependencies) as $dependency) { if (!isset($theme_data[$dependency])) { // The dependency does not exist. return FALSE; diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index 98d6179..d6c0f05 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -11,6 +11,7 @@ use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Theme\ThemeAccessCheck; use Drupal\Core\Url; +use Drupal\system\Form\ModulesListForm; use Drupal\system\SystemManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -202,6 +203,7 @@ public function themesPage() { $theme_groups = array('installed' => array(), 'uninstalled' => array()); $admin_theme = $config->get('admin'); $admin_theme_options = array(); + $modules = array(); foreach ($themes as &$theme) { if (!empty($theme->info['hidden'])) { @@ -244,9 +246,37 @@ public function themesPage() { $theme->incompatible_base = (isset($theme->info['base theme']) && !($theme->base_themes === array_filter($theme->base_themes))); // Confirm that the theme engine is available. $theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner); + // Confirm that module dependencies are available. + $theme->incompatible_module = FALSE; } + + // Check module dependencies. + if ($theme->module_dependencies) { + if (empty($modules)) { + $modules = system_rebuild_module_data(); + } + foreach ($theme->module_dependencies as $dependency => $version) { + if ($incompatible = ModulesListForm::checkDependency($modules, $dependency, $version)) { + $theme->module_dependencies[$dependency] = $incompatible; + $theme->incompatible_module = TRUE; + } + // Only display visible modules. + elseif (!empty($modules[$dependency]->hidden)) { + unset($theme->module_dependencies[$dependency]); + } else { + $name = $modules[$dependency]->info['name']; + if ($modules[$dependency]->status) { + $theme->module_dependencies[$dependency] = $this->t('@module', array('@module' => $name)); + } + else { + $theme->module_dependencies[$dependency] = $this->t('@module (disabled)', array('@module' => $name)); + } + } + } + } + $theme->operations = array(); - if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) { + if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine && !$theme->incompatible_module) { // Create the operations links. $query['theme'] = $theme->getName(); if ($this->themeAccess->checkAccess($theme->getName())) { diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php index e135356..62e0b5e 100644 --- a/core/modules/system/src/Controller/ThemeController.php +++ b/core/modules/system/src/Controller/ThemeController.php @@ -107,8 +107,22 @@ public function install(Request $request) { if (isset($theme)) { try { if ($this->themeHandler->install(array($theme))) { - $themes = $this->themeHandler->listInfo(); - drupal_set_message($this->t('The %theme theme has been installed.', array('%theme' => $themes[$theme]->info['name']))); + $theme_data = $this->themeHandler->listInfo(); + if ($theme_data[$theme]->module_dependencies) { + $module_data = system_rebuild_module_data(); + $module_names = []; + foreach(array_keys($theme_data[$theme]->module_dependencies) as $key) { + $module_names[] = $module_data[$key]->info['name']; + } + drupal_set_message($this->formatPlural(count($module_names), 'The %theme theme and its module dependency, %name, have been installed.', 'The %theme theme and its @count module dependencies have been installed: %names', array( + '%theme' => $theme_data[$theme]->info['name'], + '%name' => $module_names[0], + '%names' => implode(', ', $module_names), + ))); + } + else { + drupal_set_message($this->t('The %theme theme has been installed.', array('%theme' => $theme_data[$theme]->info['name']))); + } } else { drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error'); diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index 7831c38..30b5fd4 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -187,10 +187,48 @@ public function buildForm(array $form, FormStateInterface $form_state) { } /** + * Checks a single module dependency of a module or theme that has the given + * module version requirements. + * + * @param array $modules + * The list of existing modules. + * @param string $dependency + * The dependency to check. + * @param array $version + * Version requirement data from the module or theme declaring the + * dependency we are checking. + * + * @return string|null + * NULL if compatible, otherwise a string describing the incompatibility. + */ + public static function checkDependency(array $modules, $dependency, $version) { + if (!isset($modules[$dependency])) { + return t('@module (missing)', array('@module' => Unicode::ucfirst($dependency))); + } + else { + $name = $modules[$dependency]->info['name']; + // Check if it is incompatible with the dependency's version. + if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) { + return t('@module (incompatible with version @version)', array( + '@module' => $name . $incompatible_version, + '@version' => $modules[$dependency]->info['version'], + )); + } + // Disable the checkbox if the dependency is incompatible with this + // version of Drupal core. + elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) { + return t('@module (incompatible with this version of Drupal core)', array( + '@module' => $name, + )); + } + } + } + + /** * Builds a table row for the system modules page. * * @param array $modules - * The list existing modules. + * The list of existing modules. * @param \Drupal\Core\Extension\Extension $module * The module for which to build the form row. * @param $distribution @@ -297,35 +335,20 @@ protected function buildRow(array $modules, Extension $module, $distribution) { // If this module requires other modules, add them to the array. foreach ($module->requires as $dependency => $version) { - if (!isset($modules[$dependency])) { - $row['#requires'][$dependency] = $this->t('@module (missing)', array('@module' => Unicode::ucfirst($dependency))); - $row['enable']['#disabled'] = TRUE; - } - // Only display visible modules. - elseif (empty($modules[$dependency]->hidden)) { - $name = $modules[$dependency]->info['name']; - // Disable the module's checkbox if it is incompatible with the - // dependency's version. - if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) { - $row['#requires'][$dependency] = $this->t('@module (incompatible with version @version)', array( - '@module' => $name . $incompatible_version, - '@version' => $modules[$dependency]->info['version'], - )); + // Only display missing or visible modules. + if (empty($modules[$dependency]->hidden)) { + if ($incompatible = $this->checkDependency($modules, $dependency, $version)) { + $row['#requires'][$dependency] = $incompatible; $row['enable']['#disabled'] = TRUE; } - // Disable the checkbox if the dependency is incompatible with this - // version of Drupal core. - elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) { - $row['#requires'][$dependency] = $this->t('@module (incompatible with this version of Drupal core)', array( - '@module' => $name, - )); - $row['enable']['#disabled'] = TRUE; - } - elseif ($modules[$dependency]->status) { - $row['#requires'][$dependency] = $this->t('@module', array('@module' => $name)); - } else { - $row['#requires'][$dependency] = $this->t('@module (disabled)', array('@module' => $name)); + $name = $modules[$dependency]->info['name']; + if ($modules[$dependency]->status) { + $row['#requires'][$dependency] = $this->t('@module', array('@module' => $name)); + } + else { + $row['#requires'][$dependency] = $this->t('@module (disabled)', array('@module' => $name)); + } } } } diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index f9c49c7..d050ecd 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -225,6 +225,7 @@ function template_preprocess_system_modules_details(&$variables) { ]; $module['requires'] = $renderer->render($requires); } + // @TODO: Add theme dependencies. if (!empty($module['#required_by'])) { $required_by = [ '#theme' => 'item_list', @@ -350,6 +351,15 @@ function template_preprocess_system_themes_page(&$variables) { $current_theme['is_default'] = $theme->is_default; $current_theme['is_admin'] = $theme->is_admin; + $current_theme['requires'] = ''; + if (!empty($theme->module_dependencies)) { + $current_theme['requires'] = [ + '#theme' => 'item_list', + '#items' => $theme->module_dependencies, + '#context' => ['list_style' => 'comma-list'], + ]; + } + // Make sure to provide feedback on compatibility. $current_theme['incompatible'] = ''; if (!empty($theme->incompatible_core)) { @@ -370,6 +380,9 @@ function template_preprocess_system_themes_page(&$variables) { elseif (!empty($theme->incompatible_engine)) { $current_theme['incompatible'] = t('This theme requires the theme engine @theme_engine to operate correctly.', array('@theme_engine' => $theme->info['engine'])); } + elseif (!empty($theme->incompatible_module)) { + $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly.'); + } // Build operation links. $current_theme['operations'] = array( diff --git a/core/modules/system/templates/system-themes-page.html.twig b/core/modules/system/templates/system-themes-page.html.twig index 6e65d76..45b4930 100644 --- a/core/modules/system/templates/system-themes-page.html.twig +++ b/core/modules/system/templates/system-themes-page.html.twig @@ -22,6 +22,7 @@ * - notes: Identifies what context this theme is being used in, e.g., * default theme, admin theme. * - incompatible: Text describing any compatibility issues. + * - requires: A list of modules that this theme requires. * - operations: A list of operation links, e.g., Settings, Enable, Disable, * etc. these links should only be displayed if the theme is compatible. * @@ -62,6 +63,9 @@ {%- endif -%}
{{ theme.description }}
+ {% if theme.requires %} +
Requires: {{ theme.requires }}
+ {% endif %} {# Display operation links if the theme is compatible. #} {% if theme.incompatible %}
{{ theme.incompatible }}
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_module/test_module_required_by_theme/src/Service.php b/core/modules/system/tests/themes/test_theme_depending_on_module/test_module_required_by_theme/src/Service.php new file mode 100644 index 0000000..3e7d7ee --- /dev/null +++ b/core/modules/system/tests/themes/test_theme_depending_on_module/test_module_required_by_theme/src/Service.php @@ -0,0 +1,7 @@ +assertFalse(isset($system_list[$name]->info['regions']['test_region'])); } + public function testThemeWithModuleDependency() { + $this->assertFalse($this->moduleHandler()->moduleExists('test_module_required_by_theme')); + $this->themeInstaller()->install(['test_theme_depending_on_module']); + $this->assertTrue($this->moduleHandler()->moduleExists('test_module_required_by_theme')); + + $service = \Drupal::service('test_module_required_by_theme.service'); + $this->assertInstanceOf(Service::class, $service); + } + /** * Returns the theme handler service. * diff --git a/core/themes/stable/templates/admin/system-themes-page.html.twig b/core/themes/stable/templates/admin/system-themes-page.html.twig index 5a23f1a..0bc0439 100644 --- a/core/themes/stable/templates/admin/system-themes-page.html.twig +++ b/core/themes/stable/templates/admin/system-themes-page.html.twig @@ -22,6 +22,7 @@ * - notes: Identifies what context this theme is being used in, e.g., * default theme, admin theme. * - incompatible: Text describing any compatibility issues. + * - requires: A list of modules that this theme requires. * - operations: A list of operation links, e.g., Settings, Enable, Disable, * etc. these links should only be displayed if the theme is compatible. * @@ -60,6 +61,9 @@ {%- endif -%}
{{ theme.description }}
+ {% if theme.requires %} +
Requires: {{ theme.requires }}
+ {% endif %} {# Display operation links if the theme is compatible. #} {% if theme.incompatible %}
{{ theme.incompatible }}