Skip to content

Commit

Permalink
Issue #3120096 by alexpott, daffie, effulgentsia, Neslee Canil Pinto,…
Browse files Browse the repository at this point in the history
… xjm, mondrake, catch, ravi.shankar: Support contrib database driver directories in a fixed location in a module

(cherry picked from commit ff6279aa7830c0e18f0c6122993d1b8ba85e7abb)
(cherry picked from commit 5ca020f1399109b8c1ea6d339509fb6cebafb685)
  • Loading branch information
effulgentsia committed Apr 10, 2020
1 parent 013a249 commit f098f2a
Show file tree
Hide file tree
Showing 44 changed files with 977 additions and 62 deletions.
24 changes: 24 additions & 0 deletions assets/scaffold/files/default.settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@
* webserver. For most other drivers, you must specify a
* username, password, host, and database name.
*
* Drupal core implements drivers for mysql, pgsql, and sqlite. Other drivers
* can be provided by contributed or custom modules. To use a contributed or
* custom driver, the "namespace" property must be set to the namespace of the
* driver. The code in this namespace must be autoloadable prior to connecting
* to the database, and therefore, prior to when module root namespaces are
* added to the autoloader. To add the driver's namespace to the autoloader,
* set the "autoload" property to the PSR-4 base directory of the driver's
* namespace. This is optional for projects managed with Composer if the
* driver's namespace is in Composer's autoloader.
*
* Transaction support is enabled by default for all drivers that support it,
* including MySQL. To explicitly disable it, set the 'transactions' key to
* FALSE.
Expand Down Expand Up @@ -224,6 +234,20 @@
* 'database' => '/path/to/databasefilename',
* ];
* @endcode
*
* Sample Database configuration format for a driver in a contributed module:
* @code
* $databases['default']['default'] = [
* 'driver' => 'mydriver',
* 'namespace' => 'Drupal\mymodule\Driver\Database\mydriver',
* 'autoload' => 'modules/mymodule/src/Driver/Database/mydriver/',
* 'database' => 'databasename',
* 'username' => 'sqlusername',
* 'password' => 'sqlpassword',
* 'host' => 'localhost',
* 'prefix' => '',
* ];
* @endcode
*/

/**
Expand Down
7 changes: 6 additions & 1 deletion includes/install.core.inc
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,11 @@ function install_begin_request($class_loader, &$install_state) {
->addArgument(Settings::getInstance())
->addArgument((new LoggerChannelFactory())->get('file'));

// Register the class loader so contrib and custom database drivers can be
// autoloaded.
// @see drupal_get_database_types()
$container->set('class_loader', $class_loader);

\Drupal::setContainer($container);

// Determine whether base system services are ready to operate.
Expand Down Expand Up @@ -1207,7 +1212,7 @@ function install_database_errors($database, $settings_file) {
// calling function.
Database::addConnectionInfo('default', 'default', $database);

$errors = db_installer_object($driver)->runTasks();
$errors = db_installer_object($driver, $database['namespace'] ?? NULL)->runTasks();
}
return $errors;
}
Expand Down
57 changes: 53 additions & 4 deletions includes/install.inc
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ function drupal_get_database_types() {

// The internal database driver name is any valid PHP identifier.
$mask = ExtensionDiscovery::PHP_FUNCTION_PATTERN;

// Find drivers in the Drupal\Core and Drupal\Driver namespaces.
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$files = $file_system->scanDirectory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]);
Expand All @@ -179,11 +181,43 @@ function drupal_get_database_types() {
}
foreach ($files as $file) {
if (file_exists($file->uri . '/Install/Tasks.php')) {
$drivers[$file->filename] = $file->uri;
// The namespace doesn't need to be added here, because
// db_installer_object() will find it.
$drivers[$file->filename] = NULL;
}
}

// Find drivers in Drupal module namespaces.
/** @var \Composer\Autoload\ClassLoader $class_loader */
$class_loader = \Drupal::service('class_loader');
// We cannot use the file cache because it does not always exist.
$extension_discovery = new ExtensionDiscovery(DRUPAL_ROOT, FALSE, []);
$modules = $extension_discovery->scan('module');
foreach ($modules as $module) {
$module_driver_path = DRUPAL_ROOT . '/' . $module->getPath() . '/src/Driver/Database';
if (is_dir($module_driver_path)) {
$driver_files = $file_system->scanDirectory($module_driver_path, $mask, ['recurse' => FALSE]);
foreach ($driver_files as $driver_file) {
$tasks_file = $module_driver_path . '/' . $driver_file->filename . '/Install/Tasks.php';
if (file_exists($tasks_file)) {
$namespace = 'Drupal\\' . $module->getName() . '\\Driver\\Database\\' . $driver_file->filename;

// The namespace needs to be added for db_installer_object() to find
// it.
$drivers[$driver_file->filename] = $namespace;

// The directory needs to be added to the autoloader, because this is
// early in the installation process: the module hasn't been enabled
// yet and the database connection info array (including its 'autoload'
// key) hasn't been created yet.
$class_loader->addPsr4($namespace . '\\', $module->getPath() . '/src/Driver/Database/' . $driver_file->filename);
}
}
}
}
foreach ($drivers as $driver => $file) {
$installer = db_installer_object($driver);

foreach ($drivers as $driver => $namespace) {
$installer = db_installer_object($driver, $namespace);
if ($installer->installable()) {
$databases[$driver] = $installer;
}
Expand Down Expand Up @@ -1169,20 +1203,35 @@ function install_profile_info($profile, $langcode = 'en') {
/**
* Returns a database installer object.
*
* Before calling this function it is important the database installer object
* is autoloadable. Database drivers provided by contributed modules are added
* to the autoloader in drupal_get_database_types() and Settings::initialize().
*
* @param $driver
* The name of the driver.
* @param string $namespace
* (optional) The database driver namespace.
*
* @return \Drupal\Core\Database\Install\Tasks
* A class defining the requirements and tasks for installing the database.
*
* @see drupal_get_database_types()
* @see \Drupal\Core\Site\Settings::initialize()
*/
function db_installer_object($driver) {
function db_installer_object($driver, $namespace = NULL) {
// We cannot use Database::getConnection->getDriverClass() here, because
// the connection object is not yet functional.
if ($namespace) {
$task_class = $namespace . "\\Install\\Tasks";
return new $task_class();
}
// Old Drupal 8 style contrib namespace.
$task_class = "Drupal\\Driver\\Database\\{$driver}\\Install\\Tasks";
if (class_exists($task_class)) {
return new $task_class();
}
else {
// Core provided driver.
$task_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Install\\Tasks";
return new $task_class();
}
Expand Down
27 changes: 17 additions & 10 deletions lib/Drupal/Core/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1378,7 +1378,7 @@ abstract public function queryTemporary($query, array $args = [], array $options
* Returns the type of database driver.
*
* This is not necessarily the same as the type of the database itself. For
* instance, there could be two MySQL drivers, mysql and mysql_mock. This
* instance, there could be two MySQL drivers, mysql and mysqlMock. This
* function would return different values for each, but both would return
* "mysql" for databaseType().
*
Expand Down Expand Up @@ -1572,10 +1572,6 @@ public function __sleep() {
/**
* Creates an array of database connection options from a URL.
*
* @internal
* This method should not be called. Use
* \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo() instead.
*
* @param string $url
* The URL.
* @param string $root
Expand All @@ -1589,6 +1585,10 @@ public function __sleep() {
* Exception thrown when the provided URL does not meet the minimum
* requirements.
*
* @internal
* This method should only be called from
* \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo().
*
* @see \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo()
*/
public static function createConnectionOptionsFromUrl($url, $root) {
Expand Down Expand Up @@ -1634,12 +1634,10 @@ public static function createConnectionOptionsFromUrl($url, $root) {
/**
* Creates a URL from an array of database connection options.
*
* @internal
* This method should not be called. Use
* \Drupal\Core\Database\Database::getConnectionInfoAsUrl() instead.
*
* @param array $connection_options
* The array of connection options for a database connection.
* The array of connection options for a database connection. An additional
* key of 'module' is added by Database::getConnectionInfoAsUrl() for
* drivers provided my contributed or custom modules for convenience.
*
* @return string
* The connection info as a URL.
Expand All @@ -1648,6 +1646,10 @@ public static function createConnectionOptionsFromUrl($url, $root) {
* Exception thrown when the provided array of connection options does not
* meet the minimum requirements.
*
* @internal
* This method should only be called from
* \Drupal\Core\Database\Database::getConnectionInfoAsUrl().
*
* @see \Drupal\Core\Database\Database::getConnectionInfoAsUrl()
*/
public static function createUrlFromConnectionOptions(array $connection_options) {
Expand All @@ -1674,6 +1676,11 @@ public static function createUrlFromConnectionOptions(array $connection_options)

$db_url .= '/' . $connection_options['database'];

// Add the module when the driver is provided by a module.
if (isset($connection_options['module'])) {
$db_url .= '?module=' . $connection_options['module'];
}

if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
}
Expand Down

0 comments on commit f098f2a

Please sign in to comment.