silverstripe-framework/model/connect/MySQLiConnector.php
Damian Mooyman d8e9af8af8 API New Database abstraction layer. Ticket #7429
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector
Creation of new DBQueryGenerator for database specific generation of SQL
Support for parameterised queries, move of code base to use these over escaped conditions
Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT
Support for PDO
Installation process upgraded to use new ORM
SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data.
Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn)
3.2 upgrade docs
Performance Optimisation and simplification of code to use more concise API
API Ability for database adapters to register extensions to ConfigureFromEnv.php
2014-07-09 18:04:05 +12:00

318 lines
8.2 KiB
PHP

<?php
/**
* Connector for MySQL using the MySQLi method
* @package framework
* @subpackage model
*/
class MySQLiConnector extends DBConnector {
/**
* Connection to the MySQL database
*
* @var MySQLi
*/
protected $dbConn = null;
/**
* Name of the currently selected database
*
* @var string
*/
protected $databaseName = null;
/**
* The most recent statement returned from MySQLiConnector->preparedQuery
*
* @var mysqli_stmt
*/
protected $lastStatement = null;
/**
* Store the most recent statement for later use
*
* @param mysqli_stmt $statement
*/
public function setLastStatement($statement) {
$this->lastStatement = $statement;
}
/**
* 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
* @param boolean &$success
* @return mysqli_stmt
*/
public function getOrPrepareStatement($sql, &$success) {
// Check for cached statement
if(!empty($this->cachedStatements[$sql])) {
$success = true;
return $this->cachedStatements[$sql];
}
// Prepare statement with arguments
$statement = $this->dbConn->stmt_init();
if($success = $statement->prepare($sql)) {
// Only cache prepared statement on success
$this->cachedStatements[$sql] = $statement;
}
return $statement;
}
public function connect($parameters, $selectDB = false) {
$this->flushStatements();
// Normally $selectDB is set to false by the MySQLDatabase controller, as per convention
$selectedDB = ($selectDB && !empty($parameters['database'])) ? $parameters['database'] : null;
if(!empty($parameters['port'])) {
$this->dbConn = new MySQLi(
$parameters['server'],
$parameters['username'],
$parameters['password'],
$selectedDB,
$parameters['port']
);
} else {
$this->dbConn = new MySQLi(
$parameters['server'],
$parameters['username'],
$parameters['password'],
$selectedDB
);
}
if ($this->dbConn->connect_error) {
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
}
// Set charset if given and not null. Can explicitly set to empty string to omit
$charset = isset($parameters['charset'])
? $parameters['charset']
: 'utf8';
if (!empty($charset)) $this->dbConn->set_charset($charset);
}
public function __destruct() {
if ($this->dbConn) {
mysqli_close($this->dbConn);
$this->dbConn = null;
}
}
public function escapeString($value) {
return $this->dbConn->real_escape_string($value);
}
public function quoteString($value) {
$value = $this->escapeString($value);
return "'$value'";
}
public function getVersion() {
return $this->dbConn->server_info;
}
protected function benchmarkQuery($sql, $callback) {
// Clear the last statement
$this->setLastStatement(null);
return parent::benchmarkQuery($sql, $callback);
}
public function query($sql, $errorLevel = E_USER_ERROR) {
// Check if we should only preview this query
if ($this->previewWrite($sql)) return;
// Benchmark query
$conn = $this->dbConn;
$handle = $this->benchmarkQuery($sql, function($sql) use($conn) {
return $conn->query($sql);
});
if (!$handle || $this->dbConn->error) {
$this->databaseError($this->getLastError(), $errorLevel, $sql);
return null;
}
if($handle !== true) {
// Some non-select queries return true on success
return new MySQLQuery($this, $handle);
}
}
/**
* Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
*
* @param array $parameters List of parameters
* @param array &$blobs Out parameter for list of blobs to bind separately
* @return array List of parameters appropriate for mysqli_stmt_bind_param function
*/
public function parsePreparedParameters($parameters, &$blobs) {
$types = '';
$values = array();
$blobs = array();
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'];
}
// Convert php variable type to one that makes mysqli_stmt_bind_param happy
// @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
switch($phpType) {
case 'boolean':
case 'integer':
$types .= 'i';
break;
case 'float': // Not actually returnable from gettype
case 'double':
$types .= 'd';
break;
case 'object': // Allowed if the object or resource has a __toString method
case 'resource':
case 'string':
case 'NULL': // Take care that a where clause should use "where XX is null" not "where XX = null"
$types .= 's';
break;
case 'blob':
$types .= 'b';
// Blobs must be sent via send_long_data and set to null here
$blobs[] = array(
'index' => $index,
'value' => $value
);
$value = null;
break;
case 'array':
case 'unknown type':
default:
user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)",
E_USER_ERROR);
break;
}
$values[] = $value;
}
return array_merge(array($types), $values);
}
/**
* Binds a list of parameters to a statement
*
* @param mysqli_stmt $statement MySQLi statement
* @param array $parameters List of parameters to pass to bind_param
*/
public function bindParameters(mysqli_stmt $statement, array $parameters) {
// Because mysqli_stmt::bind_param arguments must be passed by reference
// we need to do a bit of hackery
for ($i = 0; $i < count($parameters); $i++)
{
$boundName = "param$i";
$$boundName = $parameters[$i];
$boundNames[] = &$$boundName;
}
call_user_func_array( array($statement, 'bind_param'), $boundNames);
}
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
// Shortcut to basic query when not given parameters
if(empty($parameters)) return $this->query($sql, $errorLevel);
// Check if we should only preview this query
if ($this->previewWrite($sql)) return;
// Type check, identify, and prepare parameters for passing to the statement bind function
$parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
// Benchmark query
$self = $this;
$lastStatement = $this->benchmarkQuery($sql, function($sql) use($parsedParameters, $blobs, $self) {
$statement = $self->getOrPrepareStatement($sql, $success);
if(!$success) return $statement;
$self->bindParameters($statement, $parsedParameters);
// Bind any blobs given
foreach($blobs as $blob) {
$statement->send_long_data($blob['index'], $blob['value']);
}
// Safely execute the statement
$statement->execute();
return $statement;
});
// check result
$this->setLastStatement($lastStatement);
if (!$lastStatement || $lastStatement->error) {
$values = $this->parameterValues($parameters);
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
return null;
}
// May not return result for non-select statements
if($result = $lastStatement->get_result()) {
return new MySQLQuery($this, $result, $lastStatement);
}
}
public function selectDatabase($name) {
if ($this->dbConn->select_db($name)) {
$this->databaseName = $name;
return true;
} else {
return false;
}
}
public function getSelectedDatabase() {
return $this->databaseName;
}
public function unloadDatabase() {
$this->databaseName = null;
}
public function isActive() {
return $this->databaseName && $this->dbConn && empty($this->dbConn->connect_error);
}
public function affectedRows() {
return $this->dbConn->affected_rows;
}
public function getGeneratedID($table) {
return $this->dbConn->insert_id;
}
public function getLastError() {
// Check if a statement was used for the most recent query
if($this->lastStatement && $this->lastStatement->error) {
return $this->lastStatement->error;
}
return $this->dbConn->error;
}
}