3 namespace Drupal\Core\Extension;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\DrupalKernelInterface;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drupal\Core\Entity\FieldableEntityInterface;
10 use Drupal\Core\Serialization\Yaml;
13 * Default implementation of the module installer.
15 * It registers the module in config, installs its own configuration,
16 * installs the schema, updates the Drupal kernel and more.
18 class ModuleInstaller implements ModuleInstallerInterface {
23 * @var \Drupal\Core\Extension\ModuleHandlerInterface
25 protected $moduleHandler;
30 * @var \Drupal\Core\DrupalKernelInterface
42 * The uninstall validators.
44 * @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
46 protected $uninstallValidators;
49 * Constructs a new ModuleInstaller instance.
53 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
55 * @param \Drupal\Core\DrupalKernelInterface $kernel
58 * @see \Drupal\Core\DrupalKernel
59 * @see \Drupal\Core\CoreServiceProvider
61 public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
63 $this->moduleHandler = $module_handler;
64 $this->kernel = $kernel;
70 public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
71 $this->uninstallValidators[] = $uninstall_validator;
77 public function install(array $module_list, $enable_dependencies = TRUE) {
78 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
79 if ($enable_dependencies) {
80 // Get all module data so we can find dependencies and sort.
81 $module_data = system_rebuild_module_data();
82 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
83 if ($missing_modules = array_diff_key($module_list, $module_data)) {
84 // One or more of the given modules doesn't exist.
85 throw new MissingDependencyException(sprintf('Unable to install modules %s due to missing modules %s.', implode(', ', $module_list), implode(', ', $missing_modules)));
88 // Only process currently uninstalled modules.
89 $installed_modules = $extension_config->get('module') ?: [];
90 if (!$module_list = array_diff_key($module_list, $installed_modules)) {
91 // Nothing to do. All modules already installed.
95 // Add dependencies to the list. The new modules will be processed as
96 // the foreach loop continues.
97 foreach ($module_list as $module => $value) {
98 foreach (array_keys($module_data[$module]->requires) as $dependency) {
99 if (!isset($module_data[$dependency])) {
100 // The dependency does not exist.
101 throw new MissingDependencyException("Unable to install modules: module '$module' is missing its dependency module $dependency.");
104 // Skip already installed modules.
105 if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
106 $module_list[$dependency] = $dependency;
111 // Set the actual module weights.
112 $module_list = array_map(function ($module) use ($module_data) {
113 return $module_data[$module]->sort;
116 // Sort the module list by their weights (reverse).
117 arsort($module_list);
118 $module_list = array_keys($module_list);
121 // Required for module installation checks.
122 include_once $this->root . '/core/includes/install.inc';
124 /** @var \Drupal\Core\Config\ConfigInstaller $config_installer */
125 $config_installer = \Drupal::service('config.installer');
126 $sync_status = $config_installer->isSyncing();
128 $source_storage = $config_installer->getSourceStorage();
130 $modules_installed = [];
131 foreach ($module_list as $module) {
132 $enabled = $extension_config->get("module.$module") !== NULL;
134 // Throw an exception if the module name is too long.
135 if (strlen($module) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
136 throw new ExtensionNameLengthException("Module name '$module' is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters');
139 // Load a new config object for each iteration, otherwise changes made
140 // in hook_install() are not reflected in $extension_config.
141 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
143 // Check the validity of the default configuration. This will throw
144 // exceptions if the configuration is not valid.
145 $config_installer->checkConfigurationToInstall('module', $module);
147 // Save this data without checking schema. This is a performance
148 // improvement for module installation.
150 ->set("module.$module", 0)
151 ->set('module', module_config_sort($extension_config->get('module')))
154 // Prepare the new module list, sorted by weight, including filenames.
155 // This list is used for both the ModuleHandler and DrupalKernel. It
156 // needs to be kept in sync between both. A DrupalKernel reboot or
157 // rebuild will automatically re-instantiate a new ModuleHandler that
158 // uses the new module list of the kernel. However, DrupalKernel does
159 // not cause any modules to be loaded.
160 // Furthermore, the currently active (fixed) module list can be
161 // different from the configured list of enabled modules. For all active
162 // modules not contained in the configured enabled modules, we assume a
164 $current_module_filenames = $this->moduleHandler->getModuleList();
165 $current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
166 $current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
167 $module_filenames = [];
168 foreach ($current_modules as $name => $weight) {
169 if (isset($current_module_filenames[$name])) {
170 $module_filenames[$name] = $current_module_filenames[$name];
173 $module_path = drupal_get_path('module', $name);
174 $pathname = "$module_path/$name.info.yml";
175 $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
176 $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
180 // Update the module handler in order to load the module's code.
181 // This allows the module to participate in hooks and its existence to
182 // be discovered by other modules.
183 // The current ModuleHandler instance is obsolete with the kernel
185 $this->moduleHandler->setModuleList($module_filenames);
186 $this->moduleHandler->load($module);
187 module_load_install($module);
189 // Clear the static cache of system_rebuild_module_data() to pick up the
190 // new module, since it merges the installation status of modules into
191 // its statically cached list.
192 drupal_static_reset('system_rebuild_module_data');
194 // Update the kernel to include it.
195 $this->updateKernel($module_filenames);
197 // Replace the route provider service with a version that will rebuild
198 // if routes used during installation. This ensures that a module's
199 // routes are available during installation. This has to occur before
200 // any services that depend on it are instantiated otherwise those
201 // services will have the old route provider injected. Note that, since
202 // the container is rebuilt by updating the kernel, the route provider
203 // service is the regular one even though we are in a loop and might
204 // have replaced it before.
205 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
206 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
208 // Allow modules to react prior to the installation of a module.
209 $this->moduleHandler->invokeAll('module_preinstall', [$module]);
211 // Now install the module's schema if necessary.
212 drupal_install_schema($module);
214 // Clear plugin manager caches.
215 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
217 // Set the schema version to the number of the last update provided by
218 // the module, or the minimum core schema version.
219 $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
220 $versions = drupal_get_schema_versions($module);
222 $version = max(max($versions), $version);
225 // Notify interested components that this module's entity types and
226 // field storage definitions are new. For example, a SQL-based storage
227 // handler can use this as an opportunity to create the necessary
229 // @todo Clean this up in https://www.drupal.org/node/2350111.
230 $entity_manager = \Drupal::entityManager();
231 $update_manager = \Drupal::entityDefinitionUpdateManager();
232 foreach ($entity_manager->getDefinitions() as $entity_type) {
233 if ($entity_type->getProvider() == $module) {
234 $update_manager->installEntityType($entity_type);
236 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
237 // The module being installed may be adding new fields to existing
238 // entity types. Field definitions for any entity type defined by
239 // the module are handled in the if branch.
240 foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
241 if ($storage_definition->getProvider() == $module) {
242 // If the module being installed is also defining a storage key
243 // for the entity type, the entity schema may not exist yet. It
244 // will be created later in that case.
246 $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
248 catch (EntityStorageException $e) {
249 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()]);
256 // Install default configuration of the module.
257 $config_installer = \Drupal::service('config.installer');
261 ->setSourceStorage($source_storage);
263 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
265 // If the module has no current updates, but has some that were
266 // previously removed, set the version to the value of
267 // hook_update_last_removed().
268 if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
269 $version = max($version, $last_removed);
271 drupal_set_installed_schema_version($module, $version);
273 // Ensure that all post_update functions are registered already.
274 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
275 $post_update_registry = \Drupal::service('update.post_update_registry');
276 $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
278 // Record the fact that it was installed.
279 $modules_installed[] = $module;
281 // Drupal's stream wrappers needs to be re-registered in case a
282 // module-provided stream wrapper is used later in the same request. In
283 // particular, this happens when installing Drupal via Drush, as the
284 // 'translations' stream wrapper is provided by Interface Translation
285 // module and is later used to import translations.
286 \Drupal::service('stream_wrapper_manager')->register();
288 // Update the theme registry to include it.
289 drupal_theme_rebuild();
291 // Modules can alter theme info, so refresh theme data.
292 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
293 // causes a circular service dependency.
294 // @see https://www.drupal.org/node/2208429
295 \Drupal::service('theme_handler')->refreshInfo();
297 // Allow the module to perform install tasks.
298 $this->moduleHandler->invoke($module, 'install');
300 // Record the fact that it was installed.
301 \Drupal::logger('system')->info('%module module installed.', ['%module' => $module]);
305 // If any modules were newly installed, invoke hook_modules_installed().
306 if (!empty($modules_installed)) {
307 // If the container was rebuilt during hook_install() it might not have
308 // the 'router.route_provider.old' service.
309 if (\Drupal::hasService('router.route_provider.old')) {
310 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
312 if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
313 // Rebuild routes after installing module. This is done here on top of
314 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
315 // fastCGI which executes ::destruct() after the module installation
316 // page was sent already.
317 \Drupal::service('router.builder')->rebuild();
320 $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
329 public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
330 // Get all module data so we can find dependencies and sort.
331 $module_data = system_rebuild_module_data();
332 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
333 if (array_diff_key($module_list, $module_data)) {
334 // One or more of the given modules doesn't exist.
338 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
339 $installed_modules = $extension_config->get('module') ?: [];
340 if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
341 // Nothing to do. All modules already uninstalled.
345 if ($uninstall_dependents) {
346 // Add dependent modules to the list. The new modules will be processed as
347 // the foreach loop continues.
348 $profile = drupal_get_profile();
349 foreach ($module_list as $module => $value) {
350 foreach (array_keys($module_data[$module]->required_by) as $dependent) {
351 if (!isset($module_data[$dependent])) {
352 // The dependent module does not exist.
356 // Skip already uninstalled modules.
357 if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
358 $module_list[$dependent] = $dependent;
364 // Use the validators and throw an exception with the reasons.
365 if ($reasons = $this->validateUninstall($module_list)) {
366 foreach ($reasons as $reason) {
367 $reason_message[] = implode(', ', $reason);
369 throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
371 // Set the actual module weights.
372 $module_list = array_map(function ($module) use ($module_data) {
373 return $module_data[$module]->sort;
376 // Sort the module list by their weights.
378 $module_list = array_keys($module_list);
380 // Only process modules that are enabled. A module is only enabled if it is
381 // configured as enabled. Custom or overridden module handlers might contain
382 // the module already, which means that it might be loaded, but not
383 // necessarily installed.
384 foreach ($module_list as $module) {
386 // Clean up all entity bundles (including fields) of every entity type
387 // provided by the module that is being uninstalled.
388 // @todo Clean this up in https://www.drupal.org/node/2350111.
389 $entity_manager = \Drupal::entityManager();
390 foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
391 if ($entity_type->getProvider() == $module) {
392 foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
393 $entity_manager->onBundleDelete($bundle, $entity_type_id);
398 // Allow modules to react prior to the uninstallation of a module.
399 $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
401 // Uninstall the module.
402 module_load_install($module);
403 $this->moduleHandler->invoke($module, 'uninstall');
405 // Remove all configuration belonging to the module.
406 \Drupal::service('config.manager')->uninstall('module', $module);
408 // In order to make uninstalling transactional if anything uses routes.
409 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
410 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
412 // Notify interested components that this module's entity types are being
413 // deleted. For example, a SQL-based storage handler can use this as an
414 // opportunity to drop the corresponding database tables.
415 // @todo Clean this up in https://www.drupal.org/node/2350111.
416 $update_manager = \Drupal::entityDefinitionUpdateManager();
417 foreach ($entity_manager->getDefinitions() as $entity_type) {
418 if ($entity_type->getProvider() == $module) {
419 $update_manager->uninstallEntityType($entity_type);
421 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
422 // The module being uninstalled might have added new fields to
423 // existing entity types. This will add them to the deleted fields
424 // repository so their data will be purged on cron.
425 foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
426 if ($storage_definition->getProvider() == $module) {
427 $update_manager->uninstallFieldStorageDefinition($storage_definition);
433 // Remove the schema.
434 drupal_uninstall_schema($module);
436 // Remove the module's entry from the config. Don't check schema when
437 // uninstalling a module since we are only clearing a key.
438 \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
440 // Update the module handler to remove the module.
441 // The current ModuleHandler instance is obsolete with the kernel rebuild
443 $module_filenames = $this->moduleHandler->getModuleList();
444 unset($module_filenames[$module]);
445 $this->moduleHandler->setModuleList($module_filenames);
447 // Remove any potential cache bins provided by the module.
448 $this->removeCacheBins($module);
450 // Clear the static cache of system_rebuild_module_data() to pick up the
451 // new module, since it merges the installation status of modules into
452 // its statically cached list.
453 drupal_static_reset('system_rebuild_module_data');
455 // Clear plugin manager caches.
456 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
458 // Update the kernel to exclude the uninstalled modules.
459 $this->updateKernel($module_filenames);
461 // Update the theme registry to remove the newly uninstalled module.
462 drupal_theme_rebuild();
464 // Modules can alter theme info, so refresh theme data.
465 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
466 // causes a circular service dependency.
467 // @see https://www.drupal.org/node/2208429
468 \Drupal::service('theme_handler')->refreshInfo();
470 \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
472 $schema_store = \Drupal::keyValue('system.schema');
473 $schema_store->delete($module);
475 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
476 $post_update_registry = \Drupal::service('update.post_update_registry');
477 $post_update_registry->filterOutInvokedUpdatesByModule($module);
479 // Rebuild routes after installing module. This is done here on top of
480 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
481 // fastCGI which executes ::destruct() after the Module uninstallation page
483 \Drupal::service('router.builder')->rebuild();
484 drupal_get_installed_schema_version(NULL, TRUE);
486 // Let other modules react.
487 $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
489 // Flush all persistent caches.
490 // Any cache entry might implicitly depend on the uninstalled modules,
491 // so clear all of them explicitly.
492 $this->moduleHandler->invokeAll('cache_flush');
493 foreach (Cache::getBins() as $service_id => $cache_backend) {
494 $cache_backend->deleteAll();
501 * Helper method for removing all cache bins registered by a given module.
503 * @param string $module
504 * The name of the module for which to remove all registered cache bins.
506 protected function removeCacheBins($module) {
507 $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
508 if (!file_exists($service_yaml_file)) {
512 $definitions = Yaml::decode(file_get_contents($service_yaml_file));
514 $cache_bin_services = array_filter(
515 isset($definitions['services']) ? $definitions['services'] : [],
516 function ($definition) {
517 $tags = isset($definition['tags']) ? $definition['tags'] : [];
518 foreach ($tags as $tag) {
519 if (isset($tag['name']) && ($tag['name'] == 'cache.bin')) {
527 foreach (array_keys($cache_bin_services) as $service_id) {
528 $backend = $this->kernel->getContainer()->get($service_id);
529 if ($backend instanceof CacheBackendInterface) {
530 $backend->removeBin();
536 * Updates the kernel module list.
538 * @param string $module_filenames
539 * The list of installed modules.
541 protected function updateKernel($module_filenames) {
542 // This reboots the kernel to register the module's bundle and its services
543 // in the service container. The $module_filenames argument is taken over as
544 // %container.modules% parameter, which is passed to a fresh ModuleHandler
545 // instance upon first retrieval.
546 $this->kernel->updateModules($module_filenames, $module_filenames);
547 // After rebuilding the container we need to update the injected
549 $container = $this->kernel->getContainer();
550 $this->moduleHandler = $container->get('module_handler');
556 public function validateUninstall(array $module_list) {
558 foreach ($module_list as $module) {
559 foreach ($this->uninstallValidators as $validator) {
560 $validation_reasons = $validator->validate($module);
561 if (!empty($validation_reasons)) {
562 if (!isset($reasons[$module])) {
563 $reasons[$module] = [];
565 $reasons[$module] = array_merge($reasons[$module], $validation_reasons);