silverstripe-framework/model/connect/PDOConnector.php
Sam Minnee 3ee8f505b7 MINORE: Remove training whitespace.
The main benefit of this is so that authors who make use of
.editorconfig don't end up with whitespace changes in their PRs.

Spaces vs. tabs has been left alone, although that could do with a
tidy-up in SS4 after the switch to PSR-1/2.

The command used was this:

for match in '*.ss' '*.css' '*.scss' '*.html' '*.yml' '*.php' '*.js' '*.csv' '*.inc' '*.php5'; do
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" -exec sed -E -i '' 's/[[:space:]]+$//' {} \+
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" | xargs perl -pi -e 's/ +$//'
done
2016-01-07 10:15:54 +13:00

403 lines
9.8 KiB
PHP

<?php
/**
* PDO driver database connector
* @package framework
* @subpackage model
*/
class PDOConnector extends DBConnector {
/**
* Should ATTR_EMULATE_PREPARES flag be used to emulate prepared statements?
*
* @config
* @var boolean
*/
private static $emulate_prepare = false;
/**
* The PDO connection instance
*
* @var PDO
*/
protected $pdoConnection = null;
/**
* Name of the currently selected database
*
* @var string
*/
protected $databaseName = null;
/**
* If available, the row count of the last executed statement
*
* @var int|null
*/
protected $rowCount = null;
/**
* Error generated by the errorInfo() method of the last PDOStatement
*
* @var array|null
*/
protected $lastStatementError = null;
/**
* List of prepared statements, cached by SQL string
*
* @var array
*/
protected $cachedStatements = array();
/**
* Flush all prepared statements
*/
public function flushStatements() {
$this->cachedStatements = array();
}
/**
* Retrieve a prepared statement for a given SQL string, or return an already prepared version if
* one exists for the given query
*
* @param string $sql
* @return PDOStatement
*/
public function getOrPrepareStatement($sql) {
// Return cached statements
if(isset($this->cachedStatements[$sql])) {
return $this->cachedStatements[$sql];
}
// Generate new statement
$statement = $this->pdoConnection->prepare(
$sql,
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
);
// Only cache select statements
if(preg_match('/^(\s*)select\b/i', $sql)) {
$this->cachedStatements[$sql] = $statement;
}
return $statement;
}
/**
* Is PDO running in emulated mode
*
* @return boolean
*/
public static function is_emulate_prepare() {
return Config::inst()->get('PDOConnector', 'emulate_prepare');
}
public function connect($parameters, $selectDB = false) {
$this->flushStatements();
// Build DSN string
// Note that we don't select the database here until explicitly
// requested via selectDatabase
$driver = $parameters['driver'] . ":";
$dsn = array();
// Typically this is false, but some drivers will request this
if($selectDB) {
// Specify complete file path immediately following driver (SQLLite3)
if(!empty($parameters['filepath'])) {
$dsn[] = $parameters['filepath'];
} elseif(!empty($parameters['database'])) {
// Some databases require a selected database at connection (SQLite3, Azure)
if($parameters['driver'] === 'sqlsrv') {
$dsn[] = "Database={$parameters['database']}";
} else {
$dsn[] = "dbname={$parameters['database']}";
}
}
}
// Syntax for sql server is slightly different
if($parameters['driver'] === 'sqlsrv') {
$server = $parameters['server'];
if (!empty($parameters['port'])) {
$server .= ",{$parameters['port']}";
}
$dsn[] = "Server=$server";
} else {
if (!empty($parameters['server'])) {
// Use Server instead of host for sqlsrv
$dsn[] = "host={$parameters['server']}";
}
if (!empty($parameters['port'])) {
$dsn[] = "port={$parameters['port']}";
}
}
// Set charset if given and not null. Can explicitly set to empty string to omit
if($parameters['driver'] !== 'sqlsrv') {
$charset = isset($parameters['charset'])
? $parameters['charset']
: 'utf8';
if (!empty($charset)) $dsn[] = "charset=$charset";
}
// Connection commands to be run on every re-connection
$options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
);
if(self::is_emulate_prepare()) {
$options[PDO::ATTR_EMULATE_PREPARES] = true;
}
// May throw a PDOException if fails
$this->pdoConnection = new PDO(
$driver.implode(';', $dsn),
empty($parameters['username']) ? '' : $parameters['username'],
empty($parameters['password']) ? '' : $parameters['password'],
$options
);
// Show selected DB if requested
if($this->pdoConnection && $selectDB && !empty($parameters['database'])) {
$this->databaseName = $parameters['database'];
}
}
public function getVersion() {
return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
public function escapeString($value) {
$value = $this->quoteString($value);
// Since the PDO library quotes the value, we should remove this to maintain
// consistency with MySQLDatabase::escapeString
if (preg_match('/^\'(?<value>.*)\'$/', $value, $matches)) {
$value = $matches['value'];
}
return $value;
}
public function quoteString($value) {
return $this->pdoConnection->quote($value);
}
/**
* Invoked before any query is executed
*
* @param string $sql
*/
protected function beforeQuery($sql) {
// Reset state
$this->rowCount = 0;
$this->lastStatementError = null;
// Flush if necessary
if($this->isQueryDDL($sql)) {
$this->flushStatements();
}
}
/**
* Executes a query that doesn't return a resultset
*
* @param string $sql The SQL query to execute
* @param integer $errorLevel For errors to this query, raise PHP errors
* using this error level.
* @return int
*/
public function exec($sql, $errorLevel = E_USER_ERROR) {
$this->beforeQuery($sql);
// Directly exec this query
$result = $this->pdoConnection->exec($sql);
// Check for errors
if ($result !== false) {
return $this->rowCount = $result;
}
$this->databaseError($this->getLastError(), $errorLevel, $sql);
return null;
}
public function query($sql, $errorLevel = E_USER_ERROR) {
$this->beforeQuery($sql);
// Directly query against connection
$statement = $this->pdoConnection->query($sql);
// Generate results
return $this->prepareResults($statement, $errorLevel, $sql);
}
/**
* Determines the PDO::PARAM_* type for a given PHP type string
* @param string $phpType Type of object in PHP
* @return integer PDO Parameter constant value
*/
public function getPDOParamType($phpType) {
switch($phpType) {
case 'boolean':
return PDO::PARAM_BOOL;
case 'NULL':
return PDO::PARAM_NULL;
case 'integer':
return PDO::PARAM_INT;
case 'object': // Allowed if the object or resource has a __toString method
case 'resource':
case 'float': // Not actually returnable from get_type
case 'double':
case 'string':
return PDO::PARAM_STR;
case 'blob':
return PDO::PARAM_LOB;
case 'array':
case 'unknown type':
default:
user_error("Cannot bind parameter as it is an unsupported type ($phpType)", E_USER_ERROR);
}
}
/**
* Bind all parameters to a PDOStatement
*
* @param PDOStatement $statement
* @param array $parameters
*/
public function bindParameters(PDOStatement $statement, $parameters) {
// Bind all parameters
for($index = 0; $index < count($parameters); $index++) {
$value = $parameters[$index];
$phpType = gettype($value);
// Allow overriding of parameter type using an associative array
if($phpType === 'array') {
$phpType = $value['type'];
$value = $value['value'];
}
// Check type of parameter
$type = $this->getPDOParamType($phpType);
if($type === PDO::PARAM_STR) $value = strval($value);
// Bind this value
$statement->bindValue($index+1, $value, $type);
}
}
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
$this->beforeQuery($sql);
// Prepare statement
$statement = $this->getOrPrepareStatement($sql);
// Bind and invoke statement safely
if($statement) {
$this->bindParameters($statement, $parameters);
$statement->execute($parameters);
}
// Generate results
return $this->prepareResults($statement, $errorLevel, $sql);
}
/**
* Given a PDOStatement that has just been executed, generate results
* and report any errors
*
* @param PDOStatement $statement
* @param int $errorLevel
* @param string $sql
* @param array $parameters
* @return \PDOQuery
*/
protected function prepareResults($statement, $errorLevel, $sql, $parameters = array()) {
// Record row-count and errors of last statement
if($this->hasError($statement)) {
$this->lastStatementError = $statement->errorInfo();
} elseif($statement) {
// Count and return results
$this->rowCount = $statement->rowCount();
return new PDOQuery($statement);
}
// Ensure statement is closed
if($statement) {
$statement->closeCursor();
unset($statement);
}
// Report any errors
if($parameters) {
$parameters = $this->parameterValues($parameters);
}
$this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters);
return null;
}
/**
* Determine if a resource has an attached error
*
* @param PDOStatement|PDO $resource the resource to check
* @return boolean Flag indicating true if the resource has an error
*/
protected function hasError($resource) {
// No error if no resource
if(empty($resource)) {
return false;
}
// If the error code is empty the statement / connection has not been run yet
$code = $resource->errorCode();
if(empty($code)) {
return false;
}
// Skip 'ok' and undefined 'warning' types.
// @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
return $code !== '00000' && $code !== '01000';
}
public function getLastError() {
$error = null;
if ($this->lastStatementError) {
$error = $this->lastStatementError;
} elseif($this->hasError($this->pdoConnection)) {
$error = $this->pdoConnection->errorInfo();
}
if ($error) {
return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
}
}
public function getGeneratedID($table) {
return $this->pdoConnection->lastInsertId();
}
public function affectedRows() {
return $this->rowCount;
}
public function selectDatabase($name) {
$this->exec("USE \"{$name}\"");
$this->databaseName = $name;
return true;
}
public function getSelectedDatabase() {
return $this->databaseName;
}
public function unloadDatabase() {
$this->databaseName = null;
}
public function isActive() {
return $this->databaseName && $this->pdoConnection;
}
}