diff --git a/core/core.services.yml b/core/core.services.yml index 2bc5edc..8717ce2 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -406,6 +406,9 @@ services: - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator } arguments: ['@app.root', '@module_handler', '@kernel'] lazy: true + module_listing: + class: Drupal\Core\Extension\ModuleExtensionList + arguments: ['@app.root', 'module', '@cache.default', '@info_parser', '@module_handler', '@config.factory'] content_uninstall_validator: class: Drupal\Core\Entity\ContentUninstallValidator tags: diff --git a/core/includes/module.inc b/core/includes/module.inc index 8f8bf9c..7f699dd 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -59,6 +59,7 @@ function system_list($type) { function system_list_reset() { drupal_static_reset('system_list'); drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); \Drupal::cache('bootstrap')->delete('system_list'); } diff --git a/core/lib/Drupal/Core/Extension/ExtensionList.php b/core/lib/Drupal/Core/Extension/ExtensionList.php new file mode 100644 index 0000000..bf60c89 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ExtensionList.php @@ -0,0 +1,206 @@ +root = $root; + $this->type = $type; + $this->cache = $cache; + $this->infoParser = $info_parser; + $this->moduleHandler = $module_handler; + } + + /** + * Resets the stored extension list. + */ + public function reset() { + $this->extensions = NULL; + $this->cache->delete($this->getCacheId()); + } + + /** + * Returns the used cache ID. + * + * @return string + */ + protected function getCacheId() { + return 'core.extension_listing.' . $this->type; + } + + /** + * Returns the human readable name of the extension. + * + * @param string $name + * The extension name. + * + * @return string + * The human readable name of the extension. + */ + public function getName($name) { + $extensions = $this->listExtensions(); + if (isset($extensions[$name])) { + return $extensions[$name]->info['name']; + } + throw new \InvalidArgumentException(SafeMarkup::format('The @type %name does not exist.', ['@type' => $this->type, '%name' => $name])); + } + + /** + * Returns a single extension. + * + * @param string $name + * The extension name. + * + * @return \Drupal\Core\Extension\Extension + */ + public function getExtension($name) { + $extensions = $this->listExtensions(); + if (!isset($extensions[$name])) { + return $extensions[$name]; + } + + throw new \InvalidArgumentException(SafeMarkup::format('The @type %name does not exist.', ['@type' => $this->type, '%name' => $name])); + } + + /** + * Returns all available extensions. + * + * @return \Drupal\Core\Extension\Extension[] + */ + public function listExtensions() { + if (isset($this->extensions)) { + return $this->extensions; + } + if ($extensions = $this->cache->get($this->getCacheId())) { + $this->extensions = $extensions; + return $extensions; + } + $extensions = $this->doListExtensions(); + $this->cache->get($this->getCacheId()); + $this->extensions = $extensions; + return $this->extensions; + } + + /** + * Scans the available extensions. + * + * Overriding this method gives other code the chance to add additional + * extensions to this raw listing. + * + * @param \Drupal\Core\Extension\ExtensionDiscovery $discovery + * The extension discovery. + * + * @return \Drupal\Core\Extension\Extension[] + */ + protected function doScanExtensions(ExtensionDiscovery $discovery) { + return $discovery->scan($this->type); + } + + /** + * Build the actual list of extensions before caching it. + * + * @return \Drupal\Core\Extension\Extension[] + */ + protected function doListExtensions() { + $listing = new ExtensionDiscovery($this->root); + + // Find extensions. + $extensions = $this->doScanExtensions($listing); + + // Read info files for each extension. + foreach ($extensions as $key => $extension) { + // Look for the info file. + $extension->info = $this->infoParser->parse($extension->getPathname()); + + // Add the info file modification time, so it becomes available for + // contributed extensions to use for ordering extension lists. + $extension->info['mtime'] = $extension->getMTime(); + + // Merge in defaults and save. + $extensions[$key]->info = $extension->info + $this->defaults; + + // Invoke hook_system_info_alter() to give installed modules a chance to + // modify the data in the .info.yml files if necessary. + // @todo Remove $type argument, obsolete with $module->getType(). + $this->moduleHandler->alter('system_info', $extensions[$key]->info, $extensions[$key], $this->type); + } + + return $extensions; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php new file mode 100644 index 0000000..c6461d8 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php @@ -0,0 +1,160 @@ + [], + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + 'php' => DRUPAL_MINIMUM_PHP, + ]; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a new ModuleExtensionList instance. + * + * @param string $root + * The app root. + * @param string $type + * The extension type. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache. + * @param \Drupal\Core\Extension\InfoParserInterface $info_parser + * The info parser. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) { + parent::__construct($root, $type, $cache, $info_parser, $module_handler); + + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + protected function doScanExtensions(ExtensionDiscovery $discovery) { + $extensions = parent::doScanExtensions($discovery); + + // Find installation profiles. + $profiles = $discovery->scan('profile'); + + // Include the installation profile in modules that are loaded. + if ($profile = drupal_get_profile()) { + $extensions[$profile] = $profiles[$profile]; + // Installation profile hooks are always executed last. + $extensions[$profile]->weight = 1000; + } + + return $extensions; + } + + /** + * {@inheritdoc} + */ + protected function doListExtensions() { + $listing = new ExtensionDiscovery($this->root); + $extensions = parent::doListExtensions(); + + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan requires knowing what the active profile is. + // @todo Remove as part of https://www.drupal.org/node/2186491 + $profiles = $listing->scan('profile'); + $profile = drupal_get_profile(); + if ($profile && isset($profiles[$profile])) { + // Prime the drupal_get_filename() static cache with the profile info file + // location so we can use drupal_get_path() on the active profile during + // the module scan. + // @todo Remove as part of https://www.drupal.org/node/2186491 + drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname()); + } + + // It is possible that a module was marked as required by + // hook_system_info_alter() and modules that it depends on are not required. + foreach ($extensions as $extension) { + $this->moduleDataEnsureRequired($extension, $extensions); + } + + if ($profile = drupal_get_profile()) { + // Installation profiles are hidden by default, unless explicitly + // specified otherwise in the .info.yml file. + if (!isset($extensions[$profile]->info['hidden'])) { + $extensions[$profile]->info['hidden'] = TRUE; + } + + if (isset($extensions[$profile])) { + // The installation profile is required, if it's a valid module. + $extensions[$profile]->info['required'] = TRUE; + // Add a default distribution name if the profile did not provide one. + // @see install_profile_info() + // @see drupal_install_profile_distribution_name() + if (!isset($extensions[$profile]->info['distribution']['name'])) { + $extensions[$profile]->info['distribution']['name'] = 'Drupal'; + } + } + } + + // Add status, weight, and schema version. + $installed_modules = $this->configFactory->get('core.extension')->get('module') ?: []; + foreach ($extensions as $name => $module) { + $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0; + $module->status = (int) isset($installed_modules[$name]); + $module->schema_version = SCHEMA_UNINSTALLED; + $files[$name] = $module->getPathname(); + } + $extensions = $this->moduleHandler->buildModuleDependencies($extensions); + + return $extensions; + } + + /** + * Ensures that dependencies of required modules are also required. + * + * @param \Drupal\Core\Extension\Extension $module + * The module info. + * @param \Drupal\Core\Extension\Extension[] $modules + * The array of all module info. + */ + protected function moduleDataEnsureRequired(Extension $module, array $modules = []) { + if (!empty($module->info['required'])) { + foreach ($module->info['dependencies'] as $dependency) { + $dependency_name = ModuleHandler::parseDependency($dependency)['name']; + if (!isset($modules[$dependency_name]->info['required'])) { + $modules[$dependency_name]->info['required'] = TRUE; + $modules[$dependency_name]->info['explanation'] = $this->t('Dependency of required module @module', array('@module' => $module->info['name'])); + // Ensure any dependencies it has are required. + $this->moduleDataEnsureRequired($modules[$dependency_name], $modules); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 93ca066..3c4a0bb 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -710,8 +710,7 @@ public function getModuleDirectories() { * {@inheritdoc} */ public function getName($module) { - $info = system_get_info('module', $module); - return isset($info['name']) ? $info['name'] : $module; + return \Drupal::service('module_listing')->getName($module); } } diff --git a/core/modules/book/src/Tests/BookUninstallTest.php b/core/modules/book/src/Tests/BookUninstallTest.php index 317a16a..ab7e065 100644 --- a/core/modules/book/src/Tests/BookUninstallTest.php +++ b/core/modules/book/src/Tests/BookUninstallTest.php @@ -44,7 +44,8 @@ protected function setUp() { */ public function testBookUninstall() { // No nodes exist. - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); $content_type = NodeType::create(array( @@ -62,7 +63,8 @@ public function testBookUninstall() { $node->save(); // One node in a book but not of type book. - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertTrue($module_data['book']->info['required'], 'The book module is required.'); $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that is part of a book.')); @@ -72,26 +74,30 @@ public function testBookUninstall() { // Two nodes, one in a book but not of type book and one book node (which is // not in a book). - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertTrue($module_data['book']->info['required'], 'The book module is required.'); $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that is part of a book.')); $node->delete(); // One node of type book but not actually part of a book. - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertTrue($module_data['book']->info['required'], 'The book module is required.'); $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that has the Book content type.')); $book_node->delete(); // No nodes exist therefore the book module is not required. - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); $node = Node::create(array('type' => $content_type->id())); $node->save(); // One node exists but is not part of a book therefore the book module is // not required. - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); // Uninstall the Book module and check the node type is deleted. diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 483bc22..36ce085 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -424,7 +424,8 @@ public function testDependencyRemoval() { ]; $filter_format->setFilterConfig('filter_test_restrict_tags_and_attributes', $filter_config)->save(); - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertTrue($module_data['filter_test']->info['required'], 'The filter_test module is required.'); $this->assertEqual($module_data['filter_test']->info['explanation'], SafeMarkup::format('Provides a filter plugin that is in use in the following filter formats: %formats', array('%formats' => $filter_format->label()))); @@ -443,7 +444,8 @@ public function testDependencyRemoval() { drupal_static_reset('filter_formats'); \Drupal::entityManager()->getStorage('filter_format')->resetCache(); - $module_data = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $module_data = \Drupal::service('module_listing')->listExtensions(); $this->assertFalse(isset($module_data['filter_test']->info['required']), 'The filter_test module is required.'); // Verify that a dependency exists on the module that provides the filter diff --git a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php index a9fa3b2..206dc80 100644 --- a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php +++ b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php @@ -128,6 +128,7 @@ function testDependencyResolution() { // Nothing should be installed. \Drupal::state()->set('module_test.dependency', 'missing dependency'); drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); try { $result = $this->moduleInstaller()->install(array('color')); @@ -143,6 +144,7 @@ function testDependencyResolution() { // Color module depends on Config. Config depends on Help module. \Drupal::state()->set('module_test.dependency', 'dependency'); drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); @@ -175,6 +177,7 @@ function testDependencyResolution() { // sure that Drupal\Core\Extension\ModuleHandler::install() still works. \Drupal::state()->set('module_test.dependency', 'version dependency'); drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 8acaf33..e2465f4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -882,105 +882,8 @@ function system_get_info($type, $name = NULL) { * An associative array of module information. */ function _system_rebuild_module_data() { - $listing = new ExtensionDiscovery(\Drupal::root()); - - // Find installation profiles. This needs to happen before performing a - // module scan as the module scan requires knowing what the active profile is. - // @todo Remove as part of https://www.drupal.org/node/2186491. - $profiles = $listing->scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { - // Prime the drupal_get_filename() static cache with the profile info file - // location so we can use drupal_get_path() on the active profile during - // the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491. - drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname()); - } - - // Find modules. - $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. - if ($profile) { - $modules[$profile] = $profiles[$profile]; - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; - } - - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'description' => '', - 'package' => 'Other', - 'version' => NULL, - 'php' => DRUPAL_MINIMUM_PHP, - ); - - // Read info files for each module. - foreach ($modules as $key => $module) { - // Look for the info file. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); - - // Add the info file modification time, so it becomes available for - // contributed modules to use for ordering module lists. - $module->info['mtime'] = $module->getMTime(); - - // Merge in defaults and save. - $modules[$key]->info = $module->info + $defaults; - - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { - $modules[$key]->info['hidden'] = TRUE; - } - - // Invoke hook_system_info_alter() to give installed modules a chance to - // modify the data in the .info.yml files if necessary. - // @todo Remove $type argument, obsolete with $module->getType(). - $type = 'module'; - \Drupal::moduleHandler()->alter('system_info', $modules[$key]->info, $modules[$key], $type); - } - - // It is possible that a module was marked as required by - // hook_system_info_alter() and modules that it depends on are not required. - foreach ($modules as $module) { - _system_rebuild_module_data_ensure_required($module, $modules); - } - - - if ($profile && isset($modules[$profile])) { - // The installation profile is required, if it's a valid module. - $modules[$profile]->info['required'] = TRUE; - // Add a default distribution name if the profile did not provide one. - // @see install_profile_info() - // @see drupal_install_profile_distribution_name() - if (!isset($modules[$profile]->info['distribution']['name'])) { - $modules[$profile]->info['distribution']['name'] = 'Drupal'; - } - } - - return $modules; -} - -/** - * Ensures that dependencies of required modules are also required. - * - * @param \Drupal\Core\Extension\Extension $module - * The module info. - * @param \Drupal\Core\Extension\Extension[] $modules - * The array of all module info. - */ -function _system_rebuild_module_data_ensure_required($module, &$modules) { - if (!empty($module->info['required'])) { - foreach ($module->info['dependencies'] as $dependency) { - $dependency_name = ModuleHandler::parseDependency($dependency)['name']; - if (!isset($modules[$dependency_name]->info['required'])) { - $modules[$dependency_name]->info['required'] = TRUE; - $modules[$dependency_name]->info['explanation'] = t('Dependency of required module @module', array('@module' => $module->info['name'])); - // Ensure any dependencies it has are required. - _system_rebuild_module_data_ensure_required($modules[$dependency_name], $modules); - } - } - } + \Drupal::service('module_listing')->reset(); + return \Drupal::service('module_listing')->listExtensions(); } /** @@ -995,19 +898,13 @@ function system_rebuild_module_data() { // combined into one variable, because the $modules_cache variable is reset by // reference from system_list_reset() during the rebuild. if (!isset($modules_cache)) { - $modules = _system_rebuild_module_data(); + \Drupal::service('module_listing')->reset(); + $modules_cache = \Drupal::service('module_listing')->listExtensions(); $files = array(); - ksort($modules); - // Add status, weight, and schema version. - $installed_modules = \Drupal::config('core.extension')->get('module') ?: array(); - foreach ($modules as $name => $module) { - $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0; - $module->status = (int) isset($installed_modules[$name]); - $module->schema_version = SCHEMA_UNINSTALLED; + ksort(modules_cache); + foreach ($modules_cache as $name => $module) { $files[$name] = $module->getPathname(); } - $modules = \Drupal::moduleHandler()->buildModuleDependencies($modules); - $modules_cache = $modules; // Store filenames to allow drupal_get_filename() to retrieve them without // having to rebuild or scan the filesystem.