Issue #3129534 by daffie, anmolgoyal74, mondrake, naresh_bavaskar, Beakerboy, catch, alexpott, quietone: Automatically enable the module that is providing the current database driver
parent
d722e9d760
commit
f838dbc871
|
@ -556,6 +556,12 @@ services:
|
|||
- { name: module_install.uninstall_validator }
|
||||
arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme']
|
||||
lazy: true
|
||||
database_driver_uninstall_validator:
|
||||
class: Drupal\Core\Extension\DatabaseDriverUninstallValidator
|
||||
tags:
|
||||
- { name: module_install.uninstall_validator }
|
||||
arguments: ['@string_translation', '@extension.list.module', '@database']
|
||||
lazy: true
|
||||
theme_handler:
|
||||
class: Drupal\Core\Extension\ThemeHandler
|
||||
arguments: ['%app.root%', '@config.factory', '@extension.list.theme']
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
use Drupal\Component\Utility\OpCodeCache;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Extension\Dependency;
|
||||
use Drupal\Core\Extension\ExtensionDiscovery;
|
||||
use Drupal\Core\Installer\InstallerKernel;
|
||||
|
@ -619,6 +620,18 @@ function drupal_install_system($install_state) {
|
|||
->set('profile', $install_state['parameters']['profile'])
|
||||
->save();
|
||||
|
||||
$connection = Database::getConnection();
|
||||
$provider = $connection->getProvider();
|
||||
// When the database driver is provided by a module, then install that module.
|
||||
// This module must be installed before any other module, as it must be able
|
||||
// to override any call to hook_schema() or any "backend_overridable" service.
|
||||
if ($provider !== 'core') {
|
||||
$autoload = $connection->getConnectionOptions()['autoload'] ?? '';
|
||||
if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) {
|
||||
$kernel->getContainer()->get('module_installer')->install([$provider], FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
// Install System module.
|
||||
$kernel->getContainer()->get('module_installer')->install(['system'], FALSE);
|
||||
|
||||
|
|
|
@ -1950,4 +1950,22 @@ abstract class Connection {
|
|||
return $db_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module name of the module that is providing the database driver.
|
||||
*
|
||||
* @return string
|
||||
* The module name of the module that is providing the database driver, or
|
||||
* "core" when the driver is not provided as part of a module.
|
||||
*/
|
||||
public function getProvider(): string {
|
||||
[$first, $second] = explode('\\', $this->connectionOptions['namespace'], 3);
|
||||
|
||||
// The namespace for Drupal modules is Drupal\MODULE_NAME, and the module
|
||||
// name must be all lowercase. Second-level namespaces containing uppercase
|
||||
// letters (e.g., "Core", "Component", "Driver") are not modules.
|
||||
// @see \Drupal\Core\DrupalKernel::getModuleNamespacesPsr4()
|
||||
// @see https://www.drupal.org/docs/8/creating-custom-modules/naming-and-placing-your-drupal-8-module#s-name-your-module
|
||||
return ($first === 'Drupal' && strtolower($second) === $second) ? $second : 'core';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Extension;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
|
||||
/**
|
||||
* Ensures installed modules providing a database driver are not uninstalled.
|
||||
*/
|
||||
class DatabaseDriverUninstallValidator implements ModuleUninstallValidatorInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The module extension list.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||
*/
|
||||
protected $moduleExtensionList;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Constructs a new DatabaseDriverUninstallValidator.
|
||||
*
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The string translation service.
|
||||
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
|
||||
* The module extension list.
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection.
|
||||
*/
|
||||
public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, Connection $connection) {
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->moduleExtensionList = $extension_list_module;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($module) {
|
||||
$reasons = [];
|
||||
|
||||
// @todo Remove the next line of code in
|
||||
// https://www.drupal.org/project/drupal/issues/3129043.
|
||||
$this->connection = Database::getConnection();
|
||||
|
||||
// When the database driver is provided by a module, then that module
|
||||
// cannot be uninstalled.
|
||||
if ($module === $this->connection->getProvider()) {
|
||||
$module_name = $this->moduleExtensionList->get($module)->info['name'];
|
||||
$reasons[] = $this->t("The module '@module_name' is providing the database driver '@driver_name'.",
|
||||
['@module_name' => $module_name, '@driver_name' => $this->connection->driver()]);
|
||||
}
|
||||
|
||||
return $reasons;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
// @codingStandardsIgnoreFile
|
||||
|
||||
/**
|
||||
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Extension\DatabaseDriverUninstallValidator' "core/lib/Drupal/Core".
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\ProxyClass\Extension {
|
||||
|
||||
/**
|
||||
* Provides a proxy class for \Drupal\Core\Extension\DatabaseDriverUninstallValidator.
|
||||
*
|
||||
* @see \Drupal\Component\ProxyBuilder
|
||||
*/
|
||||
class DatabaseDriverUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
|
||||
{
|
||||
|
||||
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||
|
||||
/**
|
||||
* The id of the original proxied service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $drupalProxyOriginalServiceId;
|
||||
|
||||
/**
|
||||
* The real proxied service, after it was lazy loaded.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\DatabaseDriverUninstallValidator
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* The service container.
|
||||
*
|
||||
* @var \Symfony\Component\DependencyInjection\ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Constructs a ProxyClass Drupal proxy object.
|
||||
*
|
||||
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
|
||||
* The container.
|
||||
* @param string $drupal_proxy_original_service_id
|
||||
* The service ID of the original service.
|
||||
*/
|
||||
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy loads the real service from the container.
|
||||
*
|
||||
* @return object
|
||||
* Returns the constructed real service.
|
||||
*/
|
||||
protected function lazyLoadItself()
|
||||
{
|
||||
if (!isset($this->service)) {
|
||||
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
|
||||
}
|
||||
|
||||
return $this->service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($module)
|
||||
{
|
||||
return $this->lazyLoadItself()->validate($module);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
|
||||
{
|
||||
return $this->lazyLoadItself()->setStringTranslation($translation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1126,6 +1126,24 @@ function system_requirements($phase) {
|
|||
}
|
||||
}
|
||||
|
||||
// When the database driver is provided by a module, then check that the
|
||||
// providing module is enabled.
|
||||
if ($phase === 'runtime' || $phase === 'update') {
|
||||
$connection = Database::getConnection();
|
||||
$provider = $connection->getProvider();
|
||||
if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) {
|
||||
$autoload = $connection->getConnectionOptions()['autoload'] ?? '';
|
||||
if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) {
|
||||
$requirements['database_driver_provided_by_module'] = [
|
||||
'title' => t('Database driver provided by module'),
|
||||
'value' => t('Not enabled'),
|
||||
'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]),
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check xdebug.max_nesting_level, as some pages will not work if it is too
|
||||
// low.
|
||||
if (extension_loaded('xdebug')) {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\system\Functional\System;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests output on the status overview page.
|
||||
*
|
||||
* @group system
|
||||
*/
|
||||
class DatabaseDriverProvidedByModuleTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'administer site configuration',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the status page shows the error message.
|
||||
*/
|
||||
public function testDatabaseDriverIsProvidedByModuleButTheModuleIsNotEnabled(): void {
|
||||
$driver = Database::getConnection()->driver();
|
||||
if (!in_array($driver, ['mysql', 'pgsql'])) {
|
||||
$this->markTestSkipped("This test does not support the {$driver} database driver.");
|
||||
}
|
||||
|
||||
// Change the default database connection to use the one from the module
|
||||
// driver_test.
|
||||
$connection_info = Database::getConnectionInfo();
|
||||
$database = [
|
||||
'database' => $connection_info['default']['database'],
|
||||
'username' => $connection_info['default']['username'],
|
||||
'password' => $connection_info['default']['password'],
|
||||
'prefix' => $connection_info['default']['prefix'],
|
||||
'host' => $connection_info['default']['host'],
|
||||
'driver' => 'Drivertest' . ucfirst($driver),
|
||||
'namespace' => 'Drupal\\driver_test\\Driver\\Database\\Drivertest' . ucfirst($driver),
|
||||
'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/Drivertest' . ucfirst($driver),
|
||||
];
|
||||
if (isset($connection_info['default']['port'])) {
|
||||
$database['port'] = $connection_info['default']['port'];
|
||||
}
|
||||
$settings['databases']['default']['default'] = (object) [
|
||||
'value' => $database,
|
||||
'required' => TRUE,
|
||||
];
|
||||
$this->writeSettings($settings);
|
||||
|
||||
$this->drupalGet('admin/reports/status');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// The module driver_test is not enabled and is providing to current
|
||||
// database driver. Check that the correct error is shown.
|
||||
$this->assertSession()->pageTextContains('Database driver provided by module');
|
||||
$this->assertSession()->pageTextContains('The current database driver is provided by the module: driver_test. The module is currently not enabled. You should immediately enable the module.');
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
namespace Drupal\FunctionalTests\Installer;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Extension\Extension;
|
||||
use Drupal\Core\Extension\ModuleUninstallValidatorException;
|
||||
|
||||
/**
|
||||
* Tests the interactive installer.
|
||||
|
@ -60,6 +62,32 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
|
|||
$this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents);
|
||||
$this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents);
|
||||
$this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents);
|
||||
|
||||
// Assert that the module "driver_test" has been installed.
|
||||
$this->assertEquals(\Drupal::service('module_handler')->getModule('driver_test'), new Extension($this->root, 'module', 'core/modules/system/tests/modules/driver_test/driver_test.info.yml'));
|
||||
|
||||
// Change the default database connection to use the database driver from
|
||||
// the module "driver_test".
|
||||
$connection_info = Database::getConnectionInfo();
|
||||
$driver_test_connection = $connection_info['default'];
|
||||
$driver_test_connection['driver'] = $this->testDriverName;
|
||||
$driver_test_connection['namespace'] = 'Drupal\\driver_test\\Driver\\Database\\' . $this->testDriverName;
|
||||
$driver_test_connection['autoload'] = "core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/";
|
||||
Database::renameConnection('default', 'original_database_connection');
|
||||
Database::addConnectionInfo('default', 'default', $driver_test_connection);
|
||||
|
||||
// The module "driver_test" should not be uninstallable, because it is
|
||||
// providing the database driver.
|
||||
try {
|
||||
$this->container->get('module_installer')->uninstall(['driver_test']);
|
||||
$this->fail('Uninstalled driver_test module.');
|
||||
}
|
||||
catch (ModuleUninstallValidatorException $e) {
|
||||
$this->assertStringContainsString("The module 'Contrib database driver test' is providing the database driver '{$this->testDriverName}'.", $e->getMessage());
|
||||
}
|
||||
|
||||
// Restore the old database connection.
|
||||
Database::addConnectionInfo('default', 'default', $connection_info['default']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -336,6 +336,20 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
|
|||
|
||||
$modules = self::getModulesToEnable(static::class);
|
||||
|
||||
// When a module is providing the database driver, then enable that module.
|
||||
$connection_info = Database::getConnectionInfo();
|
||||
$driver = $connection_info['default']['driver'];
|
||||
$namespace = $connection_info['default']['namespace'] ?? NULL;
|
||||
$autoload = $connection_info['default']['autoload'] ?? NULL;
|
||||
if (strpos($autoload, 'src/Driver/Database/') !== FALSE) {
|
||||
[$first, $second] = explode('\\', $namespace, 3);
|
||||
if ($first === 'Drupal' && strtolower($second) === $second) {
|
||||
// Add the module that provides the database driver to the list of
|
||||
// modules as the first to be enabled.
|
||||
array_unshift($modules, $second);
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap the kernel. Do not use createFromRequest() to retain Settings.
|
||||
$kernel = new DrupalKernel('testing', $this->classLoader, FALSE);
|
||||
$kernel->setSitePath($this->siteDirectory);
|
||||
|
@ -357,9 +371,6 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
|
|||
|
||||
// Ensure database tasks have been run.
|
||||
require_once __DIR__ . '/../../../includes/install.inc';
|
||||
$connection_info = Database::getConnectionInfo();
|
||||
$driver = $connection_info['default']['driver'];
|
||||
$namespace = $connection_info['default']['namespace'] ?? NULL;
|
||||
$errors = db_installer_object($driver, $namespace)->runTasks();
|
||||
if (!empty($errors)) {
|
||||
$this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\KernelTests;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\KernelTests\KernelTestBase
|
||||
*
|
||||
* @group PHPUnit
|
||||
* @group Test
|
||||
* @group KernelTests
|
||||
*/
|
||||
class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDatabaseConnectionInfo() {
|
||||
// If the test is run with argument SIMPLETEST_DB then use it.
|
||||
$db_url = getenv('SIMPLETEST_DB');
|
||||
if (empty($db_url)) {
|
||||
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
|
||||
}
|
||||
else {
|
||||
$database = Database::convertDbUrlToConnectionInfo($db_url, $this->root);
|
||||
|
||||
if (in_array($database['driver'], ['mysql', 'pgsql'])) {
|
||||
// Change the used database driver to the one provided by the module
|
||||
// "driver_test".
|
||||
$driver = 'Drivertest' . ucfirst($database['driver']);
|
||||
$database['driver'] = $driver;
|
||||
$database['namespace'] = 'Drupal\\driver_test\\Driver\\Database\\' . $driver;
|
||||
$database['autoload'] = "core/modules/system/tests/modules/driver_test/src/Driver/Database/$driver/";
|
||||
}
|
||||
|
||||
Database::addConnectionInfo('default', 'default', $database);
|
||||
}
|
||||
|
||||
// Clone the current connection and replace the current prefix.
|
||||
$connection_info = Database::getConnectionInfo('default');
|
||||
if (!empty($connection_info)) {
|
||||
Database::renameConnection('default', 'simpletest_original_default');
|
||||
foreach ($connection_info as $target => $value) {
|
||||
// Replace the full table prefix definition to ensure that no table
|
||||
// prefixes of the test runner leak into the test.
|
||||
$connection_info[$target]['prefix'] = [
|
||||
'default' => $this->databasePrefix,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $connection_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::bootEnvironment
|
||||
*/
|
||||
public function testDatabaseDriverModuleEnabled(): void {
|
||||
$driver = Database::getConnection()->driver();
|
||||
if (!in_array($driver, ['DrivertestMysql', 'DrivertestPgsql'])) {
|
||||
$this->markTestSkipped("This test does not support the {$driver} database driver.");
|
||||
}
|
||||
|
||||
// Test that the module that is providing the database driver is enabled.
|
||||
$this->assertSame(1, \Drupal::service('extension.list.module')->get('driver_test')->status);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue