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 while loop continues.
97 while (list($module) = each($module_list)) {
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 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
308 if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
309 // Rebuild routes after installing module. This is done here on top of
310 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
311 // fastCGI which executes ::destruct() after the module installation
312 // page was sent already.
313 \Drupal::service('router.builder')->rebuild();
316 $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
325 public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
326 // Get all module data so we can find dependencies and sort.
327 $module_data = system_rebuild_module_data();
328 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
329 if (array_diff_key($module_list, $module_data)) {
330 // One or more of the given modules doesn't exist.
334 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
335 $installed_modules = $extension_config->get('module') ?: [];
336 if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
337 // Nothing to do. All modules already uninstalled.
341 if ($uninstall_dependents) {
342 // Add dependent modules to the list. The new modules will be processed as
343 // the while loop continues.
344 $profile = drupal_get_profile();
345 while (list($module) = each($module_list)) {
346 foreach (array_keys($module_data[$module]->required_by) as $dependent) {
347 if (!isset($module_data[$dependent])) {
348 // The dependent module does not exist.
352 // Skip already uninstalled modules.
353 if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
354 $module_list[$dependent] = $dependent;
360 // Use the validators and throw an exception with the reasons.
361 if ($reasons = $this->validateUninstall($module_list)) {
362 foreach ($reasons as $reason) {
363 $reason_message[] = implode(', ', $reason);
365 throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
367 // Set the actual module weights.
368 $module_list = array_map(function ($module) use ($module_data) {
369 return $module_data[$module]->sort;
372 // Sort the module list by their weights.
374 $module_list = array_keys($module_list);
376 // Only process modules that are enabled. A module is only enabled if it is
377 // configured as enabled. Custom or overridden module handlers might contain
378 // the module already, which means that it might be loaded, but not
379 // necessarily installed.
380 foreach ($module_list as $module) {
382 // Clean up all entity bundles (including fields) of every entity type
383 // provided by the module that is being uninstalled.
384 // @todo Clean this up in https://www.drupal.org/node/2350111.
385 $entity_manager = \Drupal::entityManager();
386 foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
387 if ($entity_type->getProvider() == $module) {
388 foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
389 $entity_manager->onBundleDelete($bundle, $entity_type_id);
394 // Allow modules to react prior to the uninstallation of a module.
395 $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
397 // Uninstall the module.
398 module_load_install($module);
399 $this->moduleHandler->invoke($module, 'uninstall');
401 // Remove all configuration belonging to the module.
402 \Drupal::service('config.manager')->uninstall('module', $module);
404 // In order to make uninstalling transactional if anything uses routes.
405 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
406 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
408 // Notify interested components that this module's entity types are being
409 // deleted. For example, a SQL-based storage handler can use this as an
410 // opportunity to drop the corresponding database tables.
411 // @todo Clean this up in https://www.drupal.org/node/2350111.
412 $update_manager = \Drupal::entityDefinitionUpdateManager();
413 foreach ($entity_manager->getDefinitions() as $entity_type) {
414 if ($entity_type->getProvider() == $module) {
415 $update_manager->uninstallEntityType($entity_type);
417 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
418 // The module being installed may be adding new fields to existing
419 // entity types. Field definitions for any entity type defined by
420 // the module are handled in the if branch.
421 $entity_type_id = $entity_type->id();
422 /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
423 $storage = $entity_manager->getStorage($entity_type_id);
424 foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
425 // @todo We need to trigger field purging here.
426 // See https://www.drupal.org/node/2282119.
427 if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
428 $update_manager->uninstallFieldStorageDefinition($storage_definition);
434 // Remove the schema.
435 drupal_uninstall_schema($module);
437 // Remove the module's entry from the config. Don't check schema when
438 // uninstalling a module since we are only clearing a key.
439 \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
441 // Update the module handler to remove the module.
442 // The current ModuleHandler instance is obsolete with the kernel rebuild
444 $module_filenames = $this->moduleHandler->getModuleList();
445 unset($module_filenames[$module]);
446 $this->moduleHandler->setModuleList($module_filenames);
448 // Remove any potential cache bins provided by the module.
449 $this->removeCacheBins($module);
451 // Clear the static cache of system_rebuild_module_data() to pick up the
452 // new module, since it merges the installation status of modules into
453 // its statically cached list.
454 drupal_static_reset('system_rebuild_module_data');
456 // Clear plugin manager caches.
457 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
459 // Update the kernel to exclude the uninstalled modules.
460 $this->updateKernel($module_filenames);
462 // Update the theme registry to remove the newly uninstalled module.
463 drupal_theme_rebuild();
465 // Modules can alter theme info, so refresh theme data.
466 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
467 // causes a circular service dependency.
468 // @see https://www.drupal.org/node/2208429
469 \Drupal::service('theme_handler')->refreshInfo();
471 \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
473 $schema_store = \Drupal::keyValue('system.schema');
474 $schema_store->delete($module);
476 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
477 $post_update_registry = \Drupal::service('update.post_update_registry');
478 $post_update_registry->filterOutInvokedUpdatesByModule($module);
480 // Rebuild routes after installing module. This is done here on top of
481 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
482 // fastCGI which executes ::destruct() after the Module uninstallation page
484 \Drupal::service('router.builder')->rebuild();
485 drupal_get_installed_schema_version(NULL, TRUE);
487 // Let other modules react.
488 $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
490 // Flush all persistent caches.
491 // Any cache entry might implicitly depend on the uninstalled modules,
492 // so clear all of them explicitly.
493 $this->moduleHandler->invokeAll('cache_flush');
494 foreach (Cache::getBins() as $service_id => $cache_backend) {
495 $cache_backend->deleteAll();
502 * Helper method for removing all cache bins registered by a given module.
504 * @param string $module
505 * The name of the module for which to remove all registered cache bins.
507 protected function removeCacheBins($module) {
508 $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
509 if (!file_exists($service_yaml_file)) {
513 $definitions = Yaml::decode(file_get_contents($service_yaml_file));
515 $cache_bin_services = array_filter(
516 isset($definitions['services']) ? $definitions['services'] : [],
517 function ($definition) {
518 $tags = isset($definition['tags']) ? $definition['tags'] : [];
519 foreach ($tags as $tag) {
520 if (isset($tag['name']) && ($tag['name'] == 'cache.bin')) {
528 foreach (array_keys($cache_bin_services) as $service_id) {
529 $backend = $this->kernel->getContainer()->get($service_id);
530 if ($backend instanceof CacheBackendInterface) {
531 $backend->removeBin();
537 * Updates the kernel module list.
539 * @param string $module_filenames
540 * The list of installed modules.
542 protected function updateKernel($module_filenames) {
543 // This reboots the kernel to register the module's bundle and its services
544 // in the service container. The $module_filenames argument is taken over as
545 // %container.modules% parameter, which is passed to a fresh ModuleHandler
546 // instance upon first retrieval.
547 $this->kernel->updateModules($module_filenames, $module_filenames);
548 // After rebuilding the container we need to update the injected
550 $container = $this->kernel->getContainer();
551 $this->moduleHandler = $container->get('module_handler');
557 public function validateUninstall(array $module_list) {
559 foreach ($module_list as $module) {
560 foreach ($this->uninstallValidators as $validator) {
561 $validation_reasons = $validator->validate($module);
562 if (!empty($validation_reasons)) {
563 if (!isset($reasons[$module])) {
564 $reasons[$module] = [];
566 $reasons[$module] = array_merge($reasons[$module], $validation_reasons);