1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-08 07:47:00 +02:00

Refactor of WireDatabasePDO to add support for separate read-only database connection

This commit is contained in:
Ryan Cramer
2021-04-02 16:18:44 -04:00
parent d5a85e07ce
commit 6fa201c522
2 changed files with 492 additions and 173 deletions

View File

@@ -294,13 +294,13 @@ $config->sessionExpireSeconds = 86400;
* automatically enabled. * automatically enabled.
* *
* ~~~~~ * ~~~~~
* $config->sessionAllow = function($session) { * $config->sessionAllow = function($session) use($config) {
* *
* // if there is a session cookie, a session is likely already in use so keep it going * // if there is a session cookie, a session is likely already in use so keep it going
* if($session->hasCookie()) return true; * if($session->hasCookie()) return true;
* *
* // if URL is an admin URL, allow session (replace /processwire/ with your admin URL) * // if URL is an admin URL, allow session (replace /processwire/ with your admin URL)
* if(strpos($_SERVER['REQUEST_URI'], '/processwire/) === 0) return true; * if(strpos($config->requestPath(), '/processwire/) === 0) return true;
* *
* // otherwise disallow session * // otherwise disallow session
* return false; * return false;
@@ -1189,7 +1189,59 @@ $config->dbQueryLogMax = 500;
*/ */
$config->dbStripMB4 = false; $config->dbStripMB4 = false;
/**
* Optional settings for read-only “reader” database connection
*
* All `$config->db*` settings above are for a read/write database connection. You can
* optionally maintain a separate read-only database connection to reduce costs and
* allow for further database scalability. Use of this feature requires an environment
* that supports a separate read-only database connection to the same database used by the
* read/write connection. When enabled, ProcessWire will direct all non-writing queries to
* the read-only connection, while queries that write to the database are directed to the
* read/write connection.
*
* Specify one or more existing `$config->db*` settings in the array to use that value for
* the read-only connection. To enable a separate read-only database connection, this array
* must contain at minimum a `host` or `socket` entry. Beyond that, values not present in
* this array will be pulled from the existing `$config->db*` settings. Note, when specifying
* settings in this array, omit the `db` prefix and use lowercase for the first letter. For
* example, use `host` rather than `dbHost`, `name` rather than `dbName`, etc.
*
* When using this feature, you may want to exclude your admin from it, as the admin is an
* environment that's designed for both read and write, so there's less reason to maintain
* separate read-only and read/write connections in the admin. See the examples below.
*
* For more details see: https://processwire.com/blog/posts/pw-3.0.175/
*
* ~~~~~
* // allow read-only database connection always…
* $config->dbReader = [
* 'host' => 'readonly.mydb.domain.com'
* ];
*
* // …or, use read-only connection only if not in the admin…
* if(!$config->requestPath('/processwire/')) {
* $config->dbReader = [ 'host' => 'readonly.mydb.domain.com' ];
* }
*
* // …or limit read-only to GET requests, exclude admin and contact page…
* $skipPaths = [ '/processwire/', '/contact/' ];
* if($config->requestMethod('GET') && !$config->requestPath($skipPaths)) {
* $config->dbReader = [ 'host' => 'readonly.mydb.domain.com' ];
* }
* ~~~~~
*
* @var array
* @since 3.0.175
* @see https://processwire.com/blog/posts/pw-3.0.175/
*
*/
$config->dbReader = array(
// 'host' => 'readonly.mydb.domain.com',
// 'port' => 3306,
// 'name' => 'mydb',
// …etc., though most likely you will only need 'host' entry to setup a reader
);
/*** 8. MODULES *********************************************************************************/ /*** 8. MODULES *********************************************************************************/

View File

@@ -3,10 +3,9 @@
/** /**
* ProcessWire PDO Database * ProcessWire PDO Database
* *
* Serves as a wrapper to PHP's PDO class * Serves as a wrapper to PHPs PDO class
* *
* * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
*/ */
@@ -29,92 +28,140 @@ class WireDatabasePDO extends Wire implements WireDatabase {
/** /**
* Log of all queries performed in this instance * Log of all queries performed in this instance
* *
* @var array
*
*/ */
protected $queryLog = array(); protected $queryLog = array();
/** /**
* Max queries allowedin the query log (set from $config->dbQueryLogMax) * Max queries allowedin the query log (set from $config->dbQueryLogMax)
* *
* @var int * @var int
* *
*/ */
protected $queryLogMax = 500; protected $queryLogMax = 500;
/** /**
* Whether queries will be logged * Whether queries will be logged
* *
*/ */
protected $debugMode = false; protected $debugMode = false;
/** /**
* Cached result from getTables() method * Cached result from getTables() method
* *
* @var array * @var array
* *
*/ */
protected $tablesCache = array(); protected $tablesCache = array();
/** /**
* Instance of PDO * Data for read-write PDO connection
* *
* @var \PDO * @var array
* *
*/ */
protected $pdo = null; protected $writer = array(
'pdo' => null,
'init' => false,
'commands' => array(
// commands that rewrite a writable connection
'alter',
'call',
'comment',
'commit',
'create',
'delete',
'drop',
'insert',
'lock',
'merge',
'rename',
'replace',
'rollback',
'savepoint',
'set',
'start',
'truncate',
'unlock',
'update',
)
);
/**
* Data for read-only PDO connection
*
* @var array
*
*/
protected $reader = array(
'pdo' => null,
'has' => false, // is reader available?
'init' => false, // is reader initalized?
'allow' => true, // is reader allowed? (false when in transaction, etc.)
);
/**
* Last used PDO connection
*
* @var null|\PDO
*
*/
protected $pdoLast = null;
/** /**
* Whether or not our _init() has been called for the current $pdo connection * Whether or not our _init() has been called for the current $pdo connection
* *
* @var bool * @var bool
* *
*/ */
protected $init = false; protected $init = false;
/** /**
* Strip 4-byte characters in “quote” and “escapeStr” methods? (only when dbEngine is not utf8mb4) * Strip 4-byte characters in “quote” and “escapeStr” methods? (only when dbEngine is not utf8mb4)
* *
* @var bool * @var bool
* *
*/ */
protected $stripMB4 = false; protected $stripMB4 = false;
/** /**
* Lowercase value of $config->dbEngine * Lowercase value of $config->dbEngine
* *
* @var string * @var string
* *
*/ */
protected $engine = ''; protected $engine = '';
/** /**
* Lowercase value of $config->dbCharset * Lowercase value of $config->dbCharset
* *
* @var string * @var string
* *
*/ */
protected $charset = ''; protected $charset = '';
/** /**
* Regular comparison operators * Regular comparison operators
* *
* @var array * @var array
* *
*/ */
protected $comparisonOperators = array('=', '<', '>', '>=', '<=', '<>', '!='); protected $comparisonOperators = array('=', '<', '>', '>=', '<=', '<>', '!=');
/** /**
* Bitwise comparison operators * Bitwise comparison operators
* *
* @var array * @var array
* *
*/ */
protected $bitwiseOperators = array('&', '~', '&~', '|', '^', '<<', '>>'); protected $bitwiseOperators = array('&', '~', '&~', '|', '^', '<<', '>>');
/** /**
* Substitute variable names according to engine as used by getVariable() method * Substitute variable names according to engine as used by getVariable() method
* *
* @var array * @var array
* *
*/ */
protected $subVars = array( protected $subVars = array(
'myisam' => array(), 'myisam' => array(),
@@ -126,130 +173,225 @@ class WireDatabasePDO extends Wire implements WireDatabase {
/** /**
* PDO connection settings * PDO connection settings
* *
*/ */
private $pdoConfig = array( private $pdoConfig = array(
'dsn' => '', 'dsn' => '',
'user' => '', 'user' => '',
'pass' => '', 'pass' => '',
'options' => '', 'options' => '',
'reader' => array(
'dsn' => '',
'user' => '',
'pass' => '',
'options' => '',
),
); );
/** /**
* Cached values from getVariable method * Cached values from getVariable method
* *
* @var array associative of name => value * @var array associative of name => value
* *
*/ */
protected $variableCache = array(); protected $variableCache = array();
/** /**
* Cached InnoDB stopwords (keys are the stopwords and values are irrelevant) * Cached InnoDB stopwords (keys are the stopwords and values are irrelevant)
* *
* @var array|null Becomes array once loaded * @var array|null Becomes array once loaded
* *
*/ */
protected $stopwordCache = null; protected $stopwordCache = null;
/** /**
* Create a new PDO instance from ProcessWire $config API variable * Create a new PDO instance from ProcessWire $config API variable
* *
* If you need to make other PDO connections, just instantiate a new WireDatabasePDO (or native PDO) * If you need to make other PDO connections, just instantiate a new WireDatabasePDO (or native PDO)
* rather than calling this getInstance method. * rather than calling this getInstance method.
* *
* #pw-internal * #pw-internal
* *
* @param Config $config * @param Config $config
* @return WireDatabasePDO *
* @return WireDatabasePDO
* @throws WireException * @throws WireException
* *
*/ */
public static function getInstance(Config $config) { public static function getInstance(Config $config) {
if(!class_exists('\PDO')) { if(!class_exists('\PDO')) {
throw new WireException('Required PDO class (database) not found - please add PDO support to your PHP.'); throw new WireException('Required PDO class (database) not found - please add PDO support to your PHP.');
} }
$host = $config->dbHost;
$username = $config->dbUser; $username = $config->dbUser;
$password = $config->dbPass; $password = $config->dbPass;
$name = $config->dbName;
$socket = $config->dbSocket;
$charset = $config->dbCharset; $charset = $config->dbCharset;
$options = $config->dbOptions; $options = $config->dbOptions;
$reader = $config->dbReader;
$initCommand = str_replace('{charset}', $charset, $config->dbInitCommand); $initCommand = str_replace('{charset}', $charset, $config->dbInitCommand);
if($socket) {
// if socket is provided ignore $host and $port and use $socket instead:
$dsn = "mysql:unix_socket=$socket;dbname=$name;";
} else {
$dsn = "mysql:dbname=$name;host=$host";
$port = $config->dbPort;
if($port) $dsn .= ";port=$port";
}
if(!is_array($options)) $options = array(); if(!is_array($options)) $options = array();
if(!isset($options[\PDO::ATTR_ERRMODE])) { if(!isset($options[\PDO::ATTR_ERRMODE])) {
$options[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION; $options[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION;
} }
if($initCommand && !isset($options[\PDO::MYSQL_ATTR_INIT_COMMAND])) { if($initCommand && !isset($options[\PDO::MYSQL_ATTR_INIT_COMMAND])) {
$options[\PDO::MYSQL_ATTR_INIT_COMMAND] = $initCommand; $options[\PDO::MYSQL_ATTR_INIT_COMMAND] = $initCommand;
} }
$database = new WireDatabasePDO($dsn, $username, $password, $options); $dsnArray = array(
'socket' => $config->dbSocket,
'name' => $config->dbName,
'host' => $config->dbHost,
'port' => $config->dbPort,
);
$data = array(
'dsn' => self::dsn($dsnArray),
'user' => $username,
'pass' => $password,
'options' => $options,
);
if(!empty($reader) && (!empty($reader['host']) || !empty($reader['socket']))) {
$reader['dsn'] = self::dsn(array_merge($dsnArray, $reader));
$reader = array_merge($data, $reader);
$data['reader'] = $reader;
}
$database = new WireDatabasePDO($data);
$database->setDebugMode($config->debug); $database->setDebugMode($config->debug);
$config->wire($database); $config->wire($database);
$database->_init(); // $database->_init();
return $database; return $database;
} }
/** /**
* Construct WireDatabasePDO * Create a PDO DSN string from array
* *
* #pw-internal * #pw-internal
* *
* @param $dsn * @param array $options May contain keys: 'name', 'host', 'port', 'socket' (if applies), 'type' (default=mysql)
*
* @return string
* @since 3.0.175
*
*/
static public function dsn(array $options) {
$defaults = array(
'type' => 'mysql',
'socket' => '',
'name' => '',
'host' => '',
'port' => '',
);
$options = array_merge($defaults, $options);
if($options['socket']) {
// if socket is provided ignore $host and $port and use socket instead
$dsn = "mysql:unix_socket=$options[socket];dbname=$options[name];";
} else {
$dsn = "mysql:dbname=$options[name];host=$options[host]";
if($options['port']) $dsn .= ";port=$options[port]";
}
return $dsn;
}
/**
* Construct WireDatabasePDO
*
* ~~~~~
* // The following are required to construct a WireDatabasePDO
* $dsn = 'mysql:dbname=mydb;host=myhost;port=3306';
* $username = 'username';
* $password = 'password';
* $driver_options = []; // optional
*
* // Construct option A
* $db = new WireDatabasePDO($dsn, $username, $password, $driver_options);
*
* // Construct option B
* $db = new WireDatabasePDO([
* 'dsn' => $dsn,
* 'user' => $username,
* 'pass' => $password,
* 'options' => $driver_options, // optional
* 'reader' => [ // optional
* 'dsn' => '…',
* …
* ],
* …
* ]);
* ~~~~~
*
* #pw-internal
*
* @param string|array $dsn DSN string or (3.0.175+) optionally use array of connection options and omit all remaining arguments.
* @param null $username * @param null $username
* @param null $password * @param null $password
* @param array $driver_options * @param array $driver_options
* *
*/ */
public function __construct($dsn, $username = null, $password = null, array $driver_options = array()) { public function __construct($dsn, $username = null, $password = null, array $driver_options = array()) {
parent::__construct(); parent::__construct();
$this->pdoConfig['dsn'] = $dsn; if(is_array($dsn) && isset($dsn['dsn'])) {
$this->pdoConfig['user'] = $username; if($username !== null && empty($dsn['user'])) $dsn['user'] = $username;
$this->pdoConfig['pass'] = $password; if($password !== null && empty($dsn['pass'])) $dsn['pass'] = $password;
$this->pdoConfig['options'] = $driver_options; if(!isset($dsn['options'])) $dsn['options'] = $driver_options;
$this->pdo(); $this->pdoConfig = array_merge($this->pdoConfig, $dsn);
if(!empty($this->pdoConfig['reader']['dsn'])) $this->reader['has'] = true;
} else {
$this->pdoConfig['dsn'] = $dsn;
$this->pdoConfig['user'] = $username;
$this->pdoConfig['pass'] = $password;
$this->pdoConfig['options'] = $driver_options;
}
// $this->pdo();
} }
/** /**
* Additional initialization after DB connection established and Wire instance populated * Additional initialization after DB connection established and Wire instance populated
* *
* #pw-internal * #pw-internal
* *
* @param \PDO|null
*
*/ */
public function _init() { public function _init($pdo = null) {
if($this->init || !$this->isWired()) return;
$this->init = true; if(!$this->isWired()) return;
if($pdo === $this->reader['pdo']) {
if($this->reader['init']) return;
$this->reader['init'] = true;
} else {
if($this->writer['init']) return;
$this->writer['init'] = true;
if($pdo === null) $pdo = $this->writer['pdo'];
}
$config = $this->wire()->config; $config = $this->wire()->config;
$this->stripMB4 = $config->dbStripMB4 && strtolower($config->dbEngine) != 'utf8mb4';
$this->engine = strtolower($config->dbEngine); if(empty($this->engine)) {
$this->charset = strtolower($config->dbCharset); $this->engine = strtolower($config->dbEngine);
$this->queryLogMax = (int) $config->dbQueryLogMax; $this->charset = strtolower($config->dbCharset);
if($config->debug && $this->pdo) { $this->stripMB4 = $config->dbStripMB4 && $this->charset != 'utf8mb4';
$this->queryLogMax = (int) $config->dbQueryLogMax;
}
if($config->debug && $pdo) {
// custom PDO statement for debug mode // custom PDO statement for debug mode
$this->debugMode = true; $this->debugMode = true;
$this->pdo->setAttribute( $pdo->setAttribute(
\PDO::ATTR_STATEMENT_CLASS, \PDO::ATTR_STATEMENT_CLASS,
array(__NAMESPACE__ . "\\WireDatabasePDOStatement", array($this)) array(__NAMESPACE__ . "\\WireDatabasePDOStatement", array($this))
); );
} }
$sqlModes = $config->dbSqlModes; $sqlModes = $config->dbSqlModes;
if(is_array($sqlModes)) { if(is_array($sqlModes)) {
// ["5.7.0" => "remove:mode1,mode2/add:mode3"] // ["5.7.0" => "remove:mode1,mode2/add:mode3"]
foreach($sqlModes as $minVersion => $commands) { foreach($sqlModes as $minVersion => $commands) {
@@ -263,7 +405,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
if(empty($modes)) continue; if(empty($modes)) continue;
$action = 'set'; $action = 'set';
if(strpos($modes, ':')) list($action, $modes) = explode(':', $modes); if(strpos($modes, ':')) list($action, $modes) = explode(':', $modes);
$this->sqlMode(trim($action), trim($modes), $minVersion); $this->sqlMode(trim($action), trim($modes), $minVersion, $pdo);
} }
} }
} }
@@ -272,25 +414,143 @@ class WireDatabasePDO extends Wire implements WireDatabase {
/** /**
* Return the actual current PDO connection instance * Return the actual current PDO connection instance
* *
* If connection is lost, this will restore it automatically. * If connection is lost, this will restore it automatically.
* *
* #pw-group-PDO * #pw-group-PDO
* *
* @param string|\PDOStatement|null SQL, statement, or statement type (reader or primary) (3.0.175+)
*
* @return \PDO * @return \PDO
* *
*/ */
public function pdo() { public function pdo($type = null) {
if(!$this->pdo) { if($type === null) return $this->pdoWriter();
$this->init = false; return $this->pdoType($type);
$this->pdo = new \PDO( }
/**
* Return read-write (primary) PDO connection
*
* @return \PDO
* @since 3.0.175
*
*/
protected function pdoWriter() {
if(!$this->writer['pdo']) {
$this->writer['init'] = false;
$pdo = new \PDO(
$this->pdoConfig['dsn'], $this->pdoConfig['dsn'],
$this->pdoConfig['user'], $this->pdoConfig['user'],
$this->pdoConfig['pass'], $this->pdoConfig['pass'],
$this->pdoConfig['options'] $this->pdoConfig['options']
); );
$this->writer['pdo'] = $pdo;
$this->_init($pdo);
} else {
$pdo = $this->writer['pdo'];
} }
if(!$this->init) $this->_init(); $this->pdoLast = $pdo;
return $this->pdo; return $pdo;
}
/**
* Return read-only PDO connection if available or read/write PDO connection if not
*
* @return \PDO
* @since 3.0.175
*
*/
protected function pdoReader() {
if(!$this->allowReader()) return $this->pdoWriter();
if(!$this->reader['pdo']) {
$this->reader['init'] = false;
$pdo = new \PDO(
$this->pdoConfig['reader']['dsn'],
$this->pdoConfig['reader']['user'],
$this->pdoConfig['reader']['pass'],
$this->pdoConfig['reader']['options']
);
$this->reader['pdo'] = $pdo;
$this->_init($pdo);
} else {
$pdo = $this->reader['pdo'];
}
$this->pdoLast = $pdo;
return $pdo;
}
/**
* Return correct PDO instance type (reader or writer) based on given statement
*
* @param string|\PDOStatement $statement
* @param bool $getName Get name of PDO type rather than instance? (default=false)
* @return \PDO|string
*
*/
protected function pdoType(&$statement, $getName = false) {
$reader = 'reader';
$writer = 'writer';
if(!$this->reader['has']) return $getName ? $writer : $this->pdoWriter();
if($statement === $writer || $statement === $reader) {
$type = $statement;
} else if(!$this->reader['has']) {
$type = $writer;
} else if(!is_string($statement)) {
// PDOStatement or other, always return write
// @todo add support for inspection of PDOStatement
$type = $writer;
} else if(stripos($statement, 'select') === 0) {
$type = $reader;
} else if(stripos($statement, 'insert') === 0) {
$type = $writer;
} else {
$pos = strpos($statement, ' ');
$word = strtolower(($pos ? substr($statement, 0, $pos) : $statement));
if($word === 'set') {
// all 'set' commands are read-only allowed except autocommit and transaction
$word = trim(substr($statement, $pos + 1, 12));
if(stripos($word, 'autocommit') === 0 || stripos($word, 'transaction') === 0) {
$type = $writer;
} else {
$type = $reader;
}
} else if($word === 'lock') {
if(!$getName) $this->allowReader(false);
$type = $writer;
} else if($word === 'unlock') {
if(!$getName) $this->allowReader(true);
$type = $writer;
} else {
$type = in_array($word, $this->writer['commands']) ? $writer : $reader;
}
}
if($type === $reader && !$this->reader['allow']) $type = $writer;
if($getName) return $type;
return $type === 'reader' ? $this->pdoReader() : $this->pdoWriter();
}
/**
* Return last used PDO connection
*
* @return \PDO
* @since 3.0.175
*
*/
protected function pdoLast() {
if($this->pdoLast) {
$pdo = $this->pdoLast;
if($pdo === $this->reader['pdo'] && !$this->reader['allow']) $pdo = null;
} else {
$pdo = null;
}
if($pdo === null) $pdo = $this->pdoWriter();
return $pdo;
} }
/** /**
@@ -303,7 +563,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function errorCode() { public function errorCode() {
return $this->pdo()->errorCode(); return $this->pdoLast()->errorCode();
} }
/** /**
@@ -316,7 +576,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function errorInfo() { public function errorInfo() {
return $this->pdo()->errorInfo(); return $this->pdoLast()->errorInfo();
} }
/** /**
@@ -330,7 +590,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function getAttribute($attribute) { public function getAttribute($attribute) {
return $this->pdo()->getAttribute($attribute); return $this->pdoLast()->getAttribute($attribute);
} }
/** /**
@@ -345,7 +605,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function setAttribute($attribute, $value) { public function setAttribute($attribute, $value) {
return $this->pdo()->setAttribute($attribute, $value); return $this->pdoLast()->setAttribute($attribute, $value);
} }
/** /**
@@ -359,7 +619,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function lastInsertId($name = null) { public function lastInsertId($name = null) {
return $this->pdo()->lastInsertId($name); return $this->pdoWriter()->lastInsertId($name);
} }
/** /**
@@ -375,7 +635,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/ */
public function query($statement, $note = '') { public function query($statement, $note = '') {
if($this->debugMode) $this->queryLog($statement, $note); if($this->debugMode) $this->queryLog($statement, $note);
return $this->pdo()->query($statement); $pdo = $this->pdoType($statement);
return $pdo->query($statement);
} }
/** /**
@@ -388,7 +649,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function beginTransaction() { public function beginTransaction() {
return $this->pdo()->beginTransaction(); $this->allowReader(false);
return $this->pdoWriter()->beginTransaction();
} }
/** /**
@@ -401,7 +663,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function inTransaction() { public function inTransaction() {
return (bool) $this->pdo()->inTransaction(); return (bool) $this->pdoWriter()->inTransaction();
} }
/** /**
@@ -416,7 +678,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
public function supportsTransaction($table = '') { public function supportsTransaction($table = '') {
$engine = ''; $engine = '';
if($table) { if($table) {
$query = $this->prepare('SHOW TABLE STATUS WHERE name=:name'); $query = $this->pdoReader()->prepare('SHOW TABLE STATUS WHERE name=:name');
$query->bindValue(':name', $table); $query->bindValue(':name', $table);
$query->execute(); $query->execute();
if($query->rowCount()) { if($query->rowCount()) {
@@ -425,7 +687,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
} }
$query->closeCursor(); $query->closeCursor();
} else { } else {
$engine = $this->wire('config')->dbEngine; $engine = $this->engine;
} }
return strtoupper($engine) === 'INNODB'; return strtoupper($engine) === 'INNODB';
} }
@@ -456,7 +718,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function commit() { public function commit() {
return $this->pdo()->commit(); $this->allowReader(true);
return $this->pdoWriter()->commit();
} }
/** /**
@@ -469,7 +732,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function rollBack() { public function rollBack() {
return $this->pdo()->rollBack(); $this->allowReader(true);
return $this->pdoWriter()->rollBack();
} }
/** /**
@@ -513,7 +777,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
\PDO::ATTR_STATEMENT_CLASS => array(__NAMESPACE__ . "\\WireDatabasePDOStatement", array($this)) \PDO::ATTR_STATEMENT_CLASS => array(__NAMESPACE__ . "\\WireDatabasePDOStatement", array($this))
); );
} }
$pdoStatement = $this->pdo()->prepare($statement, $driver_options); $pdo = $this->reader['has'] ? $this->pdoType($statement) : $this->pdoWriter();
$pdoStatement = $pdo->prepare($statement, $driver_options);
if($this->debugMode) { if($this->debugMode) {
if($pdoStatement instanceof WireDatabasePDOStatement) { if($pdoStatement instanceof WireDatabasePDOStatement) {
/** @var WireDatabasePDOStatement $pdoStatement */ /** @var WireDatabasePDOStatement $pdoStatement */
@@ -544,7 +809,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
return $this->execute($statement); return $this->execute($statement);
} }
if($this->debugMode) $this->queryLog($statement, $note); if($this->debugMode) $this->queryLog($statement, $note);
return $this->pdo()->exec($statement); $pdo = $this->reader['has'] ? $this->pdoType($statement) : $this->pdoWriter();
return $pdo->exec($statement);
} }
/** /**
@@ -568,64 +834,31 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
* @param \PDOStatement $query * @param \PDOStatement $query
* @param bool $throw Whether or not to throw exception on query error (default=true) * @param bool $throw Whether or not to throw exception on query error (default=true)
* @param int $maxTries Max number of times it will attempt to retry query on error * @param int $maxTries Deprecated/argument does nothing (was: “Max number of times it will attempt to retry query on error”)
* @return bool True on success, false on failure. Note if you want this, specify $throw=false in your arguments. * @return bool True on success, false on failure. Note if you want this, specify $throw=false in your arguments.
* @throws \PDOException * @throws \PDOException
* *
*/ */
public function execute(\PDOStatement $query, $throw = true, $maxTries = 3) { public function execute(\PDOStatement $query, $throw = true, $maxTries = 3) {
$tryAgain = 0; try {
$_throw = $throw; $result = $query->execute();
} catch(\PDOException $e) {
do { $result = false;
try { if($query->errorCode() == '42S22') {
$result = $query->execute(); // unknown column error
$errorInfo = $query->errorInfo();
} catch(\PDOException $e) { if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
$this->unknownColumnError($matches[1]);
$result = false;
$error = $e->getMessage();
$throw = false; // temporarily disable while we try more
if($tryAgain === 0) {
// setup retry loop
$tryAgain = $maxTries;
} else {
// decrement retry loop
$tryAgain--;
}
if(stripos($error, 'MySQL server has gone away') !== false) {
// forces reconection on next query
$this->wire('database')->closeConnection();
} else if($query->errorCode() == '42S22') {
// unknown column error
$errorInfo = $query->errorInfo();
if(preg_match('/[\'"]([_a-z0-9]+\.[_a-z0-9]+)[\'"]/i', $errorInfo[2], $matches)) {
$this->unknownColumnError($matches[1]);
}
} else {
// some other error that we don't have retry plans for
// tryAgain=0 will force the loop to stop
$tryAgain = 0;
}
if($tryAgain < 1) {
// if at end of retry loop, restore original throw state
$throw = $_throw;
}
if($throw) {
throw $e;
} else {
$this->error($error);
} }
} }
if($throw) {
} while($tryAgain && !$result); throw $e;
} else {
$this->error($e->getMessage());
}
if($maxTries) {} // ignore, argument no longer used
}
return $result; return $result;
} }
@@ -677,6 +910,10 @@ class WireDatabasePDO extends Wire implements WireDatabase {
$this->queryLog['error'] = "$qty additional queries omitted because \$config->dbQueryLogMax = $this->queryLogMax"; $this->queryLog['error'] = "$qty additional queries omitted because \$config->dbQueryLogMax = $this->queryLogMax";
return false; return false;
} else { } else {
if($this->reader['has']) {
$type = $this->pdoType($sql, true);
$note = trim("$note [$type]");
}
$this->queryLog[] = $sql . ($note ? " -- $note" : ""); $this->queryLog[] = $sql . ($note ? " -- $note" : "");
return true; return true;
} }
@@ -966,9 +1203,9 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/ */
public function quote($str) { public function quote($str) {
if($this->stripMB4 && is_string($str) && !empty($str)) { if($this->stripMB4 && is_string($str) && !empty($str)) {
$str = $this->wire('sanitizer')->removeMB4($str); $str = $this->wire()->sanitizer->removeMB4($str);
} }
return $this->pdo()->quote($str); return $this->pdoLast()->quote($str);
} }
/** /**
@@ -1004,6 +1241,8 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/ */
public function __get($key) { public function __get($key) {
if($key === 'pdo') return $this->pdo(); if($key === 'pdo') return $this->pdo();
if($key === 'pdoReader') return $this->pdoReader();
if($key === 'pdoWriter') return $this->pdoWriter();
if($key === 'debugMode') return $this->debugMode; if($key === 'debugMode') return $this->debugMode;
return parent::__get($key); return parent::__get($key);
} }
@@ -1015,7 +1254,10 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* *
*/ */
public function closeConnection() { public function closeConnection() {
$this->pdo = null; $this->reader['pdo'] = null;
$this->writer['pdo'] = null;
$this->reader['init'] = false;
$this->writer['init'] = false;
} }
/** /**
@@ -1044,6 +1286,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
/** @noinspection PhpUnusedLocalVariableInspection */ /** @noinspection PhpUnusedLocalVariableInspection */
list($varName, $value) = $query->fetch(\PDO::FETCH_NUM); list($varName, $value) = $query->fetch(\PDO::FETCH_NUM);
$this->variableCache[$name] = $value; $this->variableCache[$name] = $value;
$query->closeCursor();
return $value; return $value;
} }
@@ -1119,17 +1362,17 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/ */
public function backups() { public function backups() {
$path = $this->wire('config')->paths->assets . 'backups/database/'; $path = $this->wire()->config->paths->assets . 'backups/database/';
if(!is_dir($path)) { if(!is_dir($path)) {
$this->wire('files')->mkdir($path, true); $this->wire()->files->mkdir($path, true);
if(!is_dir($path)) throw new WireException("Unable to create path for backups: $path"); if(!is_dir($path)) throw new WireException("Unable to create path for backups: $path");
} }
$backups = new WireDatabaseBackup($path); $backups = new WireDatabaseBackup($path);
$backups->setWire($this->wire()); $backups->setWire($this->wire());
$backups->setDatabase($this); $backups->setDatabase($this);
$backups->setDatabaseConfig($this->wire('config')); $backups->setDatabaseConfig($this->wire()->config);
$backups->setBackupOptions(array('user' => $this->wire('user')->name)); $backups->setBackupOptions(array('user' => $this->wire()->user->name));
return $backups; return $backups;
} }
@@ -1152,6 +1395,22 @@ class WireDatabasePDO extends Wire implements WireDatabase {
return $max; return $max;
} }
/**
* Enable or disable PDO reader instance, or omit argument to get current state
*
* Returns true if reader is configured and allowed
* Returns false if reader is not configured or not allowed
*
* @param bool $allow
* @return bool
* @since 3.0.175
*
*/
protected function allowReader($allow = null) {
if($allow !== null) $this->reader['allow'] = (bool) $allow;
return $this->reader['has'] && $this->reader['allow'];
}
/** /**
* Get SQL mode, set SQL mode, add to existing SQL mode, or remove from existing SQL mode * Get SQL mode, set SQL mode, add to existing SQL mode, or remove from existing SQL mode
* *
@@ -1172,15 +1431,22 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* @param string $mode Mode string or CSV string with SQL mode(s), i.e. "STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY". * @param string $mode Mode string or CSV string with SQL mode(s), i.e. "STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY".
* This argument should be omitted when using the "get" action. * This argument should be omitted when using the "get" action.
* @param string $minVersion Make the given action only apply if MySQL version is at least $minVersion, i.e. "5.7.0". * @param string $minVersion Make the given action only apply if MySQL version is at least $minVersion, i.e. "5.7.0".
* @param \PDO PDO connection to use or omit for current (default=null) 3.0.175+
* @return string|bool Returns string in "get" action, boolean false if required version not present, or true otherwise. * @return string|bool Returns string in "get" action, boolean false if required version not present, or true otherwise.
* @throws WireException If given an invalid $action * @throws WireException If given an invalid $action
* *
*/ */
public function sqlMode($action = 'get', $mode = '', $minVersion = '') { public function sqlMode($action = 'get', $mode = '', $minVersion = '', $pdo = null) {
$result = true; $result = true;
$modes = array(); $modes = array();
if($pdo === null) {
$pdo = $this->pdoLast();
} else {
$this->pdoLast = $pdo;
}
if(empty($action)) $action = 'get'; if(empty($action)) $action = 'get';
if($action !== 'get' && $minVersion) { if($action !== 'get' && $minVersion) {
@@ -1190,29 +1456,29 @@ class WireDatabasePDO extends Wire implements WireDatabase {
if($mode) { if($mode) {
foreach(explode(',', $mode) as $m) { foreach(explode(',', $mode) as $m) {
$modes[] = $this->escapeStr(strtoupper($this->wire('sanitizer')->fieldName($m))); $modes[] = $this->escapeStr(strtoupper($this->wire()->sanitizer->fieldName($m)));
} }
} }
switch($action) { switch($action) {
case 'get': case 'get':
$query = $this->pdo()->query("SELECT @@sql_mode"); $query = $pdo->query("SELECT @@sql_mode");
$result = $query->fetchColumn(); $result = $query->fetchColumn();
$query->closeCursor(); $query->closeCursor();
break; break;
case 'set': case 'set':
$modes = implode(',', $modes); $modes = implode(',', $modes);
$result = $modes; $result = $modes;
$this->pdo()->exec("SET sql_mode='$modes'"); $pdo->exec("SET sql_mode='$modes'");
break; break;
case 'add': case 'add':
foreach($modes as $m) { foreach($modes as $m) {
$this->pdo()->exec("SET sql_mode=(SELECT CONCAT(@@sql_mode,',$m'))"); $pdo->exec("SET sql_mode=(SELECT CONCAT(@@sql_mode,',$m'))");
} }
break; break;
case 'remove': case 'remove':
foreach($modes as $m) { foreach($modes as $m) {
$this->pdo()->exec("SET sql_mode=(SELECT REPLACE(@@sql_mode,'$m',''))"); $pdo->exec("SET sql_mode=(SELECT REPLACE(@@sql_mode,'$m',''))");
} }
break; break;
default: default:
@@ -1221,5 +1487,6 @@ class WireDatabasePDO extends Wire implements WireDatabase {
return $result; return $result;
} }
} }