mirror of
https://github.com/processwire/processwire.git
synced 2025-08-13 02:04:35 +02:00
Some updates to base DatabaseQuery class to improve bound value support among other things
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
* of what other methods/objects have done to it. It also means being able
|
||||
* to build a complex query without worrying about correct syntax placement.
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* This file is licensed under the MIT license
|
||||
@@ -18,7 +18,7 @@
|
||||
*
|
||||
* @property array $where
|
||||
* @property array $bindValues
|
||||
* @property array $bindIndex
|
||||
* @property array $bindIndex
|
||||
*
|
||||
*/
|
||||
abstract class DatabaseQuery extends WireData {
|
||||
@@ -32,44 +32,180 @@ abstract class DatabaseQuery extends WireData {
|
||||
protected $bindValues = array();
|
||||
|
||||
/**
|
||||
* Index of bound values per originating method
|
||||
* Bound parameter types of name => \PDO::PARAM_* type constant
|
||||
*
|
||||
* Populated only when a type is provided to the bindValue() call
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
*/
|
||||
protected $bindTypes = array();
|
||||
|
||||
/**
|
||||
* Index of bound values per originating method (deprecated)
|
||||
*
|
||||
* Indexed by originating method, with values as the bound parameter names as in $bindValues.
|
||||
* This is populated by the setupBindValues() method
|
||||
* This is populated by the setupBindValues() method. The purpose of this is for one part of
|
||||
* one query is imported to another, the appropriate bound values are also imported. This is
|
||||
* deprecated because it does not work if values are bound independently of `$q->where()`, etc.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
*/
|
||||
protected $bindIndex = array();
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
protected $instanceNum = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
static $uniq = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
*/
|
||||
static $numInstances = 0;
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
self::$numInstances++;
|
||||
$this->instanceNum = self::$numInstances;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a parameter value
|
||||
*
|
||||
* @param string $key Parameter name
|
||||
* @param mixed $value Parameter value
|
||||
* @param null|int|string Optionally specify value type: string, int, bool, null or PDO::PARAM_* constant.
|
||||
* @return $this
|
||||
*
|
||||
*/
|
||||
public function bindValue($key, $value) {
|
||||
public function bindValue($key, $value, $type = null) {
|
||||
if(strpos($key, ':') !== 0) $key = ":$key";
|
||||
$this->bindValues[$key] = $value;
|
||||
if($type !== null) $this->setBindType($key, $type);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind multiple parameter values
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param array $bindValues
|
||||
* @return $this
|
||||
* @since 3.0.156
|
||||
*
|
||||
*/
|
||||
public function bindValues(array $bindValues) {
|
||||
foreach($bindValues as $key => $value) {
|
||||
$this->bindValue($key, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bound parameter values, optionally for a specific method call
|
||||
* Set bind type
|
||||
*
|
||||
* @param string $method
|
||||
* @return array
|
||||
* @param string $key
|
||||
* @param int|string $type
|
||||
*
|
||||
*/
|
||||
public function getBindValues($method = '') {
|
||||
if(empty($method)) return $this->bindValues;
|
||||
if(!isset($this->bindIndex[$method])) return array();
|
||||
$names = $this->bindIndex[$method];
|
||||
$values = array();
|
||||
foreach($names as $name) {
|
||||
$values[$name] = $this->bindValues[$name];
|
||||
protected function setBindType($key, $type) {
|
||||
|
||||
if(is_int($type) || ctype_digit("$type")) {
|
||||
$this->bindTypes[$key] = (int) $type;
|
||||
}
|
||||
|
||||
switch(strtolower(substr($type, 0, 3))) {
|
||||
case 'str': $type = \PDO::PARAM_STR; break;
|
||||
case 'int': $type = \PDO::PARAM_INT; break;
|
||||
case 'boo': $type = \PDO::PARAM_BOOL; break;
|
||||
case 'nul': $type = \PDO::PARAM_NULL; break;
|
||||
default: $type = null;
|
||||
}
|
||||
|
||||
if($type !== null) $this->bindTypes[$key] = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique key to use for bind value
|
||||
*
|
||||
* @param string $key Preferred bind key or prefix, or omit to auto-generate
|
||||
* @return string Returns bind key/name in format ":name" (with leading colon)
|
||||
* @since 3.0.156
|
||||
*
|
||||
*/
|
||||
public function getUniqueBindKey($key = '') {
|
||||
$key = ltrim($key, ':');
|
||||
if(!ctype_alnum(str_replace('_', '', $key))) {
|
||||
$key = $this->wire('database')->escapeCol($key);
|
||||
}
|
||||
if(!strlen($key) || ctype_digit($key[0])) $key = "pwbk$key";
|
||||
do {
|
||||
$name = ':' . $key . '_' . $this->instanceNum . '_' . (++self::$uniq);
|
||||
} while(isset($this->bindValues[$name]));
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bound parameter values (or populate to given query)
|
||||
*
|
||||
* - If given a string for $options argument it assumed to be the `method` option.
|
||||
* - If given a \PDOStatement or DatabaseQuery, it is assumed to be the `query` option.
|
||||
*
|
||||
* Note: The $options argument was added in 3.0.156, prior to this it was a $method argument,
|
||||
* which is the same as the `method` option (string).
|
||||
*
|
||||
* @param string|\PDOStatement|DatabaseQuery|array $options Optionally specify an option:
|
||||
* - `method` (string): Get bind values just for this DatabaseQuery method name (default='') deprecated
|
||||
* - `query` (\PDOStatement|DatabaseQuery): Populate bind values to this query object (default=null)
|
||||
* @return array Associative array in format [ ":column" => "value" ] where each "value" is int, string or NULL.
|
||||
*
|
||||
*/
|
||||
public function getBindValues($options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'method' => is_string($options) ? $options : '',
|
||||
'query' => is_object($options) ? $options : null,
|
||||
);
|
||||
|
||||
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
|
||||
$query = $options['query'];
|
||||
$method = $options['method'];
|
||||
|
||||
if($method) {
|
||||
if(!isset($this->bindIndex[$method])) return array();
|
||||
$names = isset($this->bindIndex[$method]) ? $this->bindIndex[$method] : array();
|
||||
$values = array();
|
||||
foreach($names as $name) {
|
||||
$name = ':' . ltrim($name, ':');
|
||||
$values[$name] = $this->bindValues[$name];
|
||||
}
|
||||
} else {
|
||||
$values = $this->bindValues;
|
||||
}
|
||||
|
||||
if($query && $query instanceof \PDOStatement) {
|
||||
foreach($values as $k => $v) {
|
||||
$type = $this->pdoParamType($v);
|
||||
$query->bindValue($k, $v, $type);
|
||||
}
|
||||
} else if($query && $query instanceof DatabaseQuery && $query !== $this) {
|
||||
$query->bindValues($values);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
@@ -82,7 +218,7 @@ abstract class DatabaseQuery extends WireData {
|
||||
*
|
||||
* To bind parameters, specify associative array as second argument:
|
||||
*
|
||||
* $query->where("name=:name", array(':name' => $page->name));
|
||||
* $query->where("name=:name", [ ':name' => $page->name ]);
|
||||
*
|
||||
* To import query/method and bound values from another DatabaseQuery:
|
||||
*
|
||||
@@ -96,12 +232,18 @@ abstract class DatabaseQuery extends WireData {
|
||||
*
|
||||
*/
|
||||
public function __call($method, $args) {
|
||||
|
||||
if(!$this->has($method)) return parent::__call($method, $args);
|
||||
if(empty($args[0])) return $this;
|
||||
|
||||
$curValue = $this->get($method);
|
||||
$value = $args[0];
|
||||
|
||||
if(!is_array($curValue)) $curValue = array();
|
||||
if(empty($value)) return $this;
|
||||
|
||||
if(is_object($value) && $value instanceof DatabaseQuery) {
|
||||
// if we've been given another DatabaseQuery, load from it's $method
|
||||
// if we've been given another DatabaseQuery, load from its $method
|
||||
$query = $value;
|
||||
$value = $query->$method;
|
||||
if(!is_array($value) || !count($value)) return $this; // nothing to import
|
||||
@@ -109,67 +251,92 @@ abstract class DatabaseQuery extends WireData {
|
||||
} else {
|
||||
$params = isset($args[1]) && is_array($args[1]) ? $args[1] : null;
|
||||
}
|
||||
|
||||
if(!empty($params)) {
|
||||
$value = $this->setupBindValues($value, $params, $method);
|
||||
$this->methodBindValues($value, $params, $method);
|
||||
}
|
||||
|
||||
if(is_array($value)) {
|
||||
$curValue = array_merge($curValue, $value);
|
||||
} else {
|
||||
$curValue[] = trim($value, ", ");
|
||||
}
|
||||
|
||||
$this->set($method, $curValue);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup bound parameters for the given query, returning an updated $value if any renames needed to be made
|
||||
* Setup bound parameters for the given SQL provided to method call
|
||||
*
|
||||
* This is only used when params are provided as part of a method call like:
|
||||
* $query->where("foo=:bar", [ ":bar" => "baz" ]);
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string|array $sql
|
||||
* @param array $params
|
||||
* @param array $values Associative array of bound values
|
||||
* @param string $method Method name that the bound values are for
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function setupBindValues($sql, array $params, $method) {
|
||||
foreach($params as $name => $value) {
|
||||
if(strpos($name, ':') !== 0) $name = ":$name";
|
||||
$newName = $name;
|
||||
$n = 0;
|
||||
while(isset($this->bindValues[$newName])) {
|
||||
$newName = $name . (++$n);
|
||||
}
|
||||
if($n) {
|
||||
protected function methodBindValues(&$sql, array $values, $method) {
|
||||
|
||||
foreach($values as $name => $value) {
|
||||
$name = ':' . ltrim($name, ':');
|
||||
|
||||
if(isset($this->bindValues[$name])) {
|
||||
// bind key already in use, use a different unique one instead
|
||||
$newName = $this->getUniqueBindKey($name);
|
||||
if(is_array($sql)) {
|
||||
foreach($sql as $k => $v) {
|
||||
if(strpos($v, $name) === false) continue;
|
||||
$sql[$k] = preg_replace('/' . $name . '\b/', $newName, $v);
|
||||
}
|
||||
} else {
|
||||
} else if(strpos($sql, $name) !== false) {
|
||||
$sql = preg_replace('/' . $name . '\b/', $newName, $sql);
|
||||
}
|
||||
$name = $newName;
|
||||
}
|
||||
|
||||
$this->bindValue($name, $value);
|
||||
if(!isset($this->bindIndex[$method])) $this->bindIndex[$method] = array();
|
||||
$this->bindIndex[$method][] = $name;
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*
|
||||
*/
|
||||
public function __set($key, $value) {
|
||||
if(is_array($this->$key)) $this->__call($key, array($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array|mixed|null
|
||||
*
|
||||
*/
|
||||
public function __get($key) {
|
||||
if($key == 'query') return $this->getQuery();
|
||||
else if($key == 'bindValues') return $this->bindValues;
|
||||
else if($key == 'bindIndex') return $this->bindIndex;
|
||||
else return parent::__get($key);
|
||||
|
||||
if($key === 'query') {
|
||||
return $this->getQuery();
|
||||
} else if($key === 'bindValues') {
|
||||
return $this->bindValues;
|
||||
} else if($key === 'bindIndex') {
|
||||
return $this->bindIndex;
|
||||
}
|
||||
|
||||
return parent::__get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the contents of current query with another (experimental/incomplete)
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @internal
|
||||
* @param DatabaseQuery $query
|
||||
* @return $this
|
||||
@@ -209,16 +376,35 @@ abstract class DatabaseQuery extends WireData {
|
||||
public function prepare() {
|
||||
$query = $this->wire('database')->prepare($this->getQuery());
|
||||
foreach($this->bindValues as $key => $value) {
|
||||
$query->bindValue($key, $value);
|
||||
$type = isset($this->bindTypes[$key]) ? $this->bindTypes[$key] : $this->pdoParamType($value);
|
||||
$query->bindValue($key, $value, $type);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDO::PARAM_* type for given value
|
||||
*
|
||||
* @param string|int|null $value
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
protected function pdoParamType($value) {
|
||||
if(is_int($value)) {
|
||||
$type = \PDO::PARAM_INT;
|
||||
} else if($value === null) {
|
||||
$type = \PDO::PARAM_NULL;
|
||||
} else {
|
||||
$type = \PDO::PARAM_STR;
|
||||
}
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query with the current database handle
|
||||
*
|
||||
* @return \PDOStatement
|
||||
* @throws WireException|\Exception|\PDOException
|
||||
* @throws WireDatabaseQueryException|\PDOException
|
||||
*
|
||||
*/
|
||||
public function execute() {
|
||||
@@ -230,7 +416,7 @@ abstract class DatabaseQuery extends WireData {
|
||||
$msg = $e->getMessage();
|
||||
if(stripos($msg, 'MySQL server has gone away') !== false) $database->closeConnection();
|
||||
if($this->wire('config')->allowExceptions) throw $e; // throw original
|
||||
throw new WireException($msg); // throw WireException
|
||||
throw new WireDatabaseQueryException($msg, $e->getCode(), $e);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
@@ -51,6 +51,7 @@ class DatabaseQuerySelect extends DatabaseQuery {
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->set('select', array());
|
||||
$this->set('join', array());
|
||||
$this->set('from', array());
|
||||
|
@@ -121,6 +121,17 @@ class Wire404Exception extends WireException {
|
||||
*/
|
||||
class WireDatabaseException extends WireException {}
|
||||
|
||||
/**
|
||||
* Thrown by DatabaseQuery classes on query exception
|
||||
*
|
||||
* May have \PDOException populated with call to its getPrevious(); method,
|
||||
* in which can it also has same getCode() and getMessage() as \PDOException.
|
||||
*
|
||||
* @since 3.0.156
|
||||
*
|
||||
*/
|
||||
class WireDatabaseQueryException extends WireException {}
|
||||
|
||||
/**
|
||||
* Thrown when cross site request forgery detected by SessionCSRF::validate()
|
||||
*
|
||||
|
Reference in New Issue
Block a user