--- /dev/null
+<?php
+
+namespace Drupal\Core\Database;
+
+/**
+ * Primary front-controller for the database system.
+ *
+ * This class is uninstantiatable and un-extendable. It acts to encapsulate
+ * all control and shepherding of database connections into a single location
+ * without the use of globals.
+ */
+abstract class Database {
+
+ /**
+ * Flag to indicate a query call should simply return NULL.
+ *
+ * This is used for queries that have no reasonable return value anyway, such
+ * as INSERT statements to a table without a serial primary key.
+ */
+ const RETURN_NULL = 0;
+
+ /**
+ * Flag to indicate a query call should return the prepared statement.
+ */
+ const RETURN_STATEMENT = 1;
+
+ /**
+ * Flag to indicate a query call should return the number of affected rows.
+ */
+ const RETURN_AFFECTED = 2;
+
+ /**
+ * Flag to indicate a query call should return the "last insert id".
+ */
+ const RETURN_INSERT_ID = 3;
+
+ /**
+ * An nested array of all active connections. It is keyed by database name
+ * and target.
+ *
+ * @var array
+ */
+ static protected $connections = [];
+
+ /**
+ * A processed copy of the database connection information from settings.php.
+ *
+ * @var array
+ */
+ static protected $databaseInfo = [];
+
+ /**
+ * A list of key/target credentials to simply ignore.
+ *
+ * @var array
+ */
+ static protected $ignoreTargets = [];
+
+ /**
+ * The key of the currently active database connection.
+ *
+ * @var string
+ */
+ static protected $activeKey = 'default';
+
+ /**
+ * An array of active query log objects.
+ *
+ * Every connection has one and only one logger object for all targets and
+ * logging keys.
+ *
+ * array(
+ * '$db_key' => DatabaseLog object.
+ * );
+ *
+ * @var array
+ */
+ static protected $logs = [];
+
+ /**
+ * Starts logging a given logging key on the specified connection.
+ *
+ * @param string $logging_key
+ * The logging key to log.
+ * @param string $key
+ * The database connection key for which we want to log.
+ *
+ * @return \Drupal\Core\Database\Log
+ * The query log object. Note that the log object does support richer
+ * methods than the few exposed through the Database class, so in some
+ * cases it may be desirable to access it directly.
+ *
+ * @see \Drupal\Core\Database\Log
+ */
+ final public static function startLog($logging_key, $key = 'default') {
+ if (empty(self::$logs[$key])) {
+ self::$logs[$key] = new Log($key);
+
+ // Every target already active for this connection key needs to have the
+ // logging object associated with it.
+ if (!empty(self::$connections[$key])) {
+ foreach (self::$connections[$key] as $connection) {
+ $connection->setLogger(self::$logs[$key]);
+ }
+ }
+ }
+
+ self::$logs[$key]->start($logging_key);
+ return self::$logs[$key];
+ }
+
+ /**
+ * Retrieves the queries logged on for given logging key.
+ *
+ * This method also ends logging for the specified key. To get the query log
+ * to date without ending the logger request the logging object by starting
+ * it again (which does nothing to an open log key) and call methods on it as
+ * desired.
+ *
+ * @param string $logging_key
+ * The logging key to log.
+ * @param string $key
+ * The database connection key for which we want to log.
+ *
+ * @return array
+ * The query log for the specified logging key and connection.
+ *
+ * @see \Drupal\Core\Database\Log
+ */
+ final public static function getLog($logging_key, $key = 'default') {
+ if (empty(self::$logs[$key])) {
+ return [];
+ }
+ $queries = self::$logs[$key]->get($logging_key);
+ self::$logs[$key]->end($logging_key);
+ return $queries;
+ }
+
+ /**
+ * Gets the connection object for the specified database key and target.
+ *
+ * @param string $target
+ * The database target name.
+ * @param string $key
+ * The database connection key. Defaults to NULL which means the active key.
+ *
+ * @return \Drupal\Core\Database\Connection
+ * The corresponding connection object.
+ */
+ final public static function getConnection($target = 'default', $key = NULL) {
+ if (!isset($key)) {
+ // By default, we want the active connection, set in setActiveConnection.
+ $key = self::$activeKey;
+ }
+ // If the requested target does not exist, or if it is ignored, we fall back
+ // to the default target. The target is typically either "default" or
+ // "replica", indicating to use a replica SQL server if one is available. If
+ // it's not available, then the default/primary server is the correct server
+ // to use.
+ if (!empty(self::$ignoreTargets[$key][$target]) || !isset(self::$databaseInfo[$key][$target])) {
+ $target = 'default';
+ }
+
+ if (!isset(self::$connections[$key][$target])) {
+ // If necessary, a new connection is opened.
+ self::$connections[$key][$target] = self::openConnection($key, $target);
+ }
+ return self::$connections[$key][$target];
+ }
+
+ /**
+ * Determines if there is an active connection.
+ *
+ * Note that this method will return FALSE if no connection has been
+ * established yet, even if one could be.
+ *
+ * @return bool
+ * TRUE if there is at least one database connection established, FALSE
+ * otherwise.
+ */
+ final public static function isActiveConnection() {
+ return !empty(self::$activeKey) && !empty(self::$connections) && !empty(self::$connections[self::$activeKey]);
+ }
+
+ /**
+ * Sets the active connection to the specified key.
+ *
+ * @return string|null
+ * The previous database connection key.
+ */
+ final public static function setActiveConnection($key = 'default') {
+ if (!empty(self::$databaseInfo[$key])) {
+ $old_key = self::$activeKey;
+ self::$activeKey = $key;
+ return $old_key;
+ }
+ }
+
+ /**
+ * Process the configuration file for database information.
+ *
+ * @param 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.
+ */
+ final public static function parseConnectionInfo(array $info) {
+ // If there is no "driver" property, then we assume it's an array of
+ // possible connections for this target. Pick one at random. That allows
+ // us to have, for example, multiple replica servers.
+ if (empty($info['driver'])) {
+ $info = $info[mt_rand(0, count($info) - 1)];
+ }
+ // Parse the prefix information.
+ if (!isset($info['prefix'])) {
+ // Default to an empty prefix.
+ $info['prefix'] = [
+ 'default' => '',
+ ];
+ }
+ elseif (!is_array($info['prefix'])) {
+ // Transform the flat form into an array form.
+ $info['prefix'] = [
+ 'default' => $info['prefix'],
+ ];
+ }
+ return $info;
+ }
+
+ /**
+ * Adds database connection information for a given key/target.
+ *
+ * This method allows to add new connections at runtime.
+ *
+ * Under normal circumstances the preferred way to specify database
+ * credentials is via settings.php. However, this method allows them to be
+ * added at arbitrary times, such as during unit tests, when connecting to
+ * admin-defined third party databases, etc.
+ *
+ * If the given key/target pair already exists, this method will be ignored.
+ *
+ * @param string $key
+ * The database key.
+ * @param string $target
+ * The database target name.
+ * @param 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.
+ */
+ final public static function addConnectionInfo($key, $target, array $info) {
+ if (empty(self::$databaseInfo[$key][$target])) {
+ self::$databaseInfo[$key][$target] = self::parseConnectionInfo($info);
+ }
+ }
+
+ /**
+ * Gets information on the specified database connection.
+ *
+ * @param string $key
+ * (optional) The connection key for which to return information.
+ *
+ * @return array|null
+ */
+ final public static function getConnectionInfo($key = 'default') {
+ if (!empty(self::$databaseInfo[$key])) {
+ return self::$databaseInfo[$key];
+ }
+ }
+
+ /**
+ * Gets connection information for all available databases.
+ *
+ * @return array
+ */
+ final public static function getAllConnectionInfo() {
+ return self::$databaseInfo;
+ }
+
+ /**
+ * Sets connection information for multiple databases.
+ *
+ * @param array $databases
+ * A multi-dimensional array specifying database connection parameters, as
+ * defined in settings.php.
+ */
+ final public static function setMultipleConnectionInfo(array $databases) {
+ foreach ($databases as $key => $targets) {
+ foreach ($targets as $target => $info) {
+ self::addConnectionInfo($key, $target, $info);
+ }
+ }
+ }
+
+ /**
+ * Rename a connection and its corresponding connection information.
+ *
+ * @param string $old_key
+ * The old connection key.
+ * @param string $new_key
+ * The new connection key.
+ *
+ * @return bool
+ * TRUE in case of success, FALSE otherwise.
+ */
+ final public static function renameConnection($old_key, $new_key) {
+ if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) {
+ // Migrate the database connection information.
+ self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key];
+ unset(self::$databaseInfo[$old_key]);
+
+ // Migrate over the DatabaseConnection object if it exists.
+ if (isset(self::$connections[$old_key])) {
+ self::$connections[$new_key] = self::$connections[$old_key];
+ unset(self::$connections[$old_key]);
+ }
+
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Remove a connection and its corresponding connection information.
+ *
+ * @param string $key
+ * The connection key.
+ *
+ * @return bool
+ * TRUE in case of success, FALSE otherwise.
+ */
+ final public static function removeConnection($key) {
+ if (isset(self::$databaseInfo[$key])) {
+ self::closeConnection(NULL, $key);
+ unset(self::$databaseInfo[$key]);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Opens a connection to the server specified by the given key and target.
+ *
+ * @param string $key
+ * The database connection key, as specified in settings.php. The default is
+ * "default".
+ * @param string $target
+ * The database target to open.
+ *
+ * @throws \Drupal\Core\Database\ConnectionNotDefinedException
+ * @throws \Drupal\Core\Database\DriverNotSpecifiedException
+ */
+ final protected static function openConnection($key, $target) {
+ // If the requested database does not exist then it is an unrecoverable
+ // error.
+ if (!isset(self::$databaseInfo[$key])) {
+ throw new ConnectionNotDefinedException('The specified database connection is not defined: ' . $key);
+ }
+
+ if (!$driver = self::$databaseInfo[$key][$target]['driver']) {
+ throw new DriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
+ }
+
+ if (!empty(self::$databaseInfo[$key][$target]['namespace'])) {
+ $driver_class = self::$databaseInfo[$key][$target]['namespace'] . '\\Connection';
+ }
+ else {
+ // Fallback for Drupal 7 settings.php.
+ $driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
+ }
+
+ $pdo_connection = $driver_class::open(self::$databaseInfo[$key][$target]);
+ $new_connection = new $driver_class($pdo_connection, self::$databaseInfo[$key][$target]);
+ $new_connection->setTarget($target);
+ $new_connection->setKey($key);
+
+ // If we have any active logging objects for this connection key, we need
+ // to associate them with the connection we just opened.
+ if (!empty(self::$logs[$key])) {
+ $new_connection->setLogger(self::$logs[$key]);
+ }
+
+ return $new_connection;
+ }
+
+ /**
+ * Closes a connection to the server specified by the given key and target.
+ *
+ * @param string $target
+ * The database target name. Defaults to NULL meaning that all target
+ * connections will be closed.
+ * @param string $key
+ * The database connection key. Defaults to NULL which means the active key.
+ */
+ public static function closeConnection($target = NULL, $key = NULL) {
+ // Gets the active connection by default.
+ if (!isset($key)) {
+ $key = self::$activeKey;
+ }
+ // To close a connection, it needs to be set to NULL and removed from the
+ // static variable. In all cases, closeConnection() might be called for a
+ // connection that was not opened yet, in which case the key is not defined
+ // yet and we just ensure that the connection key is undefined.
+ if (isset($target)) {
+ if (isset(self::$connections[$key][$target])) {
+ self::$connections[$key][$target]->destroy();
+ self::$connections[$key][$target] = NULL;
+ }
+ unset(self::$connections[$key][$target]);
+ }
+ else {
+ if (isset(self::$connections[$key])) {
+ foreach (self::$connections[$key] as $target => $connection) {
+ self::$connections[$key][$target]->destroy();
+ self::$connections[$key][$target] = NULL;
+ }
+ }
+ unset(self::$connections[$key]);
+ }
+ }
+
+ /**
+ * Instructs the system to temporarily ignore a given key/target.
+ *
+ * At times we need to temporarily disable replica queries. To do so, call this
+ * method with the database key and the target to disable. That database key
+ * will then always fall back to 'default' for that key, even if it's defined.
+ *
+ * @param string $key
+ * The database connection key.
+ * @param string $target
+ * The target of the specified key to ignore.
+ */
+ public static function ignoreTarget($key, $target) {
+ self::$ignoreTargets[$key][$target] = TRUE;
+ }
+
+ /**
+ * Converts a URL to a database connection info array.
+ *
+ * @param string $url
+ * The URL.
+ * @param string $root
+ * The root directory of the Drupal installation.
+ *
+ * @return array
+ * The database connection info.
+ *
+ * @throws \InvalidArgumentException
+ * Exception thrown when the provided URL does not meet the minimum
+ * requirements.
+ */
+ public static function convertDbUrlToConnectionInfo($url, $root) {
+ $info = parse_url($url);
+ if (!isset($info['scheme'], $info['host'], $info['path'])) {
+ throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
+ }
+ $info += [
+ 'user' => '',
+ 'pass' => '',
+ 'fragment' => '',
+ ];
+
+ // A SQLite database path with two leading slashes indicates a system path.
+ // Otherwise the path is relative to the Drupal root.
+ if ($info['path'][0] === '/') {
+ $info['path'] = substr($info['path'], 1);
+ }
+ if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
+ $info['path'] = $root . '/' . $info['path'];
+ }
+
+ $database = [
+ 'driver' => $info['scheme'],
+ 'username' => $info['user'],
+ 'password' => $info['pass'],
+ 'host' => $info['host'],
+ 'database' => $info['path'],
+ ];
+ if (isset($info['port'])) {
+ $database['port'] = $info['port'];
+ }
+ return $database;
+ }
+
+ /**
+ * Gets database connection info as a URL.
+ *
+ * @param string $key
+ * (Optional) The database connection key.
+ *
+ * @return string
+ * The connection info as a URL.
+ */
+ public static function getConnectionInfoAsUrl($key = 'default') {
+ $db_info = static::getConnectionInfo($key);
+ if ($db_info['default']['driver'] == 'sqlite') {
+ $db_url = 'sqlite://localhost/' . $db_info['default']['database'];
+ }
+ else {
+ $user = '';
+ if ($db_info['default']['username']) {
+ $user = $db_info['default']['username'];
+ if ($db_info['default']['password']) {
+ $user .= ':' . $db_info['default']['password'];
+ }
+ $user .= '@';
+ }
+
+ $db_url = $db_info['default']['driver'] . '://' . $user . $db_info['default']['host'];
+ if (isset($db_info['default']['port'])) {
+ $db_url .= ':' . $db_info['default']['port'];
+ }
+ $db_url .= '/' . $db_info['default']['database'];
+ }
+ if ($db_info['default']['prefix']['default']) {
+ $db_url .= '#' . $db_info['default']['prefix']['default'];
+ }
+ return $db_url;
+ }
+
+}