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/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 235a7c1a41..ea16fcd4e7 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -486,7 +486,7 @@ protected function findPreExistingConfiguration(StorageInterface $storage) { /** * {@inheritdoc} */ - public function checkConfigurationToInstall($type, $name) { + public function checkConfigurationToInstall($type, $name, StorageInterface $storage = NULL) { if ($this->isSyncing()) { // Configuration is assumed to already be checked by the config importer // validation events. @@ -497,7 +497,9 @@ public function checkConfigurationToInstall($type, $name) { return; } - $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION); + if (!$storage) { + $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION); + } $enabled_extensions = $this->getEnabledExtensions(); // Add the extension that will be enabled to the list of enabled extensions. diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php index aae9474a52..50634c9568 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -109,10 +109,14 @@ public function isSyncing(); * Type of extension to install. * @param string $name * Name of extension to install. + * @param \Drupal\Core\Config\StorageInterface|null $storage + * A storage that can be used instead of a FileStorage for accessing + * configuration from the extension prior to installation. * * @throws \Drupal\Core\Config\UnmetDependenciesException * @throws \Drupal\Core\Config\PreExistingConfigException */ - public function checkConfigurationToInstall($type, $name); + // phpcs:ignore + public function checkConfigurationToInstall($type, $name /*, StorageInterface $storage = NULL */); } diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index 1ad7017f5d..327c89e3fc 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -273,4 +273,28 @@ public function reset() { $this->folders = NULL; } + /** + * @param $extensions + * An associative array of Extension objects, keyed by extension name. + * + * @return $this + */ + public function addFoldersFromExtension(Extension $extension) { + if (!isset($this->folders)) { + $this->folders = []; + } + $this->forceCopyFolderOnCreateCollection = TRUE; + $this->folders += $this->getComponentNames([$extension->getName() => $extension]); + return $this; + } + + public function createCollection($collection) { + // Creating a new storage object in FileStorage for a new collection, throws + // away the folders information, so we keep the state alive. + $storage = parent::createCollection($collection); + $storage->folders = $this->folders; + return $storage; + } + + } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index bc2f9d0323..e4f2ba8c27 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -4,6 +4,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\InstallStorage; use Drupal\Core\DrupalKernelInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\FieldableEntityInterface; @@ -145,7 +146,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $source_storage = $config_installer->getSourceStorage(); } $modules_installed = []; - foreach ($module_list as $module) { + // Create a storage able to access uninstalled configuration from a module. + $install_storage = new InstallStorage(); + 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 +158,15 @@ 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'); + // Add the module to the install storage so it's configuration is + // included when checking dependencies. + $install_storage->addFoldersFromExtension($module_data[$module]); // Check the validity of the default configuration. This will throw // exceptions if the configuration is not valid. - $config_installer->checkConfigurationToInstall('module', $module); + $config_installer->checkConfigurationToInstall('module', $module, $install_storage); // Save this data without checking schema. This is a performance // improvement for module installation. @@ -187,153 +194,167 @@ 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) { + // 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())); } - 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()]); - } + 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()]); } } } } + } + } - // 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); + 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); + } - // 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. diff --git a/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php b/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php index e72fc43f81..3a5f15e3ab 100644 --- a/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php @@ -127,9 +127,9 @@ public function isSyncing() /** * {@inheritdoc} */ - public function checkConfigurationToInstall($type, $name) + public function checkConfigurationToInstall($type, $name, ?\Drupal\Core\Config\StorageInterface $storage = NULL) { - return $this->lazyLoadItself()->checkConfigurationToInstall($type, $name); + return $this->lazyLoadItself()->checkConfigurationToInstall($type, $name, $storage); } } diff --git a/core/modules/system/tests/src/Functional/Pager/PagerTest.php b/core/modules/system/tests/src/Functional/Pager/PagerTest.php index b6a2c66aca..97bea05ac9 100644 --- a/core/modules/system/tests/src/Functional/Pager/PagerTest.php +++ b/core/modules/system/tests/src/Functional/Pager/PagerTest.php @@ -92,7 +92,7 @@ public function testPagerQueryParametersAndCacheContext() { $elements = $this->xpath('//li[contains(@class, :class)]/a', [':class' => 'pager__item--last']); $elements[0]->click(); $this->assertText('Pager calls: 1', 'First link call to pager shows 1 calls.'); - $this->assertText('[url.query_args.pagers:0]=0.60'); + $this->assertText('[url.query_args.pagers:0]=0.61'); $this->assertCacheContext('url.query_args'); // Reset counter to 0.