diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php index 6439c17dba..d4b5aab86e 100644 --- a/core/lib/Drupal/Core/Database/Database.php +++ b/core/lib/Drupal/Core/Database/Database.php @@ -261,6 +261,44 @@ final public static function parseConnectionInfo(array $info) { $info['namespace'] = 'Drupal\\' . $info['driver'] . '\\Driver\\Database\\' . $info['driver']; } + // Backwards compatibility layer for Drupal 8 style database connection + // arrays. Those have the wrong 'namespace' key set, or not set at all + // for core supported database drivers. + if (strpos($info['namespace'], 'Drupal\\Core\\Database\\Driver\\') === 0) { + switch (strtolower($info['driver'])) { + case 'mysql': + $info['namespace'] = 'Drupal\\mysql\\Driver\\Database\\mysql'; + break; + + case 'pgsql': + $info['namespace'] = 'Drupal\\pgsql\\Driver\\Database\\pgsql'; + break; + + case 'sqlite': + $info['namespace'] = 'Drupal\\sqlite\\Driver\\Database\\sqlite'; + break; + } + } + + // Backwards compatibility layer for Drupal 8 style database connection + // arrays. Those do not have the 'autoload' key set for core database + // drivers. + if (empty($info['autoload'])) { + switch (trim($info['namespace'], '\\')) { + case "Drupal\\mysql\\Driver\\Database\\mysql": + $info['autoload'] = "core/modules/mysql/src/Driver/Database/mysql/"; + break; + + case "Drupal\\pgsql\\Driver\\Database\\pgsql": + $info['autoload'] = "core/modules/pgsql/src/Driver/Database/pgsql/"; + break; + + case "Drupal\\sqlite\\Driver\\Database\\sqlite": + $info['autoload'] = "core/modules/sqlite/src/Driver/Database/sqlite/"; + break; + } + } + return $info; } @@ -286,12 +324,26 @@ final public static function parseConnectionInfo(array $info) { * The database connection information, as defined in settings.php. The * structure of this array depends on the database driver it is connecting * to. + * @param \Composer\Autoload\ClassLoader $class_loader + * The class loader. Used for adding the database driver to the autoloader + * if $info['autoload'] is set. * * @see \Drupal\Core\Database\Database::setActiveConnection */ - final public static function addConnectionInfo($key, $target, array $info) { + final public static function addConnectionInfo($key, $target, array $info, $class_loader = NULL) { if (empty(self::$databaseInfo[$key][$target])) { - self::$databaseInfo[$key][$target] = self::parseConnectionInfo($info); + $info = self::parseConnectionInfo($info); + self::$databaseInfo[$key][$target] = $info; + + // The database driver needs to be loadable before module namespaces have + // been added to the autoloader, because the list of enabled modules and + // the service container definition are typically both in the database. + if (isset($info['autoload'])) { + if (!$class_loader) { + $class_loader = self::getClassLoader(); + } + $class_loader->addPsr4($info['namespace'] . '\\', $info['autoload']); + } } } @@ -324,11 +376,17 @@ final public static function getAllConnectionInfo() { * @param array $databases * A multi-dimensional array specifying database connection parameters, as * defined in settings.php. + * @param \Composer\Autoload\ClassLoader $class_loader + * The class loader. Used for adding the database driver(s) to the + * autoloader if $databases[$key][$target]['autoload'] is set. */ - final public static function setMultipleConnectionInfo(array $databases) { + final public static function setMultipleConnectionInfo(array $databases, $class_loader = NULL) { + if (!$class_loader) { + $class_loader = self::getClassLoader(); + } foreach ($databases as $key => $targets) { foreach ($targets as $target => $info) { - self::addConnectionInfo($key, $target, $info); + self::addConnectionInfo($key, $target, $info, $class_loader); } } } @@ -701,4 +759,23 @@ private static function isWithinModuleNamespace(string $namespace) { return ($first === 'Drupal' && strtolower($second) === $second); } + /** + * Returns either Drupal's primary class loader or a separate one. + * + * @return \Composer\Autoload\ClassLoader + * A class loader into which a database driver's namespace can be added if + * it requires autoloading. + */ + private static function getClassLoader() { + // Typically, the container isn't initialized until after the default + // database connection has been opened, but use the service if it's there. + if (\Drupal::hasContainer() && \Drupal::hasService('class_loader')) { + return \Drupal::service('class_loader'); + } + + $additional_class_loader = new ClassLoader(); + $additional_class_loader->register(TRUE); + return $additional_class_loader; + } + } diff --git a/core/lib/Drupal/Core/Site/Settings.php b/core/lib/Drupal/Core/Site/Settings.php index 31d494f8b4..7563109b6b 100644 --- a/core/lib/Drupal/Core/Site/Settings.php +++ b/core/lib/Drupal/Core/Site/Settings.php @@ -159,58 +159,7 @@ public static function initialize($app_root, $site_path, &$class_loader) { self::handleDeprecations($settings); - // Initialize databases. - foreach ($databases as $key => $targets) { - foreach ($targets as $target => $info) { - // Backwards compatibility layer for Drupal 8 style database connection - // arrays. Those have the wrong 'namespace' key set, or not set at all - // for core supported database drivers. - if (empty($info['namespace']) || (strpos($info['namespace'], 'Drupal\\Core\\Database\\Driver\\') === 0)) { - switch (strtolower($info['driver'])) { - case 'mysql': - $info['namespace'] = 'Drupal\\mysql\\Driver\\Database\\mysql'; - break; - - case 'pgsql': - $info['namespace'] = 'Drupal\\pgsql\\Driver\\Database\\pgsql'; - break; - - case 'sqlite': - $info['namespace'] = 'Drupal\\sqlite\\Driver\\Database\\sqlite'; - break; - } - } - // Backwards compatibility layer for Drupal 8 style database connection - // arrays. Those do not have the 'autoload' key set for core database - // drivers. - if (empty($info['autoload'])) { - switch (trim($info['namespace'], '\\')) { - case "Drupal\\mysql\\Driver\\Database\\mysql": - $info['autoload'] = "core/modules/mysql/src/Driver/Database/mysql/"; - break; - - case "Drupal\\pgsql\\Driver\\Database\\pgsql": - $info['autoload'] = "core/modules/pgsql/src/Driver/Database/pgsql/"; - break; - - case "Drupal\\sqlite\\Driver\\Database\\sqlite": - $info['autoload'] = "core/modules/sqlite/src/Driver/Database/sqlite/"; - break; - } - } - - Database::addConnectionInfo($key, $target, $info); - // If the database driver is provided by a module, then its code may - // need to be instantiated prior to when the module's root namespace - // is added to the autoloader, because that happens during service - // container initialization but the container definition is likely in - // the database. Therefore, allow the connection info to specify an - // autoload directory for the driver. - if (isset($info['autoload'])) { - $class_loader->addPsr4($info['namespace'] . '\\', $info['autoload']); - } - } - } + Database::setMultipleConnectionInfo($databases, $class_loader); // Initialize Settings. new Settings($settings); diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php index fb1c9dde8d..5b41dbcbb6 100644 --- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php @@ -32,8 +32,10 @@ protected function setUp(): void { $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container->expects($this->any()) ->method('has') - ->with('kernel') - ->willReturn(TRUE); + ->will($this->returnValueMap([ + ['kernel', TRUE], + ['class_loader', FALSE], + ])); $container->expects($this->any()) ->method('getParameter') ->with('site.path') diff --git a/core/tests/Drupal/Tests/Core/Site/SettingsTest.php b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php index bcc2eda6c9..662c33a51a 100644 --- a/core/tests/Drupal/Tests/Core/Site/SettingsTest.php +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php @@ -403,4 +403,46 @@ public function providerTestDatabaseInfoInitialization(): array { ]; } + /** + * Tests that code within settings.php can load database driver classes. + */ + public function testDatabaseAccessWithinSettingsPhp(): void { + $class_exists = NULL; + + // During PHPUnit tests, the current working directory might not be + // DRUPAL_ROOT, but during normal Drupal execution it is, and some calls + // to \Composer\Autoload\ClassLoader::addPsr4() assume it is by passing in + // paths that are relative to it. + $original_cwd = getcwd(); + chdir(DRUPAL_ROOT); + + // Ensure the driver isn't loadable yet. + $this->assertFalse(class_exists('Drupal\\Driver\\Database\\fake\\Connection')); + + // Create a mock settings.php file and require it. + $databases['mock']['default'] = [ + 'driver' => 'fake', + 'namespace' => 'Drupal\\Driver\\Database\\fake', + 'autoload' => 'core/tests/fixtures/database_drivers/custom/fake/', + ]; + $settings_file_content = "at($vfs_root); + vfsStream::newFile('settings.php') + ->at($sites_directory) + ->setContent($settings_file_content); + require vfsStream::url('root') . '/sites/settings.php'; + + // Restore PHPUnit's original working directory. + chdir($original_cwd); + + // Ensure the database driver was loadable from within settings.php. + $this->assertTrue($class_exists); + } + }