Commit 56653079 authored by alexpott's avatar alexpott

Issue #1953800 by dawehner, amateescu: Fixed Make the database connection serializable.

parent ea2ac545
......@@ -23,7 +23,7 @@
*
* @see http://php.net/manual/book.pdo.php
*/
abstract class Connection extends PDO {
abstract class Connection implements \Serializable {
/**
* The database target this connection is for.
......@@ -100,6 +100,13 @@ abstract class Connection extends PDO {
*/
protected $temporaryNameIndex = 0;
/**
* The actual PDO connection.
*
* @var \PDO
*/
protected $connection;
/**
* The connection information for this connection object.
*
......@@ -135,22 +142,33 @@ abstract class Connection extends PDO {
*/
protected $prefixReplace = array();
function __construct($dsn, $username, $password, $driver_options = array()) {
/**
* Constructs a Connection object.
*/
public function __construct(PDO $connection, array $connection_options) {
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
// Because the other methods don't seem to work right.
$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
// Call PDO::__construct and PDO::setAttribute.
parent::__construct($dsn, $username, $password, $driver_options);
$this->setPrefix(isset($connection_options['prefix']) ? $connection_options['prefix'] : '');
// Set a Statement class, unless the driver opted out.
if (!empty($this->statementClass)) {
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
$connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
}
$this->connection = $connection;
$this->connectionOptions = $connection_options;
}
/**
* Opens a PDO connection.
*
* @param array $connection_options
* The database connection settings array.
*
* @return \PDO
* A \PDO object.
*/
public static function open(array &$connection_options = array()) { }
/**
* Destroys this Connection object.
*
......@@ -163,7 +181,7 @@ public function destroy() {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
$this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
$this->schema = NULL;
}
......@@ -318,8 +336,7 @@ public function tablePrefix($table = 'default') {
public function prepareQuery($query) {
$query = $this->prefixTables($query);
// Call PDO::prepare.
return parent::prepare($query);
return $this->connection->prepare($query);
}
/**
......@@ -532,7 +549,7 @@ public function query($query, array $args = array(), $options = array()) {
case Database::RETURN_AFFECTED:
return $stmt->rowCount();
case Database::RETURN_INSERT_ID:
return $this->lastInsertId();
return $this->connection->lastInsertId();
case Database::RETURN_NULL:
return;
default:
......@@ -921,7 +938,7 @@ public function rollback($savepoint_name = 'drupal_transaction') {
$rolled_back_other_active_savepoints = TRUE;
}
}
parent::rollBack();
$this->connection->rollBack();
if ($rolled_back_other_active_savepoints) {
throw new TransactionOutOfOrderException();
}
......@@ -949,7 +966,7 @@ public function pushTransaction($name) {
$this->query('SAVEPOINT ' . $name);
}
else {
parent::beginTransaction();
$this->connection->beginTransaction();
}
$this->transactionLayers[$name] = $name;
}
......@@ -1000,7 +1017,7 @@ protected function popCommittableTransactions() {
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!parent::commit()) {
if (!$this->connection->commit()) {
throw new TransactionCommitFailedException();
}
}
......@@ -1084,7 +1101,7 @@ protected function generateTemporaryTableName() {
* Returns the version of the database server.
*/
public function version() {
return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
/**
......@@ -1178,4 +1195,76 @@ public function commit() {
* also larger than the $existing_id if one was passed in.
*/
abstract public function nextId($existing_id = 0);
/**
* Prepares a statement for execution and returns a statement object
*
* Emulated prepared statements does not communicate with the database server
* so this method does not check the statement.
*
* @param string $statement
* This must be a valid SQL statement for the target database server.
* @param array $driver_options
* (optional) This array holds one or more key=>value pairs to set
* attribute values for the PDOStatement object that this method returns.
* You would most commonly use this to set the \PDO::ATTR_CURSOR value to
* \PDO::CURSOR_SCROLL to request a scrollable cursor. Some drivers have
* driver specific options that may be set at prepare-time. Defaults to an
* empty array.
*
* @return \PDOStatement|false
* If the database server successfully prepares the statement, returns a
* \PDOStatement object.
* If the database server cannot successfully prepare the statement returns
* FALSE or emits \PDOException (depending on error handling).
*
* @throws \PDOException
*
* @see \PDO::prepare()
*/
public function prepare($statement, array $driver_options = array()) {
return $this->connection->prepare($statement, $driver_options);
}
/**
* Quotes a string for use in a query.
*
* @param string $string
* The string to be quoted.
* @param int $parameter_type
* (optional) Provides a data type hint for drivers that have alternate
* quoting styles. Defaults to \PDO::PARAM_STR.
*
* @return string|bool
* A quoted string that is theoretically safe to pass into an SQL statement.
* Returns FALSE if the driver does not support quoting in this way.
*
* @see \PDO::quote()
*/
public function quote($string, $parameter_type = \PDO::PARAM_STR) {
return $this->connection->quote($string, $parameter_type);
}
/**
* {@inheritdoc}
*/
public function serialize() {
$connection = clone $this;
// Don't serialize the PDO connection and other lazy-instantiated members.
unset($connection->connection, $connection->schema, $connection->driverClasses);
return serialize(get_object_vars($connection));
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized) {
$data = unserialize($serialized);
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
// Re-establish the PDO connection using the original options.
$this->connection = static::open($this->connectionOptions);
}
}
......@@ -381,7 +381,9 @@ public static function addConnectionInfo($key, $target, $info) {
// Fallback for Drupal 7 settings.php.
$driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
}
$new_connection = new $driver_class(self::$databaseInfo[$key][$target]);
$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);
......
......@@ -36,7 +36,12 @@ class Connection extends DatabaseConnection {
*/
protected $needsCleanup = FALSE;
public function __construct(array $connection_options = array()) {
/**
* Constructs a Connection object.
*/
public function __construct(PDO $connection, array $connection_options = array()) {
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
......@@ -44,7 +49,12 @@ public function __construct(array $connection_options = array()) {
$this->transactionalDDLSupport = FALSE;
$this->connectionOptions = $connection_options;
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// The DSN should use either a socket or a host/port.
if (isset($connection_options['unix_socket'])) {
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
......@@ -61,22 +71,23 @@ public function __construct(array $connection_options = array()) {
'pdo' => array(),
);
$connection_options['pdo'] += array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// So we don't have to mess around with cursors and unbuffered queries by default.
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE,
);
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
$pdo = new PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force MySQL to use the UTF-8 character set. Also set the collation, if a
// certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
// for UTF-8.
if (!empty($connection_options['collation'])) {
$this->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
$pdo->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
}
else {
$this->exec('SET NAMES utf8');
$pdo->exec('SET NAMES utf8');
}
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
......@@ -94,7 +105,9 @@ public function __construct(array $connection_options = array()) {
'sql_mode' => "SET sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
);
// Set connection options.
$this->exec(implode('; ', $connection_options['init_commands']));
$pdo->exec(implode('; ', $connection_options['init_commands']));
return $pdo;
}
public function __destruct() {
......@@ -135,8 +148,8 @@ public function createDatabase($database) {
try {
// Create the database and set it as active.
$this->exec("CREATE DATABASE $database");
$this->exec("USE $database");
$this->connection->exec("CREATE DATABASE $database");
$this->connection->exec("USE $database");
}
catch (\Exception $e) {
throw new DatabaseNotFoundException($e->getMessage());
......@@ -204,7 +217,7 @@ protected function popCommittableTransactions() {
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!PDO::commit()) {
if (!$this->connection->commit()) {
throw new TransactionCommitFailedException();
}
}
......@@ -227,7 +240,7 @@ protected function popCommittableTransactions() {
$this->transactionLayers = array();
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
PDO::commit();
$this->connection->commit();
}
else {
throw $e;
......
......@@ -34,7 +34,12 @@ class Connection extends DatabaseConnection {
*/
const DATABASE_NOT_FOUND = 7;
public function __construct(array $connection_options = array()) {
/**
* Constructs a connection object.
*/
public function __construct(PDO $connection, array $connection_options) {
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
......@@ -42,6 +47,21 @@ public function __construct(array $connection_options = array()) {
// but we'll only enable it if standard transactions are.
$this->transactionalDDLSupport = $this->transactionSupport;
$this->connectionOptions = $connection_options;
// Force PostgreSQL to use the UTF-8 character set by default.
$this->connection->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->connection->exec(implode('; ', $connection_options['init_commands']));
}
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// Default to TCP connection on port 5432.
if (empty($connection_options['port'])) {
$connection_options['port'] = 5432;
......@@ -61,8 +81,6 @@ public function __construct(array $connection_options = array()) {
$connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
}
$this->connectionOptions = $connection_options;
$connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
......@@ -71,6 +89,7 @@ public function __construct(array $connection_options = array()) {
'pdo' => array(),
);
$connection_options['pdo'] += array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Prepared statements are most effective for performance when queries
// are recycled (used several times). However, if they are not re-used,
// prepared statements become ineffecient. Since most of Drupal's
......@@ -81,17 +100,12 @@ public function __construct(array $connection_options = array()) {
// Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
$pdo = new PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force PostgreSQL to use the UTF-8 character set by default.
$this->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
}
return $pdo;
}
public function query($query, array $args = array(), $options = array()) {
$options += $this->defaultOptions();
......@@ -124,7 +138,7 @@ public function query($query, array $args = array(), $options = array()) {
case Database::RETURN_AFFECTED:
return $stmt->rowCount();
case Database::RETURN_INSERT_ID:
return $this->lastInsertId($options['sequence_name']);
return $this->connection->lastInsertId($options['sequence_name']);
case Database::RETURN_NULL:
return;
default:
......
......@@ -66,7 +66,12 @@ class Connection extends DatabaseConnection {
*/
var $tableDropped = FALSE;
public function __construct(array $connection_options = array()) {
/**
* Constructs a \Drupal\Core\Database\Driver\sqlite\Connection object.
*/
public function __construct(PDO $connection, array $connection_options) {
parent::__construct($connection, $connection_options);
// We don't need a specific PDOStatement class here, we simulate it below.
$this->statementClass = NULL;
......@@ -75,16 +80,6 @@ public function __construct(array $connection_options = array()) {
$this->connectionOptions = $connection_options;
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
parent::__construct('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
// Attach one database for each registered prefix.
$prefixes = $this->prefixes;
foreach ($prefixes as $table => &$prefix) {
......@@ -107,24 +102,43 @@ public function __construct(array $connection_options = array()) {
// Detect support for SAVEPOINT.
$version = $this->query('SELECT sqlite_version()')->fetchField();
$this->savepointSupport = (version_compare($version, '3.6.8') >= 0);
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
$pdo = new PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
// Create functions needed by SQLite.
$this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf'));
$this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest'));
$this->sqliteCreateFunction('pow', 'pow', 2);
$this->sqliteCreateFunction('length', 'strlen', 1);
$this->sqliteCreateFunction('md5', 'md5', 1);
$this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat'));
$this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
$this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
$this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
$pdo->sqliteCreateFunction('if', array(__CLASS__, 'sqlFunctionIf'));
$pdo->sqliteCreateFunction('greatest', array(__CLASS__, 'sqlFunctionGreatest'));
$pdo->sqliteCreateFunction('pow', 'pow', 2);
$pdo->sqliteCreateFunction('length', 'strlen', 1);
$pdo->sqliteCreateFunction('md5', 'md5', 1);
$pdo->sqliteCreateFunction('concat', array(__CLASS__, 'sqlFunctionConcat'));
$pdo->sqliteCreateFunction('substring', array(__CLASS__, 'sqlFunctionSubstring'), 3);
$pdo->sqliteCreateFunction('substring_index', array(__CLASS__, 'sqlFunctionSubstringIndex'), 3);
$pdo->sqliteCreateFunction('rand', array(__CLASS__, 'sqlFunctionRand'));
// Execute sqlite init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
$pdo->exec(implode('; ', $connection_options['init_commands']));
}
return $pdo;
}
/**
* Destructor for the SQLite connection.
*
......@@ -158,14 +172,14 @@ public function __destruct() {
/**
* SQLite compatibility implementation for the IF() SQL function.
*/
public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
return $condition ? $expr1 : $expr2;
}
/**
* SQLite compatibility implementation for the GREATEST() SQL function.
*/
public function sqlFunctionGreatest() {
public static function sqlFunctionGreatest() {
$args = func_get_args();
foreach ($args as $k => $v) {
if (!isset($v)) {
......@@ -183,7 +197,7 @@ public function sqlFunctionGreatest() {
/**
* SQLite compatibility implementation for the CONCAT() SQL function.
*/
public function sqlFunctionConcat() {
public static function sqlFunctionConcat() {
$args = func_get_args();
return implode('', $args);
}
......@@ -191,14 +205,14 @@ public function sqlFunctionConcat() {
/**
* SQLite compatibility implementation for the SUBSTRING() SQL function.
*/
public function sqlFunctionSubstring($string, $from, $length) {
public static function sqlFunctionSubstring($string, $from, $length) {
return substr($string, $from - 1, $length);
}
/**
* SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
*/
public function sqlFunctionSubstringIndex($string, $delimiter, $count) {
public static function sqlFunctionSubstringIndex($string, $delimiter, $count) {
// If string is empty, simply return an empty string.
if (empty($string)) {
return '';
......@@ -247,7 +261,7 @@ public function prepare($query, $options = array()) {
* the world.
*/
public function PDOPrepare($query, array $options = array()) {
return parent::prepare($query, $options);
return $this->connection->prepare($query, $options);
}
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
......@@ -354,7 +368,7 @@ public function rollback($savepoint_name = 'drupal_transaction') {
}
}
if ($this->supportsTransactions()) {
PDO::rollBack();
$this->connection->rollBack();
}
}
......@@ -394,9 +408,9 @@ public function popTransaction($name) {
// If there was any rollback() we should roll back whole transaction.
if ($this->willRollback) {
$this->willRollback = FALSE;
PDO::rollBack();
$this->connection->rollBack();
}
elseif (!PDO::commit()) {
elseif (!$this->connection->commit()) {
throw new TransactionCommitFailedException();
}
}
......
......@@ -123,4 +123,5 @@ function testConnectionOptions() {
$connectionOptions = $db->getConnectionOptions();
$this->assertNotEqual($connection_info['default']['database'], $connectionOptions['database'], 'The test connection info database does not match the current connection options database.');
}
}
......@@ -226,4 +226,64 @@ function testOpenSelectQueryClose() {
$this->assertNoConnection($id);
}
/**
* Tests the serialization and unserialization of a database connection.
*/
public function testConnectionSerialization() {
$db = Database::getConnection('default', 'default');
try {
$serialized = serialize($db);
$this->pass('The database connection can be serialized.');
$unserialized = unserialize($serialized);
$this->assertTrue(get_class($unserialized) === get_class($db));
}
catch (\Exception $e) {
$this->fail('The database connection cannot be serialized.');
}
// Ensure that all properties on the unserialized object are the same.
$db_reflection = new \ReflectionObject($db);
$unserialized_reflection = new \ReflectionObject($unserialized);
foreach ($db_reflection->getProperties() as $value) {
// Skip the pdo connection object.
if ($value->getName() == 'connection') {
continue;
}
$value->setAccessible(TRUE);
$unserialized_property = $unserialized_reflection->getProperty($value->getName());
$unserialized_property->setAccessible(TRUE);
$this->assertEqual($unserialized_property->getValue($unserialized), $value->getValue($db));
}
}
/**
* Tests pdo options override.
*/
public function testConnectionOpen() {
$connection = Database::getConnection('default');
$reflection = new \ReflectionObject($connection);
$connection_property = $reflection->getProperty('connection');
$connection_property->setAccessible(TRUE);
$error_mode = $connection_property->getValue($connection)
->getAttribute(\PDO::ATTR_ERRMODE);
$this->assertEqual($error_mode, \PDO::ERRMODE_EXCEPTION, 'Ensure the default error mode is set to exception.');
$connection = Database::getConnectionInfo('default');
$connection['default']['pdo'][\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_SILENT;
Database::addConnectionInfo('test', 'default', $connection['default']);
$connection = Database::getConnection('default', 'test');
$reflection = new \ReflectionObject($connection);
$connection_property = $reflection->getProperty('connection');
$connection_property->setAccessible(TRUE);
$error_mode = $connection_property->getValue($connection)
->getAttribute(\PDO::ATTR_ERRMODE);
$this->assertEqual($error_mode, \PDO::ERRMODE_SILENT, 'Ensure PDO connection options can be overridden.');
Database::removeConnection('test');
}
}
......@@ -664,4 +664,13 @@ function testMultiFormSameNameErrorClass() {
$this->assertFieldByXpath('//input[@id="edit-name" and contains(@class, "error")]', NULL, 'Error input form element class found for first element.');
$this->assertNoFieldByXpath('//input[@id="edit-name--2" and contains(@class, "error")]', NULL, 'No error input form element class found for second element.');
}
/**
* Tests a form with a form state storing a database connection.
*/
public function testFormStateDatabaseConnection() {
$this->assertNoText('Database connection found');
$this->drupalPost('form-test/form_state-database', array(), t('Submit'));
$this->assertText('Database connection found');
}
}
......@@ -5,6 +5,8 @@
* Helper module for the form API tests.
*/
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\form_test\Callbacks;
use Drupal\form_test\FormTestObject;
use Drupal\form_test\SystemConfigFormTestForm;
......@@ -353,6 +355,13 @@ function form_test_menu() {
'access callback' => TRUE,
);
$items['form-test/form_state-database'] = array(
'title' => t('Form state with a database connection'),
'page callback' => 'drupal_get_form',
'page arguments' => array('form_test_form_state_database'),
'access callback' => TRUE,
);
return $items;
}
......@@ -2466,3 +2475,40 @@ function form_test_group_vertical_tabs() {
);
return $form;
}
/**
* Builds a form which gets the database connection stored in the form state.
*/
function form_test_form_state_database($form, &$form_state) {
$form['text'] = array(
'#type' => 'textfield',
'#title' => t('Text field'),
);
$form['test_submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
$db = Database::getConnection('default');
$form_state['storage']['database'] = $db;
$form_state['storage']['database_class'] = get_class($db);
if (isset($form_state['storage']['database_connection_found'])) {
$form['database']['#markup'] = 'Database connection found';
}
return $form;
}
/**
* Form submit handler for database form_state test.
*/
function form_test_form_state_database_submit($form, &$form_state) {
$form_state['cache'] = TRUE;
$form_state['rebuild'] = TRUE;
if ($form_state['storage']['database'] instanceof $form_state['storage']['database_class']) {
$form_state['storage']['database_connection_found'] = TRUE;