diff --git a/core/core.services.yml b/core/core.services.yml index d9b1839..2ae0391 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -475,6 +475,14 @@ services: - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator } arguments: ['@app.root', '@module_handler', '@kernel', '@router.builder'] lazy: true + extension.list.module: + class: Drupal\Core\Extension\Hub\ExtensionHubInterface + factory: Drupal\Core\Extension\Hub\ExtensionHub::createForModules + arguments: ['@app.root', '@info_parser', '@module_handler', '@config.factory', '@extension.list.profile', '@cache.default'] + extension.list.profile: + class: Drupal\Core\Extension\Hub\ExtensionHubInterface + factory: Drupal\Core\Extension\Hub\ExtensionHub::createForProfiles + arguments: ['@app.root', '@info_parser', '@module_handler', '@cache.default'] content_uninstall_validator: class: Drupal\Core\Entity\ContentUninstallValidator tags: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 589d451..76c1be7 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -200,11 +200,26 @@ function drupal_get_filename($type, $name, $filename = NULL) { return 'core/core.info.yml'; } - // Profiles are converted into modules in system_rebuild_module_data(). - // @todo Remove false-exposure of profiles as modules. - if ($type == 'profile') { - $type = 'module'; + if ($type === 'module' || $type === 'profile') { + $service_id = 'extension.list.' . $type; + /** @var \Drupal\Core\Extension\ExtensionList $extension_list */ + $extension_list = \Drupal::service($service_id); + if (isset($filename)) { + // Manually add the info file path of an extension. + $extension_list->nameSetFilename($name, $filename); + return; + } + else { + try { + return $extension_list->nameGetFilename($name); + } + catch (\InvalidArgumentException $e) { + // Catch the exception and trigger error to maintain existing behavior. + trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name)), E_USER_WARNING); + } + } } + if (!isset($files[$type])) { $files[$type] = array(); } @@ -213,15 +228,6 @@ function drupal_get_filename($type, $name, $filename = NULL) { $files[$type][$name] = $filename; } elseif (!isset($files[$type][$name])) { - // If the pathname of the requested extension is not known, try to retrieve - // the list of extension pathnames from various providers, checking faster - // providers first. - // Retrieve the current module list (derived from the service container). - if ($type == 'module' && \Drupal::hasService('module_handler')) { - foreach (\Drupal::moduleHandler()->getModuleList() as $module_name => $module) { - $files[$type][$module_name] = $module->getPathname(); - } - } // If still unknown, retrieve the file list prepared in state by // system_rebuild_module_data() and // \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 7a9812f..9c6604c 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -301,12 +301,6 @@ function install_begin_request($class_loader, &$install_state) { // Allow command line scripts to override server variables used by Drupal. require_once __DIR__ . '/bootstrap.inc'; - // Before having installed the system module and being able to do a module - // rebuild, prime the drupal_get_filename() static cache with the module's - // exact location. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml'); - // If the hash salt leaks, it becomes possible to forge a valid testing user // agent, install a new copy of Drupal, and take over the original site. // The user agent header is used to pass a database prefix in the request when diff --git a/core/includes/install.inc b/core/includes/install.inc index 3120799..f35d0f2 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -615,6 +615,11 @@ function drupal_install_system($install_state) { $kernel->rebuildContainer(FALSE); $kernel->prepareLegacyRequest($request); + // Before having installed the system module and being able to do a module + // rebuild, prime the ModuleExtensionList static cache with the module's + // exact location. + \Drupal::service('extension.list.module')->nameSetFilename('system', 'core/modules/system/system.info.yml'); + // Install base system configuration. \Drupal::service('config.installer')->installDefaultConfig('core', 'core'); diff --git a/core/includes/module.inc b/core/includes/module.inc index bc50d2c..3044815 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -57,7 +57,7 @@ function system_list($type) { */ function system_list_reset() { drupal_static_reset('system_list'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('extension.list.module')->reset(); \Drupal::cache('bootstrap')->delete('system_list'); } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index ef0489b..f83bc7c 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -780,7 +780,10 @@ protected function initializeContainer() { if (isset($this->container)) { // Save the id of the currently logged in user. if ($this->container->initialized('current_user')) { - $current_user_id = $this->container->get('current_user')->id(); + /** @var \Drupal\Core\Session\AccountProxyInterface $current_user */ + $current_user = $this->container->get('current_user'); + // Ensure to not accidentally initialize the user. + $current_user_id = $current_user->hasAccount() ? $current_user->id() : 0; } // If there is a session, close and save it. diff --git a/core/lib/Drupal/Core/Extension/Extension.php b/core/lib/Drupal/Core/Extension/Extension.php index 0238714..2c3cf81 100644 --- a/core/lib/Drupal/Core/Extension/Extension.php +++ b/core/lib/Drupal/Core/Extension/Extension.php @@ -45,6 +45,13 @@ class Extension implements \Serializable { protected $root; /** + * Parsed contents of the *.info.yml file. Stays null until initialized. + * + * @var array|null + */ + public $info; + + /** * Constructs a new Extension object. * * @param string $root diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index cdee878..cdbd521 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -18,7 +18,7 @@ * to your settings.php. * */ -class ExtensionDiscovery { +class ExtensionDiscovery implements ExtensionDiscoveryInterface { /** * Origin directory weight: Core. diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscoveryInterface.php b/core/lib/Drupal/Core/Extension/ExtensionDiscoveryInterface.php new file mode 100644 index 0000000..24af3c8 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscoveryInterface.php @@ -0,0 +1,58 @@ +scan('module'); + * @endcode + * + * The following directories will be searched (in the order stated): + * - the core directory; i.e., /core + * - the installation profile directory; e.g., /core/profiles/standard + * - the legacy site-wide directory; i.e., /sites/all + * - the site-wide directory; i.e., / + * - the site-specific directory; e.g., /sites/example.com + * + * To also find test modules, add + * @code + * $settings['extension_discovery_scan_tests'] = TRUE; + * @encode + * to your settings.php. + * + * The information is returned in an associative array, keyed by the extension + * name (without .info.yml extension). Extensions found later in the search + * will take precedence over extensions found earlier - unless they are not + * compatible with the current version of Drupal core. + * + * @param string $type + * The extension type to search for. One of 'profile', 'module', 'theme', or + * 'theme_engine'. + * @param bool $include_tests + * (optional) Whether to explicitly include or exclude test extensions. By + * default, test extensions are only discovered when in a test environment. + * + * @return \Drupal\Core\Extension\Extension[] + * An associative array of Extension objects, keyed by extension name. + */ + public function scan($type, $include_tests = NULL); +} diff --git a/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameList.php b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameList.php new file mode 100644 index 0000000..1d537ca --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameList.php @@ -0,0 +1,40 @@ +extensionList = $extension_list; + } + + /** + * {@inheritdoc} + */ + public function reset() { + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilenames() { + $file_names = []; + $extensions = $this->extensionList->listExtensions(); + ksort($extensions); + foreach ($extensions as $name => $extension) { + $file_names[$name] = $extension->getPathname(); + } + return $file_names; + } +} diff --git a/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListBuffer.php b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListBuffer.php new file mode 100644 index 0000000..ab5887b --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListBuffer.php @@ -0,0 +1,41 @@ +decorated = $decorated; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->fileNames = NULL; + $this->decorated->reset(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilenames() { + return $this->fileNames !== NULL + ? $this->fileNames + : $this->fileNames = $this->decorated->getFilenames(); + } +} diff --git a/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListCache.php b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListCache.php new file mode 100644 index 0000000..b1da4c3 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListCache.php @@ -0,0 +1,59 @@ +decorated = $decorated; + $this->cache = $cache; + $this->cacheId = $cache_id; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->cache->delete($this->cacheId); + $this->decorated->reset(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilenames() { + if ($cache = $this->cache->get($this->cacheId)) { + return $cache->data; + } + // Resolve cache miss. + $file_names = $this->decorated->getFilenames(); + $this->cache->set($this->cacheId, $file_names); + return $file_names; + } +} diff --git a/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListInterface.php b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListInterface.php new file mode 100644 index 0000000..b2ac3a4 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FilenameList/ExtensionFilenameListInterface.php @@ -0,0 +1,21 @@ +type = $type; + $this->source = $source; + $this->infoList = $info_list; + $this->fileList = $file_list; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->source->reset(); + $this->infoList->reset(); + $this->fileList->reset(); + $this->addedFileNames = NULL; + return $this; + } + + /** + * {@inheritdoc} + */ + public function extensionExists($name) { + $extensions = $this->listExtensions(); + return isset($extensions[$name]); + } + + /** + * {@inheritdoc} + */ + public function nameGetLabel($machine_name) { + return $this->nameGetExtension($machine_name)->info['name']; + } + + /** + * {@inheritdoc} + */ + public function nameGetExtension($name) { + $extensions = $this->listExtensions(); + if (isset($extensions[$name])) { + return $extensions[$name]; + } + + throw new \InvalidArgumentException("The {$this->type} $name does not exist."); + } + + /** + * {@inheritdoc} + */ + public function listExtensions() { + return $this->source->listExtensions(); + } + + /** + * {@inheritdoc} + */ + public function nameGetInfo($extension_name) { + // Ensure that $this->extensionInfo is primed. + $allInfo = $this->getAllInfo(); + if (isset($allInfo[$extension_name])) { + return $allInfo[$extension_name]; + } + throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist."); + } + + /** + * {@inheritdoc} + */ + public function getAllInfo() { + return $this->infoList->getAllInfo(); + } + + /** + * {@inheritdoc} + */ + public function getFilenames() { + return $this->fileList->getFilenames(); + } + + /** + * {@inheritdoc} + */ + public function nameSetFilename($extension_name, $filename) { + $this->addedFileNames[$extension_name] = $filename; + } + + /** + * {@inheritdoc} + */ + public function nameGetFilename($extension_name) { + if (isset($this->addedFileNames[$extension_name])) { + return $this->addedFileNames[$extension_name]; + } + $filenames = $this->getFilenames(); + if (isset($filenames[$extension_name])) { + return $filenames[$extension_name]; + } + throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist."); + } + + /** + * {@inheritdoc} + */ + public function nameGetPath($extension_name) { + return dirname($this->nameGetFilename($extension_name)); + } +} diff --git a/core/lib/Drupal/Core/Extension/Hub/ExtensionHubInterface.php b/core/lib/Drupal/Core/Extension/Hub/ExtensionHubInterface.php new file mode 100644 index 0000000..5001aac --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Hub/ExtensionHubInterface.php @@ -0,0 +1,144 @@ +extensionList = $extension_list; + } + + /** + * {@inheritdoc} + */ + public function reset() { + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAllInfo() { + $info = []; + foreach ($this->extensionList->listExtensions() as $name => $extension) { + $info[$name] = $extension->info; + } + return $info; + } +} diff --git a/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListBuffer.php b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListBuffer.php new file mode 100644 index 0000000..27245d3 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListBuffer.php @@ -0,0 +1,41 @@ +decorated = $decorated; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->extensionInfo = NULL; + $this->decorated->reset(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAllInfo() { + return $this->extensionInfo !== NULL + ? $this->extensionInfo + : $this->extensionInfo = $this->decorated->getAllInfo(); + } +} diff --git a/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListCache.php b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListCache.php new file mode 100644 index 0000000..d8458c6 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListCache.php @@ -0,0 +1,56 @@ +decorated = $decorated; + $this->cache = $cache; + $this->cache_id = $cache_id; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->cache->delete($this->cache_id); + $this->decorated->reset(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAllInfo() { + if ($cache = $this->cache->get($this->cache_id)) { + return $cache->data; + } + // Resolve cache miss. + $info = $this->decorated->getAllInfo(); + $this->cache->set($this->cache_id, $info); + return $info; + } +} diff --git a/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListInterface.php b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListInterface.php new file mode 100644 index 0000000..0c57d93 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/InfoList/ExtensionInfoListInterface.php @@ -0,0 +1,26 @@ +info. + */ + public function __construct( + $type, + InfoParserInterface $info_parser, + ModuleHandlerInterface $module_handler, + ExtensionDiscovery $extension_discovery, + array $info_defaults + ) { + $this->type = $type; + $this->infoParser = $info_parser; + $this->moduleHandler = $module_handler; + $this->extensionDiscovery = $extension_discovery; + $this->defaults = $info_defaults; + } + + /** + * {@inheritdoc} + */ + public function reset() { + // Nothing to do. + } + + /** + * Scans the available extensions. + * + * Overriding this method gives other code the chance to add additional + * extensions to this raw listing. + * + * @return \Drupal\Core\Extension\Extension[] + */ + protected function doScanExtensions() { + return $this->extensionDiscovery->scan($this->type); + } + + /** + * Returns all available extensions. + * + * @return \Drupal\Core\Extension\Extension[] + * + * @throws \Drupal\Core\Extension\InfoParserException + * If one of the .info.yml is broken or incomplete. + */ + public function listExtensions() { + // Find extensions. + $extensions = $this->doScanExtensions(); + + // Read info files for each extension. + foreach ($extensions as $name => $extension) { + // @todo Clone the extension object, to prevent side effects? + + // 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. + /** @noinspection PhpUndefinedMethodInspection */ + $extension->info['mtime'] = $extension->getMTime(); + + // Merge in defaults and save. + $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. + $this->moduleHandler->alter('system_info', $extensions[$name]->info, $extensions[$name], $this->type); + } + + return $extensions; + } +} diff --git a/core/lib/Drupal/Core/Extension/List_/ExtensionListBuffer.php b/core/lib/Drupal/Core/Extension/List_/ExtensionListBuffer.php new file mode 100644 index 0000000..1132ee9 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/List_/ExtensionListBuffer.php @@ -0,0 +1,43 @@ +decorated = $decorated; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->decorated->reset(); + $this->extensions = NULL; + return $this; + } + + /** + * Returns all available extensions, with $extension->info filled in. + * + * @return \Drupal\Core\Extension\Extension[] + */ + public function listExtensions() { + return NULL !== $this->extensions + ? $this->extensions + : $this->extensions = $this->decorated->listExtensions(); + } +} diff --git a/core/lib/Drupal/Core/Extension/List_/ExtensionListCache.php b/core/lib/Drupal/Core/Extension/List_/ExtensionListCache.php new file mode 100644 index 0000000..7e10e9c --- /dev/null +++ b/core/lib/Drupal/Core/Extension/List_/ExtensionListCache.php @@ -0,0 +1,58 @@ +cache = $cache; + $this->decorated = $decorated; + $this->cache_id = $cache_id; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->cache->delete($this->cache_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function listExtensions() { + if ($cache = $this->cache->get($this->cache_id)) { + return $cache->data; + } + $extensions = $this->decorated->listExtensions(); + $this->cache->set($this->cache_id, $extensions); + return $extensions; + } +} diff --git a/core/lib/Drupal/Core/Extension/List_/ExtensionListInterface.php b/core/lib/Drupal/Core/Extension/List_/ExtensionListInterface.php new file mode 100644 index 0000000..df866c1 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/List_/ExtensionListInterface.php @@ -0,0 +1,21 @@ +info filled in. + * + * @return \Drupal\Core\Extension\Extension[] + */ + public function listExtensions(); + +} diff --git a/core/lib/Drupal/Core/Extension/List_/ModuleDiscoveryExtensionList.php b/core/lib/Drupal/Core/Extension/List_/ModuleDiscoveryExtensionList.php new file mode 100644 index 0000000..b37ce36 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/List_/ModuleDiscoveryExtensionList.php @@ -0,0 +1,184 @@ + [], + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + 'php' => DRUPAL_MINIMUM_PHP, + ], + $config_factory, + $profile_list); + } + + /** + * @param string $type + * @param \Drupal\Core\Extension\InfoParserInterface $info_parser + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery + * @param array $info_defaults + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * @param \Drupal\Core\Extension\List_\ExtensionListInterface $profile_list + */ + function __construct( + $type, + InfoParserInterface $info_parser, + ModuleHandlerInterface $module_handler, + ExtensionDiscovery $extension_discovery, + array $info_defaults, + ConfigFactoryInterface $config_factory, + ExtensionListInterface $profile_list + ) { + parent::__construct($type, $info_parser, $module_handler, $extension_discovery, $info_defaults); + + $this->configFactory = $config_factory; + $this->profileList = $profile_list; + } + + /** + * {@inheritdoc} + */ + protected function doScanExtensions() { + $extensions = parent::doScanExtensions(); + + // Find installation profiles. + $profiles = $this->profileList->listExtensions(); + + // 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} + */ + public function listExtensions() { + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan needs to know what the active profile is. + $profiles = $this->profileList->listExtensions(); + $profile = drupal_get_profile(); + if ($profile && isset($profiles[$profile])) { + // Set the profile in the ExtensionDiscovery so we can scan from the right + // profile directory. + $this->extensionDiscovery->setProfileDirectories([ + $profile => $profiles[$profile]->getPathname(), + ]); + } + + // Find modules. + $extensions = parent::listExtensions(); + // 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->ensureRequiredDependencies($extension, $extensions); + } + + if ($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) { + /** @noinspection PhpUndefinedFieldInspection */ + $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0; + /** @noinspection PhpUndefinedFieldInspection */ + $module->status = (int) isset($installed_modules[$name]); + /** @noinspection PhpUndefinedFieldInspection */ + $module->schema_version = SCHEMA_UNINSTALLED; + } + $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 ensureRequiredDependencies(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; + // @todo Use an injected (lazy/proxy) translation service. + $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->ensureRequiredDependencies($modules[$dependency_name], $modules); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/List_/ProfileDiscoveryExtensionList.php b/core/lib/Drupal/Core/Extension/List_/ProfileDiscoveryExtensionList.php new file mode 100644 index 0000000..06d903d --- /dev/null +++ b/core/lib/Drupal/Core/Extension/List_/ProfileDiscoveryExtensionList.php @@ -0,0 +1,37 @@ + [], + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + 'php' => DRUPAL_MINIMUM_PHP, + ]); + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 8ea10ad..5316166 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -717,8 +717,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('extension.list.module')->nameGetLabel($module); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index 124efa2..b04a22c 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -306,6 +306,9 @@ public function getModuleDirectories(); * @return string * Returns the human readable name of the module or the machine name passed * in if no matching module is found. + * + * @deprecated in Drupal 8.2.0, will be removed before Drupal 9.0.0. + * Use \Drupal::service('extension.list.module')->nameGetLabel() instead. */ public function getName($module); diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index a903b8a..e2ba5ab 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -166,7 +166,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $module_filenames[$name] = $current_module_filenames[$name]; } else { - $module_path = drupal_get_path('module', $name); + $module_path = \Drupal::service('extension.list.module')->nameGetPath($name); $pathname = "$module_path/$name.info.yml"; $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL; $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename); @@ -182,10 +182,10 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $this->moduleHandler->load($module); module_load_install($module); - // Clear the static cache of system_rebuild_module_data() to pick up the - // new module, since it merges the installation status of modules into - // its statically cached list. - drupal_static_reset('system_rebuild_module_data'); + // Clear the static cache of the "extension.list.module" service to pick + // up the new module, since it merges the installation status of modules + // into its statically cached list. + \Drupal::service('extension.list.module')->reset(); // Update the kernel to include it. $this->updateKernel($module_filenames); @@ -437,10 +437,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove any potential cache bins provided by the module. $this->removeCacheBins($module); - // Clear the static cache of system_rebuild_module_data() to pick up the - // new module, since it merges the installation status of modules into - // its statically cached list. - drupal_static_reset('system_rebuild_module_data'); + // Clear the static cache of the "extension.list.module" service to pick + // up the new module, since it merges the installation status of modules + // into its statically cached list. + \Drupal::service('extension.list.module')->reset(); // Clear plugin manager caches. \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions(); diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index a50a289..7c48619 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -45,6 +45,13 @@ public function setAccount(AccountInterface $account) { /** * {@inheritdoc} */ + public function hasAccount() { + return isset($this->account); + } + + /** + * {@inheritdoc} + */ public function getAccount() { if (!isset($this->account)) { if ($this->initialAccountId) { diff --git a/core/lib/Drupal/Core/Session/AccountProxyInterface.php b/core/lib/Drupal/Core/Session/AccountProxyInterface.php index 378b89d..b7525aa 100644 --- a/core/lib/Drupal/Core/Session/AccountProxyInterface.php +++ b/core/lib/Drupal/Core/Session/AccountProxyInterface.php @@ -6,6 +6,7 @@ * Defines an interface for a service which has the current account stored. * * @ingroup user_api + * @internal */ interface AccountProxyInterface extends AccountInterface { @@ -43,4 +44,12 @@ public function getAccount(); */ public function setInitialAccountId($account_id); + /** + * Checks whether an account is currently wrapped. + * + * @return bool + * TRUE, if an account is currently wrapped. FALSE otherwise. + */ + public function hasAccount(); + } diff --git a/core/lib/Drupal/Core/Updater/Module.php b/core/lib/Drupal/Core/Updater/Module.php index c7cbb7f..4258c90 100644 --- a/core/lib/Drupal/Core/Updater/Module.php +++ b/core/lib/Drupal/Core/Updater/Module.php @@ -49,8 +49,7 @@ public static function getRootDirectoryRelativePath() { public function isInstalled() { // Check if the module exists in the file system, regardless of whether it // is enabled or not. - $modules = \Drupal::state()->get('system.module.files', array()); - return isset($modules[$this->name]); + return \Drupal::service('extension.list.module')->extensionExists($this->name); } /** diff --git a/core/modules/book/tests/src/Kernel/BookUninstallTest.php b/core/modules/book/tests/src/Kernel/BookUninstallTest.php index bcd0812..17f1803 100644 --- a/core/modules/book/tests/src/Kernel/BookUninstallTest.php +++ b/core/modules/book/tests/src/Kernel/BookUninstallTest.php @@ -76,7 +76,7 @@ public function testBookUninstall() { $book_node->delete(); // No nodes exist therefore the book module is not required. - $module_data = _system_rebuild_module_data(); + $module_data = \Drupal::service('extension.list.module')->reset()->listExtensions(); $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); $node = Node::create(array('title' => $this->randomString(), 'type' => $content_type->id())); diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index a7623a9..402133a 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -97,7 +97,7 @@ public function testInstallUninstall() { // Ensure that only core required modules and the install profile can not be uninstalled. $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules)); - $this->assertEqual(['standard', 'system', 'user'], array_keys($validation_reasons)); + $this->assertEqual(['system', 'user', 'standard'], array_keys($validation_reasons)); $modules_to_uninstall = array_filter($all_modules, function ($module) use ($validation_reasons) { // Filter required and not enabled modules. diff --git a/core/modules/filter/tests/src/Kernel/FilterAPITest.php b/core/modules/filter/tests/src/Kernel/FilterAPITest.php index 49dde8a..485fba7 100644 --- a/core/modules/filter/tests/src/Kernel/FilterAPITest.php +++ b/core/modules/filter/tests/src/Kernel/FilterAPITest.php @@ -485,7 +485,7 @@ public function testDependencyRemoval() { drupal_static_reset('filter_formats'); \Drupal::entityManager()->getStorage('filter_format')->resetCache(); - $module_data = _system_rebuild_module_data(); + $module_data = \Drupal::service('extension.list.module')->reset()->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/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 7714259..519d273 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -100,7 +100,7 @@ function __construct($test_id = NULL) { protected function beforePrepareEnvironment() { // Copy/prime extension file lists once to avoid filesystem scans. if (!isset($this->moduleFiles)) { - $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: array(); + $this->moduleFiles = \Drupal::cache('default')->get('system.module.files') ?: array(); $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: array(); } } @@ -222,7 +222,7 @@ protected function setUp() { // Re-inject extension file listings into state, unless the key/value // service was overridden (in which case its storage does not exist yet). if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) { - $this->container->get('state')->set('system.module.files', $this->moduleFiles); + $this->container->get('cache.default')->set('system.module.files', $this->moduleFiles); $this->container->get('state')->set('system.theme.files', $this->themeFiles); } diff --git a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php index 9d5ef09..2c92f74 100644 --- a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php @@ -21,8 +21,6 @@ function testDrupalGetFilename() { $install_state['parameters']['profile'] = 'testing'; // Rebuild system.module.files state data. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_static_reset('system_rebuild_module_data'); system_rebuild_module_data(); // Retrieving the location of a module. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 3033a11..b286e37 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -915,27 +915,15 @@ function system_check_directory($form_element, FormStateInterface $form_state) { */ function system_get_info($type, $name = NULL) { if ($type == 'module') { - $info = &drupal_static(__FUNCTION__); - if (!isset($info)) { - if ($cache = \Drupal::cache()->get('system.module.info')) { - $info = $cache->data; - } - else { - $data = system_rebuild_module_data(); - foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) { - if (isset($data[$module])) { - $info[$module] = $data[$module]->info; - } - } - // Store the module information in cache. This cache is cleared by - // calling system_rebuild_module_data(), for example, when listing - // modules, (un)installing modules, importing configuration, updating - // the site and when flushing all the caches. - \Drupal::cache()->set('system.module.info', $info); - } + if (isset($name)) { + return \Drupal::service('extension.list.module')->nameGetInfo($name); + } + else { + return \Drupal::service('extension.list.module')->getAllInfo(); } } else { + // @todo move into ThemeExtensionList https://www.drupal.org/node/2659940 $info = array(); $list = system_list($type); foreach ($list as $shortname => $item) { @@ -943,11 +931,11 @@ function system_get_info($type, $name = NULL) { $info[$shortname] = $item->info; } } + if (isset($name)) { + return isset($info[$name]) ? $info[$name] : array(); + } + return $info; } - if (isset($name)) { - return isset($info[$name]) ? $info[$name] : array(); - } - return $info; } /** @@ -957,105 +945,7 @@ 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); - } - } - } + return \Drupal::service('extension.list.module')->reset()->listExtensions(); } /** @@ -1065,33 +955,7 @@ function _system_rebuild_module_data_ensure_required($module, &$modules) { * Array of all available modules and their data. */ function system_rebuild_module_data() { - $modules_cache = &drupal_static(__FUNCTION__); - // Only rebuild once per request. $modules and $modules_cache cannot be - // 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(); - $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; - $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. - \Drupal::state()->set('system.module.files', $files); - // Clear the module info cache. - \Drupal::cache()->delete('system.module.info'); - drupal_static_reset('system_get_info'); - } - return $modules_cache; + return \Drupal::service('extension.list.module')->reset()->listExtensions(); } /** diff --git a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php index d691beb..b4154d0 100644 --- a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php +++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php @@ -115,7 +115,7 @@ function testDependencyResolution() { // Color will depend on Config, which depends on a non-existing module Foo. // Nothing should be installed. \Drupal::state()->set('module_test.dependency', 'missing dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('extension.list.module')->reset(); try { $result = $this->moduleInstaller()->install(array('color')); @@ -130,7 +130,7 @@ function testDependencyResolution() { // Fix the missing dependency. // 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('extension.list.module')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); @@ -162,7 +162,7 @@ function testDependencyResolution() { // dependency on a specific version of Help module in its info file. Make // 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('extension.list.module')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); @@ -192,8 +192,7 @@ function testUninstallProfileDependency() { drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml'); $this->enableModules(array('module_test', $profile)); - drupal_static_reset('system_rebuild_module_data'); - $data = system_rebuild_module_data(); + $data = \Drupal::service('extension.list.module')->reset()->listExtensions(); $this->assertTrue(isset($data[$profile]->requires[$dependency])); $this->moduleInstaller()->install(array($dependency)); @@ -231,7 +230,7 @@ function testUninstallContentDependency() { // entity_test will depend on help. This way help can not be uninstalled // when there is test content preventing entity_test from being uninstalled. \Drupal::state()->set('module_test.dependency', 'dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('extension.list.module')->reset(); // Create an entity so that the modules can not be disabled. $entity = EntityTest::create(array('name' => $this->randomString())); diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionHubTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionHubTest.php new file mode 100644 index 0000000..198996a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionHubTest.php @@ -0,0 +1,237 @@ +setupEmptyTestExtensionHub(); + + $test_extension_hub->nameGetLabel('test_name'); + } + + /** + * @covers ::nameGetLabel + */ + public function testGetName() { + $test_extension_hub = $this->setupTestExtensionHub(); + + static::assertEquals('test name', $test_extension_hub->nameGetLabel('test_name')); + } + + /** + * @covers ::nameGetExtension + * @expectedException \InvalidArgumentException + */ + public function testGetExtensionWithNonExistingExtension() { + $test_extension_hub = $this->setupEmptyTestExtensionHub(); + + $test_extension_hub->nameGetExtension('test_name'); + } + + /** + * @covers ::nameGetExtension + */ + public function testGetExtension() { + $test_extension_hub = $this->setupTestExtensionHub(); + + $extension = $test_extension_hub->nameGetExtension('test_name'); + static::assertInstanceOf(Extension::class, $extension); + static::assertEquals('test_name', $extension->getName()); + } + + /** + * @covers ::listExtensions + */ + public function testListExtensions() { + $test_extension_list = $this->setupTestExtensionHub(); + + $extensions = $test_extension_list->listExtensions(); + static::assertCount(1, $extensions); + static::assertEquals('test_name', $extensions['test_name']->getName()); + } + + /** + * @covers ::nameGetInfo + * @covers ::getAllInfo + */ + public function testGetInfo() { + $test_extension_list = $this->setupTestExtensionHub(); + + $info = $test_extension_list->nameGetInfo('test_name'); + static::assertEquals([ + 'type' => 'test_extension', + 'core' => '8.x', + 'name' => 'test name', + 'mtime' => 123456789, + ], $info); + } + + /** + * @covers ::getAllInfo + */ + public function testGetAllInfo() { + $test_extension_list = $this->setupTestExtensionHub(); + + $infos = $test_extension_list->getAllInfo(); + static::assertEquals(['test_name' => [ + 'type' => 'test_extension', + 'core' => '8.x', + 'name' => 'test name', + 'mtime' => 123456789, + ]], $infos); + } + + /** + * @covers ::getFilenames + */ + public function testGetFilenames() { + $test_extension_list = $this->setupTestExtensionHub(); + + $filenames = $test_extension_list->getFilenames(); + static::assertEquals([ + 'test_name' => 'vfs://drupal_root/example/test_name/test_name.info.yml', + ], $filenames); + } + + /** + * @covers ::nameGetFilename + */ + public function testGetFilename() { + $test_extension_list = $this->setupTestExtensionHub(); + + $filename = $test_extension_list->nameGetFilename('test_name'); + static::assertEquals('vfs://drupal_root/example/test_name/test_name.info.yml', $filename); + } + + + /** + * @covers ::nameSetFilename + * @covers ::nameGetFilename + */ + public function testSetFilename() { + $test_extension_list = $this->setupTestExtensionHub(); + + $test_extension_list->nameSetFilename('test_name', 'vfs://drupal_root/example2/test_name/test_name.info.yml'); + static::assertEquals('vfs://drupal_root/example2/test_name/test_name.info.yml', $test_extension_list->nameGetFilename('test_name')); + } + + /** + * @covers ::nameGetPath + */ + public function testGetPath() { + $test_extension_list = $this->setupTestExtensionHub(); + + $path = $test_extension_list->nameGetPath('test_name'); + static::assertEquals('vfs://drupal_root/example/test_name', $path); + } + + /** + * @return \Drupal\Core\Extension\Hub\ExtensionHubInterface + */ + protected function setupEmptyTestExtensionHub() { + list($cache, $info_parser, $module_handler) = $this->getMocks(); + + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery->scan('test_extension')->willReturn([]); + + return ExtensionHub::create( + 'test_extension', + TestExtensionList::create( + 'test_extension', + $info_parser->reveal(), + $module_handler->reveal(), + $extension_discovery->reveal()), + $cache->reveal()); + } + + /** + * @return \Drupal\Core\Extension\Hub\ExtensionHubInterface + */ + protected function setupTestExtensionHub() { + vfsStream::setup('drupal_root'); + vfsStream::create([ + 'example' => [ + 'test_name' => [ + 'test_name.info.yml' => Yaml::encode([ + 'name' => 'test name', + 'type' => 'test_extension', + 'core' => '8.x', + ]), + ], + ], + ]); + touch('vfs://drupal_root/example/test_name/test_name.info.yml', 123456789); + + list($cache, $info_parser, $module_handler) = $this->getMocks(); + $info_parser->parse(Argument::any())->will(function($args) { + return Yaml::decode(file_get_contents($args[0])); + }); + + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery->scan('test_extension')->willReturn(['test_name' => new Extension($this->root, 'test_extension', 'vfs://drupal_root/example/test_name/test_name.info.yml')]); + + return ExtensionHub::create( + 'test_extension', + TestExtensionList::create( + 'test_extension', + $info_parser->reveal(), + $module_handler->reveal(), + $extension_discovery->reveal()), + $cache->reveal()); + } + + protected function getMocks() { + $cache = $this->prophesize(CacheBackendInterface::class); + $info_parser = $this->prophesize(InfoParserInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + return [$cache, $info_parser, $module_handler]; + } + +} + +class TestExtensionList extends DiscoveryExtensionListBase { + + /** + * @param string $type + * @param \Drupal\Core\Extension\InfoParserInterface $info_parser + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery + * + * @return \Drupal\Core\Extension\List_\ExtensionListInterface + */ + static function create( + $type, + InfoParserInterface $info_parser, + ModuleHandlerInterface $module_handler, + ExtensionDiscovery $extension_discovery + ) { + return new self( + $type, + $info_parser, + $module_handler, + $extension_discovery, + []); + } + +}