Commit 725641cf authored by catch's avatar catch

Issue #2208429 by dawehner, almaudoh, alexpott, jibran, Xano, donquixote,...

Issue #2208429 by dawehner, almaudoh, alexpott, jibran, Xano, donquixote, amitgoyal, bircher, stefan.r, Mile23, alvar0hurtad0, Berdir, andypost, larowlan: Extension System, Part III: ExtensionList, ModuleExtensionList and ProfileExtensionList
parent 6e06ffed
......@@ -505,6 +505,12 @@ 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\ModuleExtensionList
arguments: ['@app.root', 'module', '@cache.default', '@info_parser', '@module_handler', '@state', '@config.factory', '@extension.list.profile', '%install_profile%', '%container.modules%']
extension.list.profile:
class: Drupal\Core\Extension\ProfileExtensionList
arguments: ['@app.root', 'profile', '@cache.default', '@info_parser', '@module_handler', '@state', '%install_profile%']
content_uninstall_validator:
class: Drupal\Core\Entity\ContentUninstallValidator
tags:
......
......@@ -226,43 +226,45 @@ 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 (!isset($files[$type])) {
$files[$type] = [];
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->setPathname($name, $filename);
}
try {
return $extension_list->getPathname($name);
}
catch (\InvalidArgumentException $e) {
// Catch the exception. This will result in triggering an error.
}
}
else {
if (isset($filename)) {
$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 (!isset($files[$type])) {
$files[$type] = [];
}
// If still unknown, retrieve the file list prepared in state by
// system_rebuild_module_data() and
// \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData().
if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
$files[$type] += \Drupal::state()->get('system.' . $type . '.files', []);
if (isset($filename)) {
$files[$type][$name] = $filename;
}
// If still unknown, create a user-level error message.
if (!isset($files[$type][$name])) {
trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', ['@type' => $type, '@name' => $name]), E_USER_WARNING);
elseif (!isset($files[$type][$name])) {
// If still unknown, retrieve the file list prepared in state by
// \Drupal\Core\Extension\ExtensionList() and
// \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData().
if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
$files[$type] += \Drupal::state()->get('system.' . $type . '.files', []);
}
}
}
if (isset($files[$type][$name])) {
return $files[$type][$name];
if (isset($files[$type][$name])) {
return $files[$type][$name];
}
}
// If the filename is still unknown, create a user-level error message.
trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', ['@type' => $type, '@name' => $name]), E_USER_WARNING);
}
/**
......
......@@ -302,12 +302,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
......@@ -414,6 +408,7 @@ function install_begin_request($class_loader, &$install_state) {
}
else {
$environment = 'prod';
$GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
}
$GLOBALS['conf']['container_service_providers']['InstallerConfigOverride'] = 'Drupal\Core\Installer\ConfigOverride';
......@@ -444,6 +439,9 @@ function install_begin_request($class_loader, &$install_state) {
// Prime drupal_get_filename()'s static cache.
foreach ($install_state['profiles'] as $name => $profile) {
drupal_get_filename('profile', $name, $profile->getPathname());
// drupal_get_filename() is called both with 'module' and 'profile', see
// \Drupal\Core\Config\ConfigInstaller::getProfileStorages for example.
drupal_get_filename('module', $name, $profile->getPathname());
}
if ($profile = _install_select_profile($install_state)) {
......@@ -454,6 +452,12 @@ function install_begin_request($class_loader, &$install_state) {
}
}
// Before having installed the system module and being able to do a module
// rebuild, prime the drupal_get_filename() static cache with the system
// module's location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
// Use the language from the profile configuration, if available, to override
// the language previously set in the parameters.
if (isset($install_state['profile_info']['distribution']['langcode'])) {
......
......@@ -612,6 +612,8 @@ function drupal_verify_profile($install_state) {
function drupal_install_system($install_state) {
// Remove the service provider of the early installer.
unset($GLOBALS['conf']['container_service_providers']['InstallerServiceProvider']);
// Add the normal installer service provider.
$GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
$request = \Drupal::request();
// Reboot into a full production environment to continue the installation.
......@@ -622,6 +624,13 @@ 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 \Drupal\Core\Extension\ModuleExtensionList static cache
// with the module's location.
// @todo Try to install system as any other module, see
// https://www.drupal.org/node/2719315.
\Drupal::service('extension.list.module')->setPathname('system', 'core/modules/system/system.info.yml');
// Install base system configuration.
\Drupal::service('config.installer')->installDefaultConfig('core', 'core');
......
......@@ -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');
}
......
This diff is collapsed.
<?php
namespace Drupal\Core\Extension;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a list of available modules.
*/
class ModuleExtensionList extends ExtensionList {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected $defaults = [
'dependencies' => [],
'description' => '',
'package' => 'Other',
'version' => NULL,
'php' => DRUPAL_MINIMUM_PHP,
];
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The profile list needed by this module list.
*
* @var \Drupal\Core\Extension\ExtensionList
*/
protected $profileList;
/**
* 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\State\StateInterface $state
* The state.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ExtensionList $profile_list
* The site profile listing.
* @param string $install_profile
* The install profile used by the site.
* @param array[] $container_modules_info
* (optional) The module locations coming from the compiled container.
*/
public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ExtensionList $profile_list, $install_profile, array $container_modules_info = []) {
parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $install_profile);
$this->configFactory = $config_factory;
$this->profileList = $profile_list;
// Use the information from the container. This is an optimization.
foreach ($container_modules_info as $module_name => $info) {
$this->setPathname($module_name, $info['pathname']);
}
}
/**
* {@inheritdoc}
*/
protected function getExtensionDiscovery() {
$discovery = parent::getExtensionDiscovery();
if ($active_profile = $this->getActiveProfile()) {
$discovery->setProfileDirectories($this->getProfileDirectories($discovery));
}
return $discovery;
}
/**
* Finds all installation profile paths.
*
* @param \Drupal\Core\Extension\ExtensionDiscovery $discovery
* The extension discovery.
*
* @return string[]
* Paths to all installation profiles.
*/
protected function getProfileDirectories(ExtensionDiscovery $discovery) {
$discovery->setProfileDirectories([]);
$all_profiles = $discovery->scan('profile');
$active_profile = $all_profiles[$this->installProfile];
$profiles = array_intersect_key($all_profiles, $this->configFactory->get('core.extension')->get('module') ?: [$active_profile->getName() => 0]);
// If a module is within a profile directory but specifies another
// profile for testing, it needs to be found in the parent profile.
$parent_profile = $this->configFactory->get('simpletest.settings')->get('parent_profile');
if ($parent_profile && !isset($profiles[$parent_profile])) {
// In case both profile directories contain the same extension, the
// actual profile always has precedence.
$profiles = [$parent_profile => $all_profiles[$parent_profile]] + $profiles;
}
$profile_directories = array_map(function (Extension $profile) {
return $profile->getPath();
}, $profiles);
return $profile_directories;
}
/**
* Gets the processed active profile object, or null.
*
* @return \Drupal\Core\Extension\Extension|null
* The active profile, if there is one.
*/
protected function getActiveProfile() {
$profiles = $this->profileList->getList();
if ($this->installProfile && isset($profiles[$this->installProfile])) {
return $profiles[$this->installProfile];
}
return NULL;
}
/**
* {@inheritdoc}
*/
protected function doScanExtensions() {
$extensions = parent::doScanExtensions();
$profiles = $this->profileList->getList();
// Modify the active profile object that was previously added to the module
// list.
if ($this->installProfile && isset($profiles[$this->installProfile])) {
$extensions[$this->installProfile] = $profiles[$this->installProfile];
}
return $extensions;
}
/**
* {@inheritdoc}
*/
protected function doList() {
// Find modules.
$extensions = parent::doList();
// 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);
}
// 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;
}
$extensions = $this->moduleHandler->buildModuleDependencies($extensions);
if ($this->installProfile && $extensions[$this->installProfile]) {
$active_profile = $extensions[$this->installProfile];
// Installation profile hooks are always executed last.
$active_profile->weight = 1000;
// Installation profiles are hidden by default, unless explicitly
// specified otherwise in the .info.yml file.
if (!isset($active_profile->info['hidden'])) {
$active_profile->info['hidden'] = TRUE;
}
// The installation profile is required.
$active_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($active_profile->info['distribution']['name'])) {
$active_profile->info['distribution']['name'] = 'Drupal';
}
}
return $extensions;
}
/**
* {@inheritdoc}
*/
protected function getInstalledExtensionNames() {
return array_keys($this->moduleHandler->getModuleList());
}
/**
* Marks dependencies of required modules as 'required', recursively.
*
* @param \Drupal\Core\Extension\Extension $module
* The module extension object.
* @param \Drupal\Core\Extension\Extension[] $modules
* Extension objects for all available modules.
*/
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;
$modules[$dependency_name]->info['explanation'] = $this->t('Dependency of required module @module', ['@module' => $module->info['name']]);
// Ensure any dependencies it has are required.
$this->ensureRequiredDependencies($modules[$dependency_name], $modules);
}
}
}
}
}
......@@ -777,8 +777,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')->getName($module);
}
}
......@@ -14,6 +14,11 @@
*
* It registers the module in config, installs its own configuration,
* installs the schema, updates the Drupal kernel and more.
*
* We don't inject dependencies yet, as we would need to reload them after
* each installation or uninstallation of a module.
* https://www.drupal.org/project/drupal/issues/2350111 for example tries to
* solve this dilemma.
*/
class ModuleInstaller implements ModuleInstallerInterface {
......@@ -170,7 +175,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')->getPath($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);
......@@ -186,10 +191,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);
......@@ -443,10 +448,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();
......
<?php
namespace Drupal\Core\Extension;
/**
* Provides a list of installation profiles.
*/
class ProfileExtensionList extends ExtensionList {
/**
* {@inheritdoc}
*/
protected $defaults = [
'dependencies' => [],
'description' => '',
'package' => 'Other',
'version' => NULL,
'php' => DRUPAL_MINIMUM_PHP,
];
/**
* {@inheritdoc}
*/
protected function getInstalledExtensionNames() {
return [$this->installProfile];
}
}
<?php
namespace Drupal\Core\Installer;
use Drupal\Core\Extension\ModuleExtensionList;
/**
* Overrides the module extension list to have a static cache.
*/
class InstallerModuleExtensionList extends ModuleExtensionList {
/**
* Static version of the added file names during the installer.
*
* @var string[]
*
* @internal
*/
protected static $staticAddedPathNames;
/**
* {@inheritdoc}
*/
public function setPathname($extension_name, $pathname) {
parent::setPathname($extension_name, $pathname);
// In the early installer the container is rebuilt multiple times. Therefore
// we have to keep the added filenames across those rebuilds. This is not a
// final design, but rather just a workaround resolved at some point,
// hopefully.
// @todo Remove as part of https://drupal.org/project/drupal/issues/2934063
static::$staticAddedPathNames[$extension_name] = $pathname;
}
/**
* {@inheritdoc}
*/
public function getPathname($extension_name) {
if (isset($this->addedPathNames[$extension_name])) {
return $this->addedPathNames[$extension_name];
}
elseif (isset($this->pathNames[$extension_name])) {
return $this->pathNames[$extension_name];
}
elseif (isset(static::$staticAddedPathNames[$extension_name])) {
return static::$staticAddedPathNames[$extension_name];
}
elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
// Ensure we don't have to do path scanning more than really needed.
foreach ($path_names as $extension => $path_name) {
static::$staticAddedPathNames[$extension] = $path_name;
}
return $path_names[$extension_name];
}
throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
}
}
......@@ -59,6 +59,9 @@ public function register(ContainerBuilder $container) {
// The core router builder, but there is no reason here to be lazy, so
// we don't need to ship with a custom proxy class.
->setLazy(FALSE);
// Use a performance optimised module extension list.
$container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList');
}
/**
......
<?php
namespace Drupal\Core\Installer;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
/**
* Service provider for the non early installer environment.
*/
class NormalInstallerServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
// Use a performance optimised module extension list.
$container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList');
}
}
......@@ -49,8 +49,9 @@ 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', []);
return isset($modules[$this->name]);
/** @var \Drupal\Core\Extension\ExtensionList $module_extension_list */
$module_extension_list = \Drupal::service('extension.list.module');
return $module_extension_list->exists($this->name);
}
/**
......
......@@ -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()->getList();
$this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.');
$node = Node::create(['title' => $this->randomString(), 'type' => $content_type->id()]);
......
......@@ -98,7 +98,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.
......
......@@ -72,7 +72,7 @@ public function testFilterForm() {
// @see https://www.drupal.org/node/2387983
\Drupal::service('module_installer')->install(['filter_test_plugin']);
// Force rebuild module data.
_system_rebuild_module_data();
\Drupal::service('extension.list.module')->reset();
}
/**
......
......@@ -488,7 +488,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()->getList();
$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
......
......@@ -12,7 +12,6 @@
use Drupal\Core\Queue\QueueGarbageCollectionInterface;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory;
use Drupal\Core\PageCache\RequestPolicyInterface;
......@@ -961,27 +960,17 @@ 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);
}
/** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */
$module_list = \Drupal::service('extension.list.module');
if (isset($name)) {
return $module_list->getExtensionInfo($name);
}
else {
return $module_list->getAllInstalledInfo();
}
}
else {
// @todo move into ThemeExtensionList https://www.drupal.org/node/2659940
$info = [];
$list = system_list($type);
foreach ($list as $shortname => $item) {
......@@ -989,96 +978,11 @@ function system_get_info($type, $name = NULL) {
$info[$shortname] = $item->info;
}
}
}
if (isset($name)) {
return isset($info[$name]) ? $info[$name] : [];
}
return $info;
}
/**
* Helper function to scan and collect module .info.yml data.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array of module information.