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 * We don't inject dependencies yet, as we would need to reload them after
19 * each installation or uninstallation of a module.
20 * https://www.drupal.org/project/drupal/issues/2350111 for example tries to
23 class ModuleInstaller implements ModuleInstallerInterface {
28 * @var \Drupal\Core\Extension\ModuleHandlerInterface
30 protected $moduleHandler;
35 * @var \Drupal\Core\DrupalKernelInterface
47 * The uninstall validators.
49 * @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
51 protected $uninstallValidators;
54 * Constructs a new ModuleInstaller instance.
58 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
60 * @param \Drupal\Core\DrupalKernelInterface $kernel
63 * @see \Drupal\Core\DrupalKernel
64 * @see \Drupal\Core\CoreServiceProvider
66 public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
68 $this->moduleHandler = $module_handler;
69 $this->kernel = $kernel;
75 public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
76 $this->uninstallValidators[] = $uninstall_validator;
82 public function install(array $module_list, $enable_dependencies = TRUE) {
83 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
84 if ($enable_dependencies) {
85 // Get all module data so we can find dependencies and sort.
86 $module_data = system_rebuild_module_data();
87 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
88 if ($missing_modules = array_diff_key($module_list, $module_data)) {
89 // One or more of the given modules doesn't exist.
90 throw new MissingDependencyException(sprintf('Unable to install modules %s due to missing modules %s.', implode(', ', $module_list), implode(', ', $missing_modules)));
93 // Only process currently uninstalled modules.
94 $installed_modules = $extension_config->get('module') ?: [];
95 if (!$module_list = array_diff_key($module_list, $installed_modules)) {
96 // Nothing to do. All modules already installed.
100 // Add dependencies to the list. The new modules will be processed as
101 // the foreach loop continues.
102 foreach ($module_list as $module => $value) {
103 foreach (array_keys($module_data[$module]->requires) as $dependency) {
104 if (!isset($module_data[$dependency])) {
105 // The dependency does not exist.
106 throw new MissingDependencyException("Unable to install modules: module '$module' is missing its dependency module $dependency.");
109 // Skip already installed modules.
110 if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
111 $module_list[$dependency] = $dependency;
116 // Set the actual module weights.
117 $module_list = array_map(function ($module) use ($module_data) {
118 return $module_data[$module]->sort;
121 // Sort the module list by their weights (reverse).
122 arsort($module_list);
123 $module_list = array_keys($module_list);
126 // Required for module installation checks.
127 include_once $this->root . '/core/includes/install.inc';
129 /** @var \Drupal\Core\Config\ConfigInstaller $config_installer */
130 $config_installer = \Drupal::service('config.installer');
131 $sync_status = $config_installer->isSyncing();
133 $source_storage = $config_installer->getSourceStorage();
135 $modules_installed = [];
136 foreach ($module_list as $module) {
137 $enabled = $extension_config->get("module.$module") !== NULL;
139 // Throw an exception if the module name is too long.
140 if (strlen($module) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
141 throw new ExtensionNameLengthException("Module name '$module' is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters');
144 // Load a new config object for each iteration, otherwise changes made
145 // in hook_install() are not reflected in $extension_config.
146 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
148 // Check the validity of the default configuration. This will throw
149 // exceptions if the configuration is not valid.
150 $config_installer->checkConfigurationToInstall('module', $module);
152 // Save this data without checking schema. This is a performance
153 // improvement for module installation.
155 ->set("module.$module", 0)
156 ->set('module', module_config_sort($extension_config->get('module')))
159 // Prepare the new module list, sorted by weight, including filenames.
160 // This list is used for both the ModuleHandler and DrupalKernel. It
161 // needs to be kept in sync between both. A DrupalKernel reboot or
162 // rebuild will automatically re-instantiate a new ModuleHandler that
163 // uses the new module list of the kernel. However, DrupalKernel does
164 // not cause any modules to be loaded.
165 // Furthermore, the currently active (fixed) module list can be
166 // different from the configured list of enabled modules. For all active
167 // modules not contained in the configured enabled modules, we assume a
169 $current_module_filenames = $this->moduleHandler->getModuleList();
170 $current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
171 $current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
172 $module_filenames = [];
173 foreach ($current_modules as $name => $weight) {
174 if (isset($current_module_filenames[$name])) {
175 $module_filenames[$name] = $current_module_filenames[$name];
178 $module_path = \Drupal::service('extension.list.module')->getPath($name);
179 $pathname = "$module_path/$name.info.yml";
180 $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
181 $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
185 // Update the module handler in order to load the module's code.
186 // This allows the module to participate in hooks and its existence to
187 // be discovered by other modules.
188 // The current ModuleHandler instance is obsolete with the kernel
190 $this->moduleHandler->setModuleList($module_filenames);
191 $this->moduleHandler->load($module);
192 module_load_install($module);
194 // Clear the static cache of the "extension.list.module" service to pick
195 // up the new module, since it merges the installation status of modules
196 // into its statically cached list.
197 \Drupal::service('extension.list.module')->reset();
199 // Update the kernel to include it.
200 $this->updateKernel($module_filenames);
202 // Replace the route provider service with a version that will rebuild
203 // if routes used during installation. This ensures that a module's
204 // routes are available during installation. This has to occur before
205 // any services that depend on it are instantiated otherwise those
206 // services will have the old route provider injected. Note that, since
207 // the container is rebuilt by updating the kernel, the route provider
208 // service is the regular one even though we are in a loop and might
209 // have replaced it before.
210 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
211 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
213 // Allow modules to react prior to the installation of a module.
214 $this->moduleHandler->invokeAll('module_preinstall', [$module]);
216 // Now install the module's schema if necessary.
217 drupal_install_schema($module);
219 // Clear plugin manager caches.
220 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
222 // Set the schema version to the number of the last update provided by
223 // the module, or the minimum core schema version.
224 $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
225 $versions = drupal_get_schema_versions($module);
227 $version = max(max($versions), $version);
230 // Notify interested components that this module's entity types and
231 // field storage definitions are new. For example, a SQL-based storage
232 // handler can use this as an opportunity to create the necessary
234 // @todo Clean this up in https://www.drupal.org/node/2350111.
235 $entity_manager = \Drupal::entityManager();
236 $update_manager = \Drupal::entityDefinitionUpdateManager();
237 foreach ($entity_manager->getDefinitions() as $entity_type) {
238 if ($entity_type->getProvider() == $module) {
239 $update_manager->installEntityType($entity_type);
241 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
242 // The module being installed may be adding new fields to existing
243 // entity types. Field definitions for any entity type defined by
244 // the module are handled in the if branch.
245 foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
246 if ($storage_definition->getProvider() == $module) {
247 // If the module being installed is also defining a storage key
248 // for the entity type, the entity schema may not exist yet. It
249 // will be created later in that case.
251 $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
253 catch (EntityStorageException $e) {
254 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()]);
261 // Install default configuration of the module.
262 $config_installer = \Drupal::service('config.installer');
266 ->setSourceStorage($source_storage);
268 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
270 // If the module has no current updates, but has some that were
271 // previously removed, set the version to the value of
272 // hook_update_last_removed().
273 if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
274 $version = max($version, $last_removed);
276 drupal_set_installed_schema_version($module, $version);
278 // Ensure that all post_update functions are registered already.
279 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
280 $post_update_registry = \Drupal::service('update.post_update_registry');
281 $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
283 // Record the fact that it was installed.
284 $modules_installed[] = $module;
286 // Drupal's stream wrappers needs to be re-registered in case a
287 // module-provided stream wrapper is used later in the same request. In
288 // particular, this happens when installing Drupal via Drush, as the
289 // 'translations' stream wrapper is provided by Interface Translation
290 // module and is later used to import translations.
291 \Drupal::service('stream_wrapper_manager')->register();
293 // Update the theme registry to include it.
294 drupal_theme_rebuild();
296 // Modules can alter theme info, so refresh theme data.
297 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
298 // causes a circular service dependency.
299 // @see https://www.drupal.org/node/2208429
300 \Drupal::service('theme_handler')->refreshInfo();
302 // Allow the module to perform install tasks.
303 $this->moduleHandler->invoke($module, 'install');
305 // Record the fact that it was installed.
306 \Drupal::logger('system')->info('%module module installed.', ['%module' => $module]);
310 // If any modules were newly installed, invoke hook_modules_installed().
311 if (!empty($modules_installed)) {
312 // If the container was rebuilt during hook_install() it might not have
313 // the 'router.route_provider.old' service.
314 if (\Drupal::hasService('router.route_provider.old')) {
315 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
317 if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
318 // Rebuild routes after installing module. This is done here on top of
319 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
320 // fastCGI which executes ::destruct() after the module installation
321 // page was sent already.
322 \Drupal::service('router.builder')->rebuild();
325 $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
334 public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
335 // Get all module data so we can find dependencies and sort.
336 $module_data = system_rebuild_module_data();
337 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
338 if (array_diff_key($module_list, $module_data)) {
339 // One or more of the given modules doesn't exist.
343 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
344 $installed_modules = $extension_config->get('module') ?: [];
345 if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
346 // Nothing to do. All modules already uninstalled.
350 if ($uninstall_dependents) {
351 // Add dependent modules to the list. The new modules will be processed as
352 // the foreach loop continues.
353 foreach ($module_list as $module => $value) {
354 foreach (array_keys($module_data[$module]->required_by) as $dependent) {
355 if (!isset($module_data[$dependent])) {
356 // The dependent module does not exist.
360 // Skip already uninstalled modules.
361 if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) {
362 $module_list[$dependent] = $dependent;
368 // Use the validators and throw an exception with the reasons.
369 if ($reasons = $this->validateUninstall($module_list)) {
370 foreach ($reasons as $reason) {
371 $reason_message[] = implode(', ', $reason);
373 throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
375 // Set the actual module weights.
376 $module_list = array_map(function ($module) use ($module_data) {
377 return $module_data[$module]->sort;
380 // Sort the module list by their weights.
382 $module_list = array_keys($module_list);
384 // Only process modules that are enabled. A module is only enabled if it is
385 // configured as enabled. Custom or overridden module handlers might contain
386 // the module already, which means that it might be loaded, but not
387 // necessarily installed.
388 foreach ($module_list as $module) {
390 // Clean up all entity bundles (including fields) of every entity type
391 // provided by the module that is being uninstalled.
392 // @todo Clean this up in https://www.drupal.org/node/2350111.
393 $entity_manager = \Drupal::entityManager();
394 foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
395 if ($entity_type->getProvider() == $module) {
396 foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
397 $entity_manager->onBundleDelete($bundle, $entity_type_id);
402 // Allow modules to react prior to the uninstallation of a module.
403 $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
405 // Uninstall the module.
406 module_load_install($module);
407 $this->moduleHandler->invoke($module, 'uninstall');
409 // Remove all configuration belonging to the module.
410 \Drupal::service('config.manager')->uninstall('module', $module);
412 // In order to make uninstalling transactional if anything uses routes.
413 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
414 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
416 // Notify interested components that this module's entity types are being
417 // deleted. For example, a SQL-based storage handler can use this as an
418 // opportunity to drop the corresponding database tables.
419 // @todo Clean this up in https://www.drupal.org/node/2350111.
420 $update_manager = \Drupal::entityDefinitionUpdateManager();
421 foreach ($entity_manager->getDefinitions() as $entity_type) {
422 if ($entity_type->getProvider() == $module) {
423 $update_manager->uninstallEntityType($entity_type);
425 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
426 // The module being uninstalled might have added new fields to
427 // existing entity types. This will add them to the deleted fields
428 // repository so their data will be purged on cron.
429 foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
430 if ($storage_definition->getProvider() == $module) {
431 $update_manager->uninstallFieldStorageDefinition($storage_definition);
437 // Remove the schema.
438 drupal_uninstall_schema($module);
440 // Remove the module's entry from the config. Don't check schema when
441 // uninstalling a module since we are only clearing a key.
442 \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
444 // Update the module handler to remove the module.
445 // The current ModuleHandler instance is obsolete with the kernel rebuild
447 $module_filenames = $this->moduleHandler->getModuleList();
448 unset($module_filenames[$module]);
449 $this->moduleHandler->setModuleList($module_filenames);
451 // Remove any potential cache bins provided by the module.
452 $this->removeCacheBins($module);
454 // Clear the static cache of the "extension.list.module" service to pick
455 // up the new module, since it merges the installation status of modules
456 // into its statically cached list.
457 \Drupal::service('extension.list.module')->reset();
459 // Clear plugin manager caches.
460 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
462 // Update the kernel to exclude the uninstalled modules.
463 $this->updateKernel($module_filenames);
465 // Update the theme registry to remove the newly uninstalled module.
466 drupal_theme_rebuild();
468 // Modules can alter theme info, so refresh theme data.
469 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
470 // causes a circular service dependency.
471 // @see https://www.drupal.org/node/2208429
472 \Drupal::service('theme_handler')->refreshInfo();
474 \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
476 $schema_store = \Drupal::keyValue('system.schema');
477 $schema_store->delete($module);
479 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
480 $post_update_registry = \Drupal::service('update.post_update_registry');
481 $post_update_registry->filterOutInvokedUpdatesByModule($module);
483 // Rebuild routes after installing module. This is done here on top of
484 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
485 // fastCGI which executes ::destruct() after the Module uninstallation page
487 \Drupal::service('router.builder')->rebuild();
488 drupal_get_installed_schema_version(NULL, TRUE);
490 // Let other modules react.
491 $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
493 // Flush all persistent caches.
494 // Any cache entry might implicitly depend on the uninstalled modules,
495 // so clear all of them explicitly.
496 $this->moduleHandler->invokeAll('cache_flush');
497 foreach (Cache::getBins() as $service_id => $cache_backend) {
498 $cache_backend->deleteAll();
505 * Helper method for removing all cache bins registered by a given module.
507 * @param string $module
508 * The name of the module for which to remove all registered cache bins.
510 protected function removeCacheBins($module) {
511 $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
512 if (!file_exists($service_yaml_file)) {
516 $definitions = Yaml::decode(file_get_contents($service_yaml_file));
518 $cache_bin_services = array_filter(
519 isset($definitions['services']) ? $definitions['services'] : [],
520 function ($definition) {
521 $tags = isset($definition['tags']) ? $definition['tags'] : [];
522 foreach ($tags as $tag) {
523 if (isset($tag['name']) && ($tag['name'] == 'cache.bin')) {
531 foreach (array_keys($cache_bin_services) as $service_id) {
532 $backend = $this->kernel->getContainer()->get($service_id);
533 if ($backend instanceof CacheBackendInterface) {
534 $backend->removeBin();
540 * Updates the kernel module list.
542 * @param string $module_filenames
543 * The list of installed modules.
545 protected function updateKernel($module_filenames) {
546 // This reboots the kernel to register the module's bundle and its services
547 // in the service container. The $module_filenames argument is taken over as
548 // %container.modules% parameter, which is passed to a fresh ModuleHandler
549 // instance upon first retrieval.
550 $this->kernel->updateModules($module_filenames, $module_filenames);
551 // After rebuilding the container we need to update the injected
553 $container = $this->kernel->getContainer();
554 $this->moduleHandler = $container->get('module_handler');
560 public function validateUninstall(array $module_list) {
562 foreach ($module_list as $module) {
563 foreach ($this->uninstallValidators as $validator) {
564 $validation_reasons = $validator->validate($module);
565 if (!empty($validation_reasons)) {
566 if (!isset($reasons[$module])) {
567 $reasons[$module] = [];
569 $reasons[$module] = array_merge($reasons[$module], $validation_reasons);