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 // Check the validity of the default configuration. This will throw
140 // exceptions if the configuration is not valid.
141 $config_installer->checkConfigurationToInstall('module', $module);
143 // Save this data without checking schema. This is a performance
144 // improvement for module installation.
146 ->set("module.$module", 0)
147 ->set('module', module_config_sort($extension_config->get('module')))
150 // Prepare the new module list, sorted by weight, including filenames.
151 // This list is used for both the ModuleHandler and DrupalKernel. It
152 // needs to be kept in sync between both. A DrupalKernel reboot or
153 // rebuild will automatically re-instantiate a new ModuleHandler that
154 // uses the new module list of the kernel. However, DrupalKernel does
155 // not cause any modules to be loaded.
156 // Furthermore, the currently active (fixed) module list can be
157 // different from the configured list of enabled modules. For all active
158 // modules not contained in the configured enabled modules, we assume a
160 $current_module_filenames = $this->moduleHandler->getModuleList();
161 $current_modules = array_fill_keys(array_keys($current_module_filenames), 0);
162 $current_modules = module_config_sort(array_merge($current_modules, $extension_config->get('module')));
163 $module_filenames = [];
164 foreach ($current_modules as $name => $weight) {
165 if (isset($current_module_filenames[$name])) {
166 $module_filenames[$name] = $current_module_filenames[$name];
169 $module_path = drupal_get_path('module', $name);
170 $pathname = "$module_path/$name.info.yml";
171 $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
172 $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
176 // Update the module handler in order to load the module's code.
177 // This allows the module to participate in hooks and its existence to
178 // be discovered by other modules.
179 // The current ModuleHandler instance is obsolete with the kernel
181 $this->moduleHandler->setModuleList($module_filenames);
182 $this->moduleHandler->load($module);
183 module_load_install($module);
185 // Clear the static cache of system_rebuild_module_data() to pick up the
186 // new module, since it merges the installation status of modules into
187 // its statically cached list.
188 drupal_static_reset('system_rebuild_module_data');
190 // Update the kernel to include it.
191 $this->updateKernel($module_filenames);
193 // Allow modules to react prior to the installation of a module.
194 $this->moduleHandler->invokeAll('module_preinstall', [$module]);
196 // Now install the module's schema if necessary.
197 drupal_install_schema($module);
199 // Clear plugin manager caches.
200 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
202 // Set the schema version to the number of the last update provided by
203 // the module, or the minimum core schema version.
204 $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
205 $versions = drupal_get_schema_versions($module);
207 $version = max(max($versions), $version);
210 // Notify interested components that this module's entity types and
211 // field storage definitions are new. For example, a SQL-based storage
212 // handler can use this as an opportunity to create the necessary
214 // @todo Clean this up in https://www.drupal.org/node/2350111.
215 $entity_manager = \Drupal::entityManager();
216 $update_manager = \Drupal::entityDefinitionUpdateManager();
217 foreach ($entity_manager->getDefinitions() as $entity_type) {
218 if ($entity_type->getProvider() == $module) {
219 $update_manager->installEntityType($entity_type);
221 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
222 // The module being installed may be adding new fields to existing
223 // entity types. Field definitions for any entity type defined by
224 // the module are handled in the if branch.
225 foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
226 if ($storage_definition->getProvider() == $module) {
227 // If the module being installed is also defining a storage key
228 // for the entity type, the entity schema may not exist yet. It
229 // will be created later in that case.
231 $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
233 catch (EntityStorageException $e) {
234 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()]);
241 // Install default configuration of the module.
242 $config_installer = \Drupal::service('config.installer');
246 ->setSourceStorage($source_storage);
248 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
250 // If the module has no current updates, but has some that were
251 // previously removed, set the version to the value of
252 // hook_update_last_removed().
253 if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
254 $version = max($version, $last_removed);
256 drupal_set_installed_schema_version($module, $version);
258 // Ensure that all post_update functions are registered already.
259 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
260 $post_update_registry = \Drupal::service('update.post_update_registry');
261 $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
263 // Record the fact that it was installed.
264 $modules_installed[] = $module;
266 // Drupal's stream wrappers needs to be re-registered in case a
267 // module-provided stream wrapper is used later in the same request. In
268 // particular, this happens when installing Drupal via Drush, as the
269 // 'translations' stream wrapper is provided by Interface Translation
270 // module and is later used to import translations.
271 \Drupal::service('stream_wrapper_manager')->register();
273 // Update the theme registry to include it.
274 drupal_theme_rebuild();
276 // Modules can alter theme info, so refresh theme data.
277 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
278 // causes a circular service dependency.
279 // @see https://www.drupal.org/node/2208429
280 \Drupal::service('theme_handler')->refreshInfo();
282 // In order to make uninstalling transactional if anything uses routes.
283 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
284 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
286 // Allow the module to perform install tasks.
287 $this->moduleHandler->invoke($module, 'install');
289 // Record the fact that it was installed.
290 \Drupal::logger('system')->info('%module module installed.', ['%module' => $module]);
294 // If any modules were newly installed, invoke hook_modules_installed().
295 if (!empty($modules_installed)) {
296 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old'));
297 if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
298 // Rebuild routes after installing module. This is done here on top of
299 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
300 // fastCGI which executes ::destruct() after the module installation
301 // page was sent already.
302 \Drupal::service('router.builder')->rebuild();
305 $this->moduleHandler->invokeAll('modules_installed', [$modules_installed]);
314 public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
315 // Get all module data so we can find dependencies and sort.
316 $module_data = system_rebuild_module_data();
317 $module_list = $module_list ? array_combine($module_list, $module_list) : [];
318 if (array_diff_key($module_list, $module_data)) {
319 // One or more of the given modules doesn't exist.
323 $extension_config = \Drupal::configFactory()->getEditable('core.extension');
324 $installed_modules = $extension_config->get('module') ?: [];
325 if (!$module_list = array_intersect_key($module_list, $installed_modules)) {
326 // Nothing to do. All modules already uninstalled.
330 if ($uninstall_dependents) {
331 // Add dependent modules to the list. The new modules will be processed as
332 // the while loop continues.
333 $profile = drupal_get_profile();
334 while (list($module) = each($module_list)) {
335 foreach (array_keys($module_data[$module]->required_by) as $dependent) {
336 if (!isset($module_data[$dependent])) {
337 // The dependent module does not exist.
341 // Skip already uninstalled modules.
342 if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
343 $module_list[$dependent] = $dependent;
349 // Use the validators and throw an exception with the reasons.
350 if ($reasons = $this->validateUninstall($module_list)) {
351 foreach ($reasons as $reason) {
352 $reason_message[] = implode(', ', $reason);
354 throw new ModuleUninstallValidatorException('The following reasons prevent the modules from being uninstalled: ' . implode('; ', $reason_message));
356 // Set the actual module weights.
357 $module_list = array_map(function ($module) use ($module_data) {
358 return $module_data[$module]->sort;
361 // Sort the module list by their weights.
363 $module_list = array_keys($module_list);
365 // Only process modules that are enabled. A module is only enabled if it is
366 // configured as enabled. Custom or overridden module handlers might contain
367 // the module already, which means that it might be loaded, but not
368 // necessarily installed.
369 foreach ($module_list as $module) {
371 // Clean up all entity bundles (including fields) of every entity type
372 // provided by the module that is being uninstalled.
373 // @todo Clean this up in https://www.drupal.org/node/2350111.
374 $entity_manager = \Drupal::entityManager();
375 foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
376 if ($entity_type->getProvider() == $module) {
377 foreach (array_keys($entity_manager->getBundleInfo($entity_type_id)) as $bundle) {
378 $entity_manager->onBundleDelete($bundle, $entity_type_id);
383 // Allow modules to react prior to the uninstallation of a module.
384 $this->moduleHandler->invokeAll('module_preuninstall', [$module]);
386 // Uninstall the module.
387 module_load_install($module);
388 $this->moduleHandler->invoke($module, 'uninstall');
390 // Remove all configuration belonging to the module.
391 \Drupal::service('config.manager')->uninstall('module', $module);
393 // In order to make uninstalling transactional if anything uses routes.
394 \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider'));
395 \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder'));
397 // Notify interested components that this module's entity types are being
398 // deleted. For example, a SQL-based storage handler can use this as an
399 // opportunity to drop the corresponding database tables.
400 // @todo Clean this up in https://www.drupal.org/node/2350111.
401 $update_manager = \Drupal::entityDefinitionUpdateManager();
402 foreach ($entity_manager->getDefinitions() as $entity_type) {
403 if ($entity_type->getProvider() == $module) {
404 $update_manager->uninstallEntityType($entity_type);
406 elseif ($entity_type->entityClassImplements(FieldableEntityInterface::CLASS)) {
407 // The module being installed may be adding new fields to existing
408 // entity types. Field definitions for any entity type defined by
409 // the module are handled in the if branch.
410 $entity_type_id = $entity_type->id();
411 /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
412 $storage = $entity_manager->getStorage($entity_type_id);
413 foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
414 // @todo We need to trigger field purging here.
415 // See https://www.drupal.org/node/2282119.
416 if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
417 $update_manager->uninstallFieldStorageDefinition($storage_definition);
423 // Remove the schema.
424 drupal_uninstall_schema($module);
426 // Remove the module's entry from the config. Don't check schema when
427 // uninstalling a module since we are only clearing a key.
428 \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
430 // Update the module handler to remove the module.
431 // The current ModuleHandler instance is obsolete with the kernel rebuild
433 $module_filenames = $this->moduleHandler->getModuleList();
434 unset($module_filenames[$module]);
435 $this->moduleHandler->setModuleList($module_filenames);
437 // Remove any potential cache bins provided by the module.
438 $this->removeCacheBins($module);
440 // Clear the static cache of system_rebuild_module_data() to pick up the
441 // new module, since it merges the installation status of modules into
442 // its statically cached list.
443 drupal_static_reset('system_rebuild_module_data');
445 // Clear plugin manager caches.
446 \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
448 // Update the kernel to exclude the uninstalled modules.
449 $this->updateKernel($module_filenames);
451 // Update the theme registry to remove the newly uninstalled module.
452 drupal_theme_rebuild();
454 // Modules can alter theme info, so refresh theme data.
455 // @todo ThemeHandler cannot be injected into ModuleHandler, since that
456 // causes a circular service dependency.
457 // @see https://www.drupal.org/node/2208429
458 \Drupal::service('theme_handler')->refreshInfo();
460 \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
462 $schema_store = \Drupal::keyValue('system.schema');
463 $schema_store->delete($module);
465 /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
466 $post_update_registry = \Drupal::service('update.post_update_registry');
467 $post_update_registry->filterOutInvokedUpdatesByModule($module);
469 // Rebuild routes after installing module. This is done here on top of
470 // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
471 // fastCGI which executes ::destruct() after the Module uninstallation page
473 \Drupal::service('router.builder')->rebuild();
474 drupal_get_installed_schema_version(NULL, TRUE);
476 // Let other modules react.
477 $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list]);
479 // Flush all persistent caches.
480 // Any cache entry might implicitly depend on the uninstalled modules,
481 // so clear all of them explicitly.
482 $this->moduleHandler->invokeAll('cache_flush');
483 foreach (Cache::getBins() as $service_id => $cache_backend) {
484 $cache_backend->deleteAll();
491 * Helper method for removing all cache bins registered by a given module.
493 * @param string $module
494 * The name of the module for which to remove all registered cache bins.
496 protected function removeCacheBins($module) {
497 // Remove any cache bins defined by a module.
498 $service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
499 if (file_exists($service_yaml_file)) {
500 $definitions = Yaml::decode(file_get_contents($service_yaml_file));
501 if (isset($definitions['services'])) {
502 foreach ($definitions['services'] as $id => $definition) {
503 if (isset($definition['tags'])) {
504 foreach ($definition['tags'] as $tag) {
505 // This works for the default cache registration and even in some
506 // cases when a non-default "super" factory is used. That should
507 // be extremely rare.
508 if ($tag['name'] == 'cache.bin' && isset($definition['factory_service']) && isset($definition['factory_method']) && !empty($definition['arguments'])) {
510 $factory = \Drupal::service($definition['factory_service']);
511 if (method_exists($factory, $definition['factory_method'])) {
512 $backend = call_user_func_array([$factory, $definition['factory_method']], $definition['arguments']);
513 if ($backend instanceof CacheBackendInterface) {
514 $backend->removeBin();
518 catch (\Exception $e) {
519 watchdog_exception('system', $e, 'Failed to remove cache bin defined by the service %id.', ['%id' => $id]);
530 * Updates the kernel module list.
532 * @param string $module_filenames
533 * The list of installed modules.
535 protected function updateKernel($module_filenames) {
536 // This reboots the kernel to register the module's bundle and its services
537 // in the service container. The $module_filenames argument is taken over as
538 // %container.modules% parameter, which is passed to a fresh ModuleHandler
539 // instance upon first retrieval.
540 $this->kernel->updateModules($module_filenames, $module_filenames);
541 // After rebuilding the container we need to update the injected
543 $container = $this->kernel->getContainer();
544 $this->moduleHandler = $container->get('module_handler');
550 public function validateUninstall(array $module_list) {
552 foreach ($module_list as $module) {
553 foreach ($this->uninstallValidators as $validator) {
554 $validation_reasons = $validator->validate($module);
555 if (!empty($validation_reasons)) {
556 if (!isset($reasons[$module])) {
557 $reasons[$module] = [];
559 $reasons[$module] = array_merge($reasons[$module], $validation_reasons);