diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 77d3df7bad..f8a165e7af 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1587,8 +1587,13 @@ function install_profile_modules(&$install_state) { arsort($non_required); $operations = []; - foreach ($required + $non_required as $module => $weight) { - $operations[] = ['_install_module_batch', [$module, $files[$module]->info['name']]]; + if ($install_state['interactive']) { + foreach ($required + $non_required as $module => $weight) { + $operations[] = ['_install_module_batch', [$module, $files[$module]->info['name']]]; + } + } + else { + $operations[] = ['_install_modules_batch', [array_keys($required + $non_required)]]; } $batch = [ 'operations' => $operations, @@ -1879,6 +1884,17 @@ function _install_module_batch($module, $module_name, &$context) { $context['message'] = t('Installed %module module.', ['%module' => $module_name]); } +/** + * Implements callback_batch_operation(). + * + * Performs batch installation of modules. + */ +function _install_modules_batch(array $modules, &$context) { + \Drupal::service('module_installer')->install($modules, FALSE); + $context['results'] = $modules; + $context['message'] = t('Installed modules.'); +} + /** * Checks installation requirements and reports any errors. * diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index bc2f9d0323..e207c24f26 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -145,7 +145,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $source_storage = $config_installer->getSourceStorage(); } $modules_installed = []; - foreach ($module_list as $module) { + foreach ($module_list as $key => $module) { $enabled = $extension_config->get("module.$module") !== NULL; if (!$enabled) { // Throw an exception if the module name is too long. @@ -155,11 +155,13 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // Load a new config object for each iteration, otherwise changes made // in hook_install() are not reflected in $extension_config. - $extension_config = \Drupal::configFactory()->getEditable('core.extension'); + $extension_config = \Drupal::configFactory() + ->getEditable('core.extension'); // Check the validity of the default configuration. This will throw // exceptions if the configuration is not valid. - $config_installer->checkConfigurationToInstall('module', $module); + // @todo make this handle multiple modules... + // $config_installer->checkConfigurationToInstall('module', $module); // Save this data without checking schema. This is a performance // improvement for module installation. @@ -187,153 +189,165 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $module_filenames[$name] = $current_module_filenames[$name]; } else { - $module_path = \Drupal::service('extension.list.module')->getPath($name); + $module_path = \Drupal::service('extension.list.module') + ->getPath($name); $pathname = "$module_path/$name.info.yml"; $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL; $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename); } } + } + } - // Update the module handler in order to have the correct module list - // for the kernel update. - $this->moduleHandler->setModuleList($module_filenames); - - // Clear the static cache of the "extension.list.module" service to pick - // up the new module, since it merges the installation status of modules - // into its statically cached list. - \Drupal::service('extension.list.module')->reset(); - - // Update the kernel to include it. - $this->updateKernel($module_filenames); - - // Load the module's .module and .install files. - $this->moduleHandler->load($module); - module_load_install($module); - - if (!InstallerKernel::installationAttempted()) { - // Replace the route provider service with a version that will rebuild - // if routes used during installation. This ensures that a module's - // routes are available during installation. This has to occur before - // any services that depend on it are instantiated otherwise those - // services will have the old route provider injected. Note that, since - // the container is rebuilt by updating the kernel, the route provider - // service is the regular one even though we are in a loop and might - // have replaced it before. - \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider')); - \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder')); - } + if (!isset($module_filenames)) { + // There's nothing to do. $module_list will be empty too. + return; + } - // Allow modules to react prior to the installation of a module. - $this->moduleHandler->invokeAll('module_preinstall', [$module]); + // Update the module handler in order to have the correct module list + // for the kernel update. + $this->moduleHandler->setModuleList($module_filenames); + + // Clear the static cache of the "extension.list.module" service to pick + // up the new module, since it merges the installation status of modules + // into its statically cached list. + \Drupal::service('extension.list.module')->reset(); + + // Update the kernel to include it. + $this->updateKernel($module_filenames); + + if (!InstallerKernel::installationAttempted()) { + // Replace the route provider service with a version that will rebuild + // if routes used during installation. This ensures that a module's + // routes are available during installation. This has to occur before + // any services that depend on it are instantiated otherwise those + // services will have the old route provider injected. Note that, since + // the container is rebuilt by updating the kernel, the route provider + // service is the regular one even though we are in a loop and might + // have replaced it before. + \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider')); + \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder')); + } - // Now install the module's schema if necessary. - drupal_install_schema($module); + foreach ($module_list as $module) { + // Load the module's .module and .install files. + $this->moduleHandler->load($module); + module_load_install($module); - // Clear plugin manager caches. - \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions(); + // Allow modules to react prior to the installation of a module. + $this->moduleHandler->invokeAll('module_preinstall', [$module]); - // Set the schema version to the number of the last update provided by - // the module, or the minimum core schema version. - $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION; - $versions = drupal_get_schema_versions($module); - if ($versions) { - $version = max(max($versions), $version); - } + // Now install the module's schema if necessary. + drupal_install_schema($module); + } - // Notify interested components that this module's entity types and - // field storage definitions are new. For example, a SQL-based storage - // handler can use this as an opportunity to create the necessary - // database tables. - // @todo Clean this up in https://www.drupal.org/node/2350111. - $entity_type_manager = \Drupal::entityTypeManager(); - $update_manager = \Drupal::entityDefinitionUpdateManager(); - /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ - $entity_field_manager = \Drupal::service('entity_field.manager'); - foreach ($entity_type_manager->getDefinitions() as $entity_type) { - $is_fieldable_entity_type = $entity_type->entityClassImplements(FieldableEntityInterface::class); - - if ($entity_type->getProvider() == $module) { - if ($is_fieldable_entity_type) { - $update_manager->installFieldableEntityType($entity_type, $entity_field_manager->getFieldStorageDefinitions($entity_type->id())); - } - else { - $update_manager->installEntityType($entity_type); - } + // Clear plugin manager caches. + \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions(); + + foreach ($module_list as $module) { + // Set the schema version to the number of the last update provided by + // the module, or the minimum core schema version. + $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION; + $versions = drupal_get_schema_versions($module); + if ($versions) { + $version = max(max($versions), $version); + } + + // Notify interested components that this module's entity types and + // field storage definitions are new. For example, a SQL-based storage + // handler can use this as an opportunity to create the necessary + // database tables. + // @todo Clean this up in https://www.drupal.org/node/2350111. + $entity_type_manager = \Drupal::entityTypeManager(); + $update_manager = \Drupal::entityDefinitionUpdateManager(); + /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ + $entity_field_manager = \Drupal::service('entity_field.manager'); + foreach ($entity_type_manager->getDefinitions() as $entity_type) { + $is_fieldable_entity_type = $entity_type->entityClassImplements(FieldableEntityInterface::class); + + if ($entity_type->getProvider() == $module) { + if ($is_fieldable_entity_type) { + $update_manager->installFieldableEntityType($entity_type, $entity_field_manager->getFieldStorageDefinitions($entity_type->id())); + } + else { + $update_manager->installEntityType($entity_type); } - elseif ($is_fieldable_entity_type) { - // The module being installed may be adding new fields to existing - // entity types. Field definitions for any entity type defined by - // the module are handled in the if branch. - foreach ($entity_field_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) { - if ($storage_definition->getProvider() == $module) { - // If the module being installed is also defining a storage key - // for the entity type, the entity schema may not exist yet. It - // will be created later in that case. - try { - $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition); - } - catch (EntityStorageException $e) { - watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]); - } + } + elseif ($is_fieldable_entity_type) { + // The module being installed may be adding new fields to existing + // entity types. Field definitions for any entity type defined by + // the module are handled in the if branch. + foreach ($entity_field_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) { + if ($storage_definition->getProvider() == $module) { + // If the module being installed is also defining a storage key + // for the entity type, the entity schema may not exist yet. It + // will be created later in that case. + try { + $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition); + } + catch (EntityStorageException $e) { + watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]); } } } } + } - // Install default configuration of the module. - $config_installer = \Drupal::service('config.installer'); - if ($sync_status) { - $config_installer - ->setSyncing(TRUE) - ->setSourceStorage($source_storage); - } - \Drupal::service('config.installer')->installDefaultConfig('module', $module); - - // If the module has no current updates, but has some that were - // previously removed, set the version to the value of - // hook_update_last_removed(). - if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) { - $version = max($version, $last_removed); - } - drupal_set_installed_schema_version($module, $version); - - // Ensure that all post_update functions are registered already. This - // should include existing post-updates, as well as any specified as - // having been previously removed, to ensure that newly installed and - // updated sites have the same entries in the registry. - /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ - $post_update_registry = \Drupal::service('update.post_update_registry'); - $post_update_registry->registerInvokedUpdates(array_merge($post_update_registry->getModuleUpdateFunctions($module), array_keys($post_update_registry->getRemovedPostUpdates($module)))); + // Install default configuration of the module. + $config_installer = \Drupal::service('config.installer'); + if ($sync_status) { + $config_installer + ->setSyncing(TRUE) + ->setSourceStorage($source_storage); + } + \Drupal::service('config.installer') + ->installDefaultConfig('module', $module); + + // If the module has no current updates, but has some that were + // previously removed, set the version to the value of + // hook_update_last_removed(). + if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) { + $version = max($version, $last_removed); + } + drupal_set_installed_schema_version($module, $version); - // Record the fact that it was installed. - $modules_installed[] = $module; + // Ensure that all post_update functions are registered already. This + // should include existing post-updates, as well as any specified as + // having been previously removed, to ensure that newly installed and + // updated sites have the same entries in the registry. + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->registerInvokedUpdates(array_merge($post_update_registry->getModuleUpdateFunctions($module), array_keys($post_update_registry->getRemovedPostUpdates($module)))); - // Drupal's stream wrappers needs to be re-registered in case a - // module-provided stream wrapper is used later in the same request. In - // particular, this happens when installing Drupal via Drush, as the - // 'translations' stream wrapper is provided by Interface Translation - // module and is later used to import translations. - \Drupal::service('stream_wrapper_manager')->register(); + // Record the fact that it was installed. + $modules_installed[] = $module; + } - // Update the theme registry to include it. - drupal_theme_rebuild(); + // If any modules were newly installed, invoke hook_modules_installed(). + if (!empty($modules_installed)) { + // Drupal's stream wrappers needs to be re-registered in case a + // module-provided stream wrapper is used later in the same request. In + // particular, this happens when installing Drupal via Drush, as the + // 'translations' stream wrapper is provided by Interface Translation + // module and is later used to import translations. + \Drupal::service('stream_wrapper_manager')->register(); + + // Update the theme registry to include it. + drupal_theme_rebuild(); - // Modules can alter theme info, so refresh theme data. - // @todo ThemeHandler cannot be injected into ModuleHandler, since that - // causes a circular service dependency. - // @see https://www.drupal.org/node/2208429 - \Drupal::service('theme_handler')->refreshInfo(); + // Modules can alter theme info, so refresh theme data. + // @todo ThemeHandler cannot be injected into ModuleHandler, since that + // causes a circular service dependency. + // @see https://www.drupal.org/node/2208429 + \Drupal::service('theme_handler')->refreshInfo(); + foreach ($modules_installed as $module) { // Allow the module to perform install tasks. $this->moduleHandler->invoke($module, 'install', [$sync_status]); // Record the fact that it was installed. \Drupal::logger('system')->info('%module module installed.', ['%module' => $module]); } - } - - // If any modules were newly installed, invoke hook_modules_installed(). - if (!empty($modules_installed)) { if (!InstallerKernel::installationAttempted()) { // If the container was rebuilt during hook_install() it might not have // the 'router.route_provider.old' service.