Commit 09c048cc by Scott

Extract methods to DbQueryHelper class

parent e5d49898
...@@ -152,7 +152,7 @@ function qa_db_escape_string($string) ...@@ -152,7 +152,7 @@ function qa_db_escape_string($string)
/** /**
* Return $argument escaped for MySQL. Add quotes around it if $alwaysquote is true or it's not numeric. * Return $argument escaped for MySQL. Add quotes around it if $alwaysquote is true or it's not numeric.
* If $argument is an array, return a comma-separated list of escaped elements, with or without $arraybrackets. * If $argument is an array, return a comma-separated list of escaped elements, with or without $arraybrackets.
* @deprecated 1.9.0 * @deprecated 1.9.0 Use DbQueryHelper->expandParameters() instead.
* @param mixed|null $argument * @param mixed|null $argument
* @param bool $alwaysquote * @param bool $alwaysquote
* @param bool $arraybrackets * @param bool $arraybrackets
...@@ -185,7 +185,7 @@ function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets = false ...@@ -185,7 +185,7 @@ function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets = false
/** /**
* Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol. * Return the full name (with prefix) of database table $rawname, usually if it used after a ^ symbol.
* @deprecated 1.9.0 Use DbConnection->addTablePrefix() instead. * @deprecated 1.9.0 Use DbQueryHelper->addTablePrefix() instead.
* @param string $rawname * @param string $rawname
* @return string * @return string
*/ */
...@@ -215,7 +215,7 @@ function qa_db_prefix_callback($matches) ...@@ -215,7 +215,7 @@ function qa_db_prefix_callback($matches)
* it is converted recursively into comma-separated list). Each element in $arguments is escaped. * it is converted recursively into comma-separated list). Each element in $arguments is escaped.
* $ is replaced by the argument in quotes (even if it's a number), # only adds quotes if the argument is non-numeric. * $ is replaced by the argument in quotes (even if it's a number), # only adds quotes if the argument is non-numeric.
* It's important to use $ when matching a textual column since MySQL won't use indexes to compare text against numbers. * It's important to use $ when matching a textual column since MySQL won't use indexes to compare text against numbers.
* @deprecated 1.9.0 Use DbConnection->applyTableSub() instead. * @deprecated 1.9.0 Use DbQueryHelper->expandParameters() instead.
* @param string $query * @param string $query
* @param array $arguments * @param array $arguments
* @return mixed * @return mixed
...@@ -294,7 +294,6 @@ function qa_db_num_rows($result) ...@@ -294,7 +294,6 @@ function qa_db_num_rows($result)
if ($result instanceof \Q2A\Database\DbResult) if ($result instanceof \Q2A\Database\DbResult)
return $result->affectedRows(); return $result->affectedRows();
// backwards compatibility // backwards compatibility
if ($result instanceof mysqli_result) if ($result instanceof mysqli_result)
return $result->num_rows; return $result->num_rows;
......
...@@ -22,7 +22,11 @@ use PDO; ...@@ -22,7 +22,11 @@ use PDO;
use PDOException; use PDOException;
use PDOStatement; use PDOStatement;
use Q2A\Database\Exceptions\SelectSpecException; use Q2A\Database\Exceptions\SelectSpecException;
use Q2A\Storage\CacheFactory;
/**
* Core database class. Handles all SQL queries within Q2A.
*/
class DbConnection class DbConnection
{ {
/** @var PDO */ /** @var PDO */
...@@ -74,6 +78,7 @@ class DbConnection ...@@ -74,6 +78,7 @@ class DbConnection
/** /**
* Indicates to the Q2A database layer that database connections are permitted from this point forwards (before * Indicates to the Q2A database layer that database connections are permitted from this point forwards (before
* this point, some plugins may not have had a chance to override some database access functions). * this point, some plugins may not have had a chance to override some database access functions).
* @return void
*/ */
public function allowConnect() public function allowConnect()
{ {
...@@ -90,8 +95,8 @@ class DbConnection ...@@ -90,8 +95,8 @@ class DbConnection
} }
/** /**
* Connect to the Q2A database, optionally install the $failHandler (and call it if necessary). Uses PDO as of Q2A * Connect to the Q2A database, optionally install the $failHandler (and call it if necessary).
* 1.9. * Uses PDO as of Q2A 1.9.
* @param string $failHandler * @param string $failHandler
* @return mixed|void * @return mixed|void
*/ */
...@@ -178,11 +183,10 @@ class DbConnection ...@@ -178,11 +183,10 @@ class DbConnection
$this->connect(); $this->connect();
} }
$query = $this->applyTableSub($query); $helper = new DbQueryHelper;
// handle old-style placeholders $query = $helper->applyTableSub($query);
$query = str_replace(['#', '$'], '?', $query); // handle WHERE..IN and INSERT..VALUES queries
// handle IN queries list($query, $params) = $helper->expandParameters($query, $params);
list($query, $params) = $this->expandQueryParameters($query, $params);
if (substr_count($query, '?') != count($params)) { if (substr_count($query, '?') != count($params)) {
throw new SelectSpecException('The number of parameters and placeholders do not match'); throw new SelectSpecException('The number of parameters and placeholders do not match');
...@@ -297,7 +301,7 @@ class DbConnection ...@@ -297,7 +301,7 @@ class DbConnection
{ {
// check for cached results // check for cached results
if (isset($selectspec['caching'])) { if (isset($selectspec['caching'])) {
$cacheDriver = \Q2A\Storage\CacheFactory::getCacheDriver(); $cacheDriver = CacheFactory::getCacheDriver();
$cacheKey = 'query:' . $selectspec['caching']['key']; $cacheKey = 'query:' . $selectspec['caching']['key'];
if ($cacheDriver->isEnabled()) { if ($cacheDriver->isEnabled()) {
...@@ -506,121 +510,6 @@ class DbConnection ...@@ -506,121 +510,6 @@ class DbConnection
} }
/** /**
* Substitute ^ in a SQL query with the configured table prefix.
* @param string $query
* @return string
*/
public function applyTableSub($query)
{
return preg_replace_callback('/\^([A-Za-z_0-9]+)/', function ($matches) {
return $this->addTablePrefix($matches[1]);
}, $query);
}
/**
* Return the full name (with prefix) of a database table identifier.
* @param string $rawName
* @return string
*/
public function addTablePrefix($rawName)
{
$prefix = QA_MYSQL_TABLE_PREFIX;
if (defined('QA_MYSQL_USERS_PREFIX')) {
switch (strtolower($rawName)) {
case 'users':
case 'userlogins':
case 'userprofile':
case 'userfields':
case 'messages':
case 'cookies':
case 'blobs':
case 'cache':
case 'userlogins_ibfk_1': // also special cases for constraint names
case 'userprofile_ibfk_1':
$prefix = QA_MYSQL_USERS_PREFIX;
break;
}
}
return $prefix . $rawName;
}
/**
* Substitute single '?' in a SQL query with multiple '?' for array parameters, and flatten parameter list accordingly.
* @param string $query
* @param array $params
* @return array Query and flattened parameter list
* @throws SelectSpecException
*/
public function expandQueryParameters($query, array $params = [])
{
$numParams = count($params);
$explodedQuery = explode('?', $query);
if ($numParams !== count($explodedQuery) - 1) {
throw new SelectSpecException('The number of parameters and placeholders do not match');
}
if (empty($params)) {
return [$query, $params];
}
$outQuery = '';
$outParams = [];
// use array_values to ensure consistent indexing
foreach (array_values($params) as $i => $param) {
$outQuery .= $explodedQuery[$i];
if (is_array($param)) {
$subArray = array_values($param);
if (is_array($subArray[0])) {
// INSERT..VALUES query for inserting multiple rows
$subArrayCount = count($subArray[0]);
foreach ($subArray as $subArrayParam) {
// If the first subparam is an array, the rest of the parameter groups should have the same
// amount of elements, i.e. the output should be '(?, ?), (?, ?)' rather than '(?), (?, ?)'
if (!is_array($subArrayParam) || count($subArrayParam) !== $subArrayCount) {
throw new SelectSpecException('All parameter groups must have the same amount of parameters');
}
$outParams = array_merge($outParams, $subArrayParam);
}
$outQuery .= $this->repeatStringWithSeparators(
'(' . $this->repeatStringWithSeparators('?', $subArrayCount) . ')',
count($subArray)
);
} else {
// WHERE..IN query
$outQuery .= $this->repeatStringWithSeparators('?', count($subArray));
$outParams = array_merge($outParams, $subArray);
}
} else {
// standard query
$outQuery .= '?';
$outParams[] = $param;
}
}
$outQuery .= $explodedQuery[$numParams];
return [$outQuery, $outParams];
}
/**
* Repeat a string a given amount of times separating each of the instances with ', '.
* @param string $string
* @param int $amount
* @return string
*/
private function repeatStringWithSeparators($string, $amount)
{
return $amount == 1
? $string
: str_repeat($string . ', ', $amount - 1) . $string;
}
/**
* Return the value of the auto-increment column for the last inserted row. * Return the value of the auto-increment column for the last inserted row.
* @return string * @return string
*/ */
...@@ -647,23 +536,4 @@ class DbConnection ...@@ -647,23 +536,4 @@ class DbConnection
{ {
return $this->updateCountsSuspended <= 0; return $this->updateCountsSuspended <= 0;
} }
/**
* Flatten a two-level or three-level array into a one-level array.
* @param mixed $elements Input elements which can be one-level deep arrays
* @return array
*/
private function flattenArray($elements)
{
$result = array();
foreach ($elements as $element) {
if (is_array($element)) {
$result = array_merge($result, $this->flattenArray($element));
} else {
$result[] = $element;
}
}
return $result;
}
} }
<?php
/*
Question2Answer by Gideon Greenspan and contributors
http://www.question2answer.org/
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
More about this license: http://www.question2answer.org/license.php
*/
namespace Q2A\Database;
use Q2A\Database\Exceptions\SelectSpecException;
/**
* Various database utility functions.
*/
class DbQueryHelper
{
/**
* Substitute ^ in a SQL query with the configured table prefix.
* @param string $query
* @return string
*/
public function applyTableSub($query)
{
return preg_replace_callback('/\^([A-Za-z_0-9]+)/', function ($matches) {
return $this->addTablePrefix($matches[1]);
}, $query);
}
/**
* Return the full name (with prefix) of a database table identifier.
* @param string $rawName
* @return string
*/
public function addTablePrefix($rawName)
{
$prefix = QA_MYSQL_TABLE_PREFIX;
if (defined('QA_MYSQL_USERS_PREFIX')) {
switch (strtolower($rawName)) {
case 'users':
case 'userlogins':
case 'userprofile':
case 'userfields':
case 'messages':
case 'cookies':
case 'blobs':
case 'cache':
case 'userlogins_ibfk_1': // also special cases for constraint names
case 'userprofile_ibfk_1':
$prefix = QA_MYSQL_USERS_PREFIX;
break;
}
}
return $prefix . $rawName;
}
/**
* Substitute single '?' in a SQL query with multiple '?' for array parameters, and flatten parameter list accordingly.
* @param string $query
* @param array $params
* @return array Query and flattened parameter list
* @throws SelectSpecException
*/
public function expandParameters($query, array $params = [])
{
// handle old-style placeholders
$query = str_replace(['#', '$'], '?', $query);
$numParams = count($params);
$explodedQuery = explode('?', $query);
if ($numParams !== count($explodedQuery) - 1) {
throw new SelectSpecException('The number of parameters and placeholders do not match');
}
if (empty($params)) {
return [$query, $params];
}
$outQuery = '';
$outParams = [];
// use array_values to ensure consistent indexing
foreach (array_values($params) as $i => $param) {
$outQuery .= $explodedQuery[$i];
if (is_array($param)) {
$subArray = array_values($param);
if (is_array($subArray[0])) {
$this->handleInsertValuesQuery($subArray, $outQuery, $outParams);
} else {
$this->handleWhereInQuery($subArray, $outQuery, $outParams);
}
} else {
$this->handleStandardQuery($param, $outQuery, $outParams);
}
}
$outQuery .= $explodedQuery[$numParams];
return [$outQuery, $outParams];
}
/**
* Basic query with individual parameters.
* @param array $param
* @param string $outQuery
* @param array $outParams
*/
private function handleStandardQuery($param, &$outQuery, array &$outParams)
{
$outQuery .= '?';
$outParams[] = $param;
}
/**
* WHERE..IN query.
* @param array $params
* @param string $outQuery
* @param array $outParams
*/
private function handleWhereInQuery(array $params, &$outQuery, array &$outParams)
{
$outQuery .= $this->repeatStringWithSeparators('?', count($params));
$outParams = array_merge($outParams, $params);
}
/**
* INSERT INTO..VALUES query for inserting multiple rows.
* If the first subparam is an array, the rest of the parameter groups should have the same
* amount of elements, i.e. the output should be '(?, ?), (?, ?)' rather than '(?), (?, ?)'.
* @param array $subArray
* @param string $outQuery
* @param array $outParams
*/
private function handleInsertValuesQuery(array $subArray, &$outQuery, array &$outParams)
{
$subArrayCount = count($subArray[0]);
foreach ($subArray as $subArrayParam) {
if (!is_array($subArrayParam) || count($subArrayParam) !== $subArrayCount) {
throw new SelectSpecException('All parameter groups must have the same amount of parameters');
}
$outParams = array_merge($outParams, $subArrayParam);
}
$outQuery .= $this->repeatStringWithSeparators(
'(' . $this->repeatStringWithSeparators('?', $subArrayCount) . ')',
count($subArray)
);
}
/**
* Repeat a string a given amount of times separating each of the instances with ', '.
* @param string $string
* @param int $amount
* @return string
*/
private function repeatStringWithSeparators($string, $amount)
{
return $amount == 1
? $string
: str_repeat($string . ', ', $amount - 1) . $string;
}
}
...@@ -22,6 +22,9 @@ use PDO; ...@@ -22,6 +22,9 @@ use PDO;
use PDOStatement; use PDOStatement;
use Q2A\Database\Exceptions\ReadingFromEmptyResultException; use Q2A\Database\Exceptions\ReadingFromEmptyResultException;
/**
* Thin wrapper around the PDOStatement class which returns results in a variety of formats.
*/
class DbResult class DbResult
{ {
private $stmt; private $stmt;
......
<?php <?php
use Q2A\Database\DbConnection; use Q2A\Database\DbQueryHelper;
class DbConnectionTest extends PHPUnit_Framework_TestCase class DbQueryHelperTest extends PHPUnit_Framework_TestCase
{ {
/** @var DbConnection */ /** @var DbQueryHelper */
private $dbConnection; private $helper;
protected function setUp() protected function setUp()
{ {
$this->dbConnection = new DbConnection(); $this->helper = new DbQueryHelper();
} }
public function test__expandQueryParameters_success() public function test__expandParameters_success()
{ {
$result = $this->dbConnection->expandQueryParameters('SELECT * FROM table WHERE field = 1', []); $result = $this->helper->expandParameters('SELECT * FROM table WHERE field = 1', []);
$expected = ['SELECT * FROM table WHERE field = 1', []]; $expected = ['SELECT * FROM table WHERE field = 1', []];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('SELECT * FROM table WHERE field = ?', [1]); $result = $this->helper->expandParameters('SELECT * FROM table WHERE field = ?', [1]);
$expected = ['SELECT * FROM table WHERE field = ?', [1]]; $expected = ['SELECT * FROM table WHERE field = ?', [1]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('SELECT * FROM table WHERE field IN (?)', [[1]]); $result = $this->helper->expandParameters('SELECT * FROM table WHERE field IN (?)', [[1]]);
$expected = ['SELECT * FROM table WHERE field IN (?)', [1]]; $expected = ['SELECT * FROM table WHERE field IN (?)', [1]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('SELECT * FROM table WHERE field IN (?)', [[1, 2]]); $result = $this->helper->expandParameters('SELECT * FROM table WHERE field IN (?)', [[1, 2]]);
$expected = ['SELECT * FROM table WHERE field IN (?, ?)', [1, 2]]; $expected = ['SELECT * FROM table WHERE field IN (?, ?)', [1, 2]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('INSERT INTO table(field) VALUES ?', [[ [1] ]]); $result = $this->helper->expandParameters('INSERT INTO table(field) VALUES ?', [[ [1] ]]);
$expected = ['INSERT INTO table(field) VALUES (?)', [1]]; $expected = ['INSERT INTO table(field) VALUES (?)', [1]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('INSERT INTO table(field) VALUES ?', [[ [1], [2] ]]); $result = $this->helper->expandParameters('INSERT INTO table(field) VALUES ?', [[ [1], [2] ]]);
$expected = ['INSERT INTO table(field) VALUES (?), (?)', [1, 2]]; $expected = ['INSERT INTO table(field) VALUES (?), (?)', [1, 2]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2] ]]); $result = $this->helper->expandParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2] ]]);
$expected = ['INSERT INTO table(field1, field2) VALUES (?, ?)', [1, 2]]; $expected = ['INSERT INTO table(field1, field2) VALUES (?, ?)', [1, 2]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
$result = $this->dbConnection->expandQueryParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2], [3, 4] ]]); $result = $this->helper->expandParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2], [3, 4] ]]);
$expected = ['INSERT INTO table(field1, field2) VALUES (?, ?), (?, ?)', [1, 2, 3, 4]]; $expected = ['INSERT INTO table(field1, field2) VALUES (?, ?), (?, ?)', [1, 2, 3, 4]];
$this->assertSame($expected, $result); $this->assertSame($expected, $result);
} }
public function test__expandQueryParameters_incorrect_groups_error() public function test__expandParameters_incorrect_groups_error()
{ {
$this->setExpectedException('Q2A\Database\Exceptions\SelectSpecException'); $this->setExpectedException('Q2A\Database\Exceptions\SelectSpecException');
$this->dbConnection->expandQueryParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2], [3] ]]); $this->helper->expandParameters('INSERT INTO table(field1, field2) VALUES ?', [[ [1, 2], [3] ]]);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment