preg_replace & mysql_real_escape_string problem cleaning SQL - php

check out the method below. If entered value in text box is \ mysql_real_escape_string will return duble backslash but preg_replace will return SQL with only one backslash. Im not that good with regular expression so plz help.
$sql = "INSERT INTO tbl SET val='?'";
$params = array('someval');
public function execute($sql, array $params){
$keys = array();
foreach ($params as $key => $value) {
$keys[] = '/[?]/';
if (get_magic_quotes_gpc()) {
$value = stripslashes($value);
}
$paramsEscaped[$key] = mysql_real_escape_string(trim($value));
}
$sql = preg_replace($keys, $paramsEscaped, $sql, 1, $count);
return $this->query($sql);
}

For me it basically looks like you're re-inventing the wheel and your concept has some serious flaws:
It assumes get_magic_quotes_gpc could be switched on. This feature is broken. You should not code against it. Instead make your application require that it is switched off.
mysql_real_escape_string needs a database link identifier to properly work. You are not providing any. This is a serious issue, you should change your concept.
You're actually not using prepared statements, but you mimic the syntax of those. This is fooling other developers who might think that it is safe to use the code while it is not. This is highly discouraged.
However let's do it, but just don't use preg_replace for the job. That's for various reasons, but especially, as the first pattern of ? results in replacing everything with the first parameter. It's inflexible to deal with the error-cases like too less or too many parameters/placeholders. And additionally imagine a string you insert contains a ? character as well. It would break it. Instead, the already processed part as well as the replacement needs to be skipped (Demo of such).
For that you need to go through, take it apart and process it:
public function execute($sql, array $params)
{
$params = array_map(array($this, 'filter_value'), $params);
$sql = $this->expand_placeholders($sql, $params);
return $this->query($sql);
}
public function filter_value($value)
{
if (get_magic_quotes_gpc())
{
$value = stripslashes($value);
}
$value = trim($value);
$value = mysql_real_escape_string($value);
return $value;
}
public function expand_placeholders($sql, array $params)
{
$sql = (string) $sql;
$params = array_values($params);
$offset = 0;
foreach($params as $param)
{
$place = strpos($sql, '?', $offset);
if ($place === false)
{
throw new InvalidArgumentException('Parameter / Placeholder count mismatch. Not enough placeholders for all parameters.');
}
$sql = substr_replace($sql, $param, $place, 1);
$offset = $place + strlen($param);
}
$place = strpos($sql, '?', $offset);
if ($place === false)
{
throw new InvalidArgumentException('Parameter / Placeholder count mismatch. Too many placeholders.');
}
return $sql;
}
The benefit with already existing prepared statements is, that they actually work. You should really consider to use those. For playing things like that is nice, but you need to deal with much more cases in the end and it's far easier to re-use an existing component tested by thousand of other users.

It's better to use prepared statement. See more info http://www.php.net/manual/en/pdo.prepared-statements.php

Related

Sanitizing array values before mysql inserts

I have the following code blocks in a PHP form-handler:
function filter($data) {
$data = trim(htmlentities(strip_tags($data)));
if (get_magic_quotes_gpc()) {
$data = stripslashes($data);
}
$data = mysql_real_escape_string($data);
return $data;
}
foreach($_POST as $key => $value) {
$data[$key] = filter($value);
}
I am modifying my form to now include checkbox groups:
eg:
<input type="checkbox" name="phone_prefs[]" value="prefer_home">
<input type="checkbox" name="phone_prefs[]" value="prefer_cell">
<input type="checkbox" name="phone_prefs[]" value="prefer_work">
etc.
Because of this code I now have arrays in my _POST variables rather than just strings.
Am I correct in thinking that my filter() function the will not actually sanitize arrays properly? What changes do I need to make to my filter() function to make sure the arrays for the checkboxes are sanitized completely and not an easy target for SQL injection attacks?
As for the sql injection, I would switch to PDO using a prepared statement.
You can use a simple is_array() on your values to check for an array and then loop through it. You are correct, as it is, your filter function will not handle arrays correctly.
Edit: If you use PDO and a prepared statement, you don´t need mysql_real_escape_string anymore. strip_tags, htmlentities and trim are also not needed to store the information safely in a database, they are needed when you output information to the browser (trim not of course...), although htmlspecialchars would be sufficient for that. It´s always better to prepare your information / output correctly for the medium you are outputting to at that moment.
Your function is pretty good, but if you make it recursive it'll crawl nested arrays for you
function filter(&$array) {
$clean = array();
foreach($array as $key => &$value ) {
if( is_array($value) ) {
filter($value);
} else {
$value = trim(strip_tags($value));
if (get_magic_quotes_gpc()) {
$data = stripslashes($value);
}
$data = mysql_real_escape_string($value);
}
}
}
filter($_POST); # filters $_POST and any nested arrays by reference
Edit: Leave out htmlentities(). If you need it, then use it when outputting the values - not when getting them as input.
array_walk_recursive($array,function(&$item){
$item=mysql_real_escape_string($item);
});
You're using a foreach on the $_POST which only loops once, using the Array and handling it like a string.
Try using:
foreach($_POST['phone_prefs'] as $key => $value)
EDIT:
I believe I misunderstood your question:
foreach($_POST as $key => $value)
if (is_array($value))
foreach($_POST[$key] as $key2 => $value2)
/* Setting stuff */
else /* Setting same stuff */
Instead of sanitizing the input manually, use always prepared statements with placeholders. That will transparently pass the input to the database in such a way that it does not need to be escaped and thus is not vulnerable to SQL injection. This is the best current practice.
See the following for more information: http://php.net/manual/en/pdo.prepared-statements.php
I use this on various sites I have created:
public function clean($dirty) {
if (!is_array($dirty)) {
$dirty = ereg_replace("[\'\")(;|`,<>]", "", $dirty);
$dirty = mysql_real_escape_string(trim($dirty));
$clean = stripslashes($dirty);
return $clean;
}
$clean = array();
foreach ($dirty as $p => $data) {
$data = ereg_replace("[\'\")(;|`,<>]", "", $data);
$data = mysql_real_escape_string(trim($data));
$data = stripslashes($data);
$clean[$p] = $data;
}
return $clean;
}
Using mysql_real_escape_string means a MySQL connection is required before using the function, which is not best convenient. I used to use a work around in that case :
function real_escape_string($aQuery) {
if (!is_string($aQuery)) {
return FALSE;
} else {
return strtr($aQuery, array( "\x00" => '\x00', "\n" => '\n', "\r" => '\r', '\\' => '\\\\', "'" => "\'", '"' => '\"', "\x1a" => '\x1a' ));
}
}
But definitely, the best is to use PDO prepared statement instead of mysql. You will enjoy it.

mysql_real_escape_string not being used with given regex

I am using a dataHandler library to handle all of my db inserts / updates, etc.
The library has the following functions:
function prepareValue($value, $connection){
$preparedValue = $value;
if(is_null($value)){
$preparedValue = 'NULL';
}
else{
$preparedValue = '\''.mysql_real_escape_string($value, $connection).'\'';
}
return $preparedValue;
}
function parseParams($params, $type, $connection){
$fields = "";
$values = "";
if ($type == "UPDATE"){
$return = "";
foreach ($params as $key => $value){
if ($return == ""){
if (preg_match("/\)$/", $value)){
$return = $key."=".$value;
}
else{
$return = $key."=".$this->prepareValue($value, $connection);
}
}
else{
if (preg_match("/\)$/", $value)){
$return = $return.", ".$key."=".$value;
}
else{
$return = $return.", ".$key."=".$this->prepareValue($value,
$connection);
}
}
}
return $return;
/* rest of function contains similar but for "INSERT", etc.
}
These functions are then used to build queries using sprintf, as in:
$query = sprintf("UPDATE table SET " .
$this->parseParams($params, "UPDATE", $conn) .
" WHERE fieldValue = %s;", $this->prepareValue($thesis_id, $conn));
$params is an associative array: array("db_field_name"=>$value, "db_field_name2"=>$value2, etc.)
I am now running into problems when I want to do an update or insert of a string that ends in ")" because the parseParams function does not put these values in quotes.
My question is this:
Why would this library NOT call prepareValue on strings that end in a closed parenthesis? Would calling mysql_real_escape_string() on this value cause any problems? I could easily modify the library, but I am assuming there is a reason the author handled this particular regex this way. I just can't figure out what that reason is! And I'm hesitant to make any modifications until I understand the reasoning behind what is here.
Thanks for your help!
Please note that inside prepareValue not only mysql_real_escape_string is applied to the value but it is also put inside '. With this in mind, we could suspect that author assumed all strings ending with ) to be mysql function calls, ie:
$params = array(
'field1' => "John Doe",
'field2' => "CONCAT('John',' ','Doe')",
'field3' => "NOW()"
);
Thats the only reasonable answer that comes to mind.

Backticking MySQL Entities

I've the following method which allows me to protect MySQL entities:
public function Tick($string)
{
$string = explode('.', str_replace('`', '', $string));
foreach ($string as $key => $value)
{
if ($value != '*')
{
$string[$key] = '`' . trim($value) . '`';
}
}
return implode('.', $string);
}
This works fairly well for the use that I make of it.
It protects database, table, field names and even the * operator, however now I also want it to protect function calls, ie:
AVG(database.employees.salary)
Should become:
AVG(`database`.`employees`.`salary`) and not `AVG(database`.`employees`.`salary)`
How should I go about this? Should I use regular expressions?
Also, how can I support more advanced stuff, from:
MAX(AVG(database.table.field1), MAX(database.table.field2))
To:
MAX(AVG(`database`.`table`.`field1`), MAX(`database`.`table`.`field2`))
Please keep in mind that I want to keep this method as simple/fast as possible, since it pretty much iterates over all the entity names in my database.
If this is quoting parts of an SQL statement, and they have only complexity that you descibe, a RegEx is a great approach. On the other hand, if you need to do this to full SQL statements, or simply more complicated components of statements (such as "MAX(AVG(val),MAX(val2))"), you will need to tokenize or parse the string and have a more sophisticated understanding of it to do this quoting accurately.
Given the regular expression approach, you may find it easier to break the function name out as one step, and then use your current code to quote the database/table/column names. This can be done in one RE, but it will be tricker to get right.
Either way, I'd highly recommend writing a few unit test cases. In fact, this is an ideal situation for this approach: it's easy to write the tests, you have some existing cases that work (which you don't want to break), and you have just one more case to add.
Your test can start as simply as:
assert '`ticked`' == Tick('ticked');
assert '`table`.`ticked`' == Tick('table.ticked');
assert 'db`.`table`.`ticked`' == Tick('db.table.ticked');
And then add:
assert 'FN(`ticked`)' == Tick('FN(ticked)');
etc.
Using the test case ndp gave I created a regex to do the hard work for you. The following regex will replace all word boundaries around words that are not followed by an opening parenthesis.
\b(\w+)\b(?!\()
The Tick() functionality would then be implemented in PHP as follows:
function Tick($string)
{
return preg_replace( '/\b(\w+)\b(?!\()/', '`\1`', $string );
}
It's generally a bad idea to pass the whole SQL to the function. That way, you'll always find a case when it doesn't work, unless you fully parse the SQL syntax.
Put the ticks to the names on some previous abstraction level, which makes up the SQL.
Before you explode your string on periods, check if the last character is a parenthesis. If so, this call is a function.
<?php
$string = str_replace('`', '', $string)
$function = "";
if (substr($string,-1) == ")") {
// Strip off function call first
$opening = strpos($string, "(");
$function = substr($string, 0, $opening+1);
$string = substr($string, $opening+1, -1);
}
// Do your existing parsing to $string
if ($function == "") {
// Put function back on string
$string = $function . $string . ")";
}
?>
If you need to cover more advanced situations, like using nested functions, or multiple functions in sequence in one "$string" variable, this would become a much more advanced function, and you'd best ask yourself why these elements aren't being properly ticked in the first place, and not need any further parsing.
EDIT: Updating for nested functions, as per original post edit
To have the above function deal with multiple nested functions, you likely need something that will 'unwrap' your nested functions. I haven't tested this, but the following function might get you on the right track.
<?php
function unwrap($str) {
$pos = strpos($str, "(");
if ($pos === false) return $str; // There's no function call here
$last_close = 0;
$cur_offset = 0; // Start at the beginning
while ($cur_offset <= strlen($str)) {
$first_close = strpos($str, ")", $offset); // Find first deep function
$pos = strrpos($str, "(", $first_close-1); // Find associated opening
if ($pos > $last_close) {
// This function is entirely after the previous function
$ticked = Tick(substr($str, $pos+1, $first_close-$pos)); // Tick the string inside
$str = substr($str, 0, $pos)."{".$ticked."}".substr($str,$first_close); // Replace parenthesis by curly braces temporarily
$first_close += strlen($ticked)-($first_close-$pos); // Shift parenthesis location due to new ticks being added
} else {
// This function wraps other functions; don't tick it
$str = substr($str, 0, $pos)."{".substr($str,$pos+1, $first_close-$pos)."}".substr($str,$first_close);
}
$last_close = $first_close;
$offset = $first_close+1;
}
// Replace the curly braces with parenthesis again
$str = str_replace(array("{","}"), array("(",")"), $str);
}
If you are adding the function calls in your code, as opposed to passing them in through a string-only interface, you can replace the string parsing with type checking:
function Tick($value) {
if (is_object($value)) {
$result = $value->value;
} else {
$result = '`'.str_replace(array('`', '.'), array('', '`.`'), $value).'`';
}
return $result;
}
class SqlFunction {
var $value;
function SqlFunction($function, $params) {
$sane = implode(', ', array_map('Tick', $params));
$this->value = "$function($sane)";
}
}
function Maximum($column) {
return new SqlFunction('MAX', array($column));
}
function Avg($column) {
return new SqlFunction('AVG', array($column));
}
function Greatest() {
$params = func_get_args();
return new SqlFunction('GREATEST', $params);
}
$cases = array(
"'simple'" => Tick('simple'),
"'table.field'" => Tick('table.field'),
"'table.*'" => Tick('table.*'),
"'evil`hack'" => Tick('evil`hack'),
"Avg('database.table.field')" => Tick(Avg('database.table.field')),
"Greatest(Avg('table.field1'), Maximum('table.field2'))" => Tick(Greatest(Avg('table.field1'), Maximum('table.field2'))),
);
echo "<table>";
foreach ($cases as $case => $result) {
echo "<tr><td>$case</td><td>$result</td></tr>";
}
echo "</table>";
This avoids any possible SQL injection while remaining legible to future readers of your code.
You could use preg_replace_callback() in conjunction with your Tick() method to skip at least one level of parens:
public function tick($str)
{
return preg_replace_callback('/[^()]*/', array($this, '_tick_replace_callback'), $str);
}
protected function _tick_replace_callback($str) {
$string = explode('.', str_replace('`', '', $string));
foreach ($string as $key => $value)
{
if ($value != '*')
{
$string[$key] = '`' . trim($value) . '`';
}
}
return implode('.', $string);
}
Are you generating the SQL Query or is it being passed to you? If you generating the query I wouldn't pass the whole query string just the parms/values you want to wrap in the backticks or what ever else you need.
EXAMPLE:
function addTick($var) {
return '`' . $var . '`';
}
$condition = addTick($condition);
$SQL = 'SELECT' . $what . '
FROM ' . $table . '
WHERE ' . $condition . ' = ' . $constraint;
This is just a mock but you get the idea that you can pass or loop through your code and build the query string rather than parsing the query string and adding your backticks.

Display query created with stmt [duplicate]

Is there a way to get the raw SQL string executed when calling PDOStatement::execute() on a prepared statement? For debugging purposes this would be extremely useful.
I assume you mean that you want the final SQL query, with parameter values interpolated into it. I understand that this would be useful for debugging, but it is not the way prepared statements work. Parameters are not combined with a prepared statement on the client-side, so PDO should never have access to the query string combined with its parameters.
The SQL statement is sent to the database server when you do prepare(), and the parameters are sent separately when you do execute(). MySQL's general query log does show the final SQL with values interpolated after you execute(). Below is an excerpt from my general query log. I ran the queries from the mysql CLI, not from PDO, but the principle is the same.
081016 16:51:28 2 Query prepare s1 from 'select * from foo where i = ?'
2 Prepare [2] select * from foo where i = ?
081016 16:51:39 2 Query set #a =1
081016 16:51:47 2 Query execute s1 using #a
2 Execute [2] select * from foo where i = 1
You can also get what you want if you set the PDO attribute PDO::ATTR_EMULATE_PREPARES. In this mode, PDO interpolate parameters into the SQL query and sends the whole query when you execute(). This is not a true prepared query. You will circumvent the benefits of prepared queries by interpolating variables into the SQL string before execute().
Re comment from #afilina:
No, the textual SQL query is not combined with the parameters during execution. So there's nothing for PDO to show you.
Internally, if you use PDO::ATTR_EMULATE_PREPARES, PDO makes a copy of the SQL query and interpolates parameter values into it before doing the prepare and execute. But PDO does not expose this modified SQL query.
The PDOStatement object has a property $queryString, but this is set only in the constructor for the PDOStatement, and it's not updated when the query is rewritten with parameters.
It would be a reasonable feature request for PDO to ask them to expose the rewritten query. But even that wouldn't give you the "complete" query unless you use PDO::ATTR_EMULATE_PREPARES.
This is why I show the workaround above of using the MySQL server's general query log, because in this case even a prepared query with parameter placeholders is rewritten on the server, with parameter values backfilled into the query string. But this is only done during logging, not during query execution.
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public static function interpolateQuery($query, $params) {
$keys = array();
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
}
$query = preg_replace($keys, $params, $query, 1, $count);
#trigger_error('replaced '.$count.' keys');
return $query;
}
I modified the method to include handling output of arrays for statements like WHERE IN (?).
UPDATE: Just added check for NULL value and duplicated $params so actual $param values are not modified.
Great work bigwebguy and thanks!
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_string($value))
$values[$key] = "'" . $value . "'";
if (is_array($value))
$values[$key] = "'" . implode("','", $value) . "'";
if (is_null($value))
$values[$key] = 'NULL';
}
$query = preg_replace($keys, $values, $query);
return $query;
}
A solution is to voluntarily put an error in the query and to print the error's message:
//Connection to the database
$co = new PDO('mysql:dbname=myDB;host=localhost','root','');
//We allow to print the errors whenever there is one
$co->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//We create our prepared statement
$stmt = $co->prepare("ELECT * FROM Person WHERE age=:age"); //I removed the 'S' of 'SELECT'
$stmt->bindValue(':age','18',PDO::PARAM_STR);
try {
$stmt->execute();
} catch (PDOException $e) {
echo $e->getMessage();
}
Standard output:
SQLSTATE[42000]: Syntax error or access violation: [...] near 'ELECT * FROM Person WHERE age=18' at line 1
It is important to note that it only prints the first 80 characters of the query.
A bit late probably but now there is PDOStatement::debugDumpParams
Dumps the informations contained by a prepared statement directly on
the output. It will provide the SQL query in use, the number of
parameters used (Params), the list of parameters, with their name,
type (paramtype) as an integer, their key name or position, and the
position in the query (if this is supported by the PDO driver,
otherwise, it will be -1).
You can find more on the official php docs
Example:
<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour');
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR, 12);
$sth->execute();
$sth->debugDumpParams();
?>
Added a little bit more to the code by Mike - walk the values to add single quotes
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_array($value))
$values[$key] = implode(',', $value);
if (is_null($value))
$values[$key] = 'NULL';
}
// Walk the array to see if we can add single-quotes to strings
array_walk($values, create_function('&$v, $k', 'if (!is_numeric($v) && $v!="NULL") $v = "\'".$v."\'";'));
$query = preg_replace($keys, $values, $query, 1, $count);
return $query;
}
PDOStatement has a public property $queryString. It should be what you want.
I've just notice that PDOStatement has an undocumented method debugDumpParams() which you may also want to look at.
You can extend PDOStatement class to capture the bounded variables and store them for later use. Then 2 methods may be added, one for variable sanitizing ( debugBindedVariables ) and another to print the query with those variables ( debugQuery ):
class DebugPDOStatement extends \PDOStatement{
private $bound_variables=array();
protected $pdo;
protected function __construct($pdo) {
$this->pdo = $pdo;
}
public function bindValue($parameter, $value, $data_type=\PDO::PARAM_STR){
$this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>$value);
return parent::bindValue($parameter, $value, $data_type);
}
public function bindParam($parameter, &$variable, $data_type=\PDO::PARAM_STR, $length=NULL , $driver_options=NULL){
$this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>&$variable);
return parent::bindParam($parameter, $variable, $data_type, $length, $driver_options);
}
public function debugBindedVariables(){
$vars=array();
foreach($this->bound_variables as $key=>$val){
$vars[$key] = $val->value;
if($vars[$key]===NULL)
continue;
switch($val->type){
case \PDO::PARAM_STR: $type = 'string'; break;
case \PDO::PARAM_BOOL: $type = 'boolean'; break;
case \PDO::PARAM_INT: $type = 'integer'; break;
case \PDO::PARAM_NULL: $type = 'null'; break;
default: $type = FALSE;
}
if($type !== FALSE)
settype($vars[$key], $type);
}
if(is_numeric(key($vars)))
ksort($vars);
return $vars;
}
public function debugQuery(){
$queryString = $this->queryString;
$vars=$this->debugBindedVariables();
$params_are_numeric=is_numeric(key($vars));
foreach($vars as $key=>&$var){
switch(gettype($var)){
case 'string': $var = "'{$var}'"; break;
case 'integer': $var = "{$var}"; break;
case 'boolean': $var = $var ? 'TRUE' : 'FALSE'; break;
case 'NULL': $var = 'NULL';
default:
}
}
if($params_are_numeric){
$queryString = preg_replace_callback( '/\?/', function($match) use( &$vars) { return array_shift($vars); }, $queryString);
}else{
$queryString = strtr($queryString, $vars);
}
echo $queryString.PHP_EOL;
}
}
class DebugPDO extends \PDO{
public function __construct($dsn, $username="", $password="", $driver_options=array()) {
$driver_options[\PDO::ATTR_STATEMENT_CLASS] = array('DebugPDOStatement', array($this));
$driver_options[\PDO::ATTR_PERSISTENT] = FALSE;
parent::__construct($dsn,$username,$password, $driver_options);
}
}
And then you can use this inherited class for debugging purpouses.
$dbh = new DebugPDO('mysql:host=localhost;dbname=test;','user','pass');
$var='user_test';
$sql=$dbh->prepare("SELECT user FROM users WHERE user = :test");
$sql->bindValue(':test', $var, PDO::PARAM_STR);
$sql->execute();
$sql->debugQuery();
print_r($sql->debugBindedVariables());
Resulting in
SELECT user FROM users WHERE user = 'user_test'
Array (
[:test] => user_test
)
I spent a good deal of time researching this situation for my own needs. This and several other SO threads helped me a great deal, so I wanted to share what I came up with.
While having access to the interpolated query string is a significant benefit while troubleshooting, we wanted to be able to maintain a log of only certain queries (therefore, using the database logs for this purpose was not ideal). We also wanted to be able to use the logs to recreate the condition of the tables at any given time, therefore, we needed to make certain the interpolated strings were escaped properly. Finally, we wanted to extend this functionality to our entire code base having to re-write as little of it as possible (deadlines, marketing, and such; you know how it is).
My solution was to extend the functionality of the default PDOStatement object to cache the parameterized values (or references), and when the statement is executed, use the functionality of the PDO object to properly escape the parameters when they are injected back in to the query string. We could then tie in to execute method of the statement object and log the actual query that was executed at that time (or at least as faithful of a reproduction as possible).
As I said, we didn't want to modify the entire code base to add this functionality, so we overwrite the default bindParam() and bindValue() methods of the PDOStatement object, do our caching of the bound data, then call parent::bindParam() or parent::bindValue(). This allowed our existing code base to continue to function as normal.
Finally, when the execute() method is called, we perform our interpolation and provide the resultant string as a new property E_PDOStatement->fullQuery. This can be output to view the query or, for example, written to a log file.
The extension, along with installation and configuration instructions, are available on github:
https://github.com/noahheck/E_PDOStatement
DISCLAIMER:
Obviously, as I mentioned, I wrote this extension. Because it was developed with help from many threads here, I wanted to post my solution here in case anyone else comes across these threads, just as I did.
None of the existing answers seemed complete or safe, so I came up with this function, which has the following improvements:
works with both unnamed (?) and named (:foo) parameters.
using PDO::quote() to properly escape values which are not NULL, int, float or bool.
properly handles string values containing "?" and ":foo" without mistaking them for placeholders.
function interpolateSQL(PDO $pdo, string $query, array $params) : string {
$s = chr(2); // Escape sequence for start of placeholder
$e = chr(3); // Escape sequence for end of placeholder
$keys = [];
$values = [];
// Make sure we use escape sequences that are not present in any value
// to escape the placeholders.
foreach ($params as $key => $value) {
while( mb_stripos($value, $s) !== false ) $s .= $s;
while( mb_stripos($value, $e) !== false ) $e .= $e;
}
foreach ($params as $key => $value) {
// Build a regular expression for each parameter
$keys[] = is_string($key) ? "/$s:$key$e/" : "/$s\?$e/";
// Treat each value depending on what type it is.
// While PDO::quote() has a second parameter for type hinting,
// it doesn't seem reliable (at least for the SQLite driver).
if( is_null($value) ){
$values[$key] = 'NULL';
}
elseif( is_int($value) || is_float($value) ){
$values[$key] = $value;
}
elseif( is_bool($value) ){
$values[$key] = $value ? 'true' : 'false';
}
else{
$value = str_replace('\\', '\\\\', $value);
$values[$key] = $pdo->quote($value);
}
}
// Surround placehodlers with escape sequence, so we don't accidentally match
// "?" or ":foo" inside any of the values.
$query = preg_replace(['/\?/', '/(:[a-zA-Z0-9_]+)/'], ["$s?$e", "$s$1$e"], $query);
// Replace placeholders with actual values
$query = preg_replace($keys, $values, $query, 1, $count);
// Verify that we replaced exactly as many placeholders as there are keys and values
if( $count !== count($keys) || $count !== count($values) ){
throw new \Exception('Number of replacements not same as number of keys and/or values');
}
return $query;
}
I'm sure it can be improved further.
In my case, I eventually ended up just logging the actual "unprepared query" (i.e. SQL containing placeholders) along with JSON-encoded parameters. However, this code might come in use for some use cases where you really need to interpolate the final SQL query.
You can use sprintf(str_replace('?', '"%s"', $sql), ...$params);
Here is an example:
function mysqli_prepared_query($link, $sql, $types='', $params=array()) {
echo sprintf(str_replace('?', '"%s"', $sql), ...$params);
//prepare, bind, execute
}
$link = new mysqli($server, $dbusername, $dbpassword, $database);
$sql = "SELECT firstname, lastname FROM users WHERE userage >= ? AND favecolor = ?";
$types = "is"; //integer and string
$params = array(20, "Brown");
if(!$qry = mysqli_prepared_query($link, $sql, $types, $params)){
echo "Failed";
} else {
echo "Success";
}
Note this only works for PHP >= 5.6
The $queryString property mentioned will probably only return the query passed in, without the parameters replaced with their values. In .Net, I have the catch part of my query executer do a simple search replace on the parameters with their values which was supplied so that the error log can show actual values that were being used for the query. You should be able to enumerate the parameters in PHP, and replace the parameters with their assigned value.
I know this question is a bit old, but, I'm using this code since lot time ago (I've used response from #chris-go), and now, these code are obsolete with PHP 7.2
I'll post an updated version of these code (Credit for the main code are from #bigwebguy, #mike and #chris-go, all of them answers of this question):
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_array($value))
$values[$key] = implode(',', $value);
if (is_null($value))
$values[$key] = 'NULL';
}
// Walk the array to see if we can add single-quotes to strings
array_walk($values, function(&$v, $k) { if (!is_numeric($v) && $v != "NULL") $v = "\'" . $v . "\'"; });
$query = preg_replace($keys, $values, $query, 1, $count);
return $query;
}
Note the change on the code are on array_walk() function, replacing create_function by an anonymous function. This make these good piece of code functional and compatible with PHP 7.2 (and hope future versions too).
preg_replace didn't work for me and when binding_ was over 9, binding_1 and binding_10 was replaced with str_replace (leaving the 0 behind), so I made the replacements backwards:
public function interpolateQuery($query, $params) {
$keys = array();
$length = count($params)-1;
for ($i = $length; $i >=0; $i--) {
$query = str_replace(':binding_'.(string)$i, '\''.$params[$i]['val'].'\'', $query);
}
// $query = str_replace('SQL_CALC_FOUND_ROWS', '', $query, $count);
return $query;
}
Hope someone finds it useful.
I need to log full query string after bind param so this is a piece in my code. Hope, it is useful for everyone hat has the same issue.
/**
*
* #param string $str
* #return string
*/
public function quote($str) {
if (!is_array($str)) {
return $this->pdo->quote($str);
} else {
$str = implode(',', array_map(function($v) {
return $this->quote($v);
}, $str));
if (empty($str)) {
return 'NULL';
}
return $str;
}
}
/**
*
* #param string $query
* #param array $params
* #return string
* #throws Exception
*/
public function interpolateQuery($query, $params) {
$ps = preg_split("/'/is", $query);
$pieces = [];
$prev = null;
foreach ($ps as $p) {
$lastChar = substr($p, strlen($p) - 1);
if ($lastChar != "\\") {
if ($prev === null) {
$pieces[] = $p;
} else {
$pieces[] = $prev . "'" . $p;
$prev = null;
}
} else {
$prev .= ($prev === null ? '' : "'") . $p;
}
}
$arr = [];
$indexQuestionMark = -1;
$matches = [];
for ($i = 0; $i < count($pieces); $i++) {
if ($i % 2 !== 0) {
$arr[] = "'" . $pieces[$i] . "'";
} else {
$st = '';
$s = $pieces[$i];
while (!empty($s)) {
if (preg_match("/(\?|:[A-Z0-9_\-]+)/is", $s, $matches, PREG_OFFSET_CAPTURE)) {
$index = $matches[0][1];
$st .= substr($s, 0, $index);
$key = $matches[0][0];
$s = substr($s, $index + strlen($key));
if ($key == '?') {
$indexQuestionMark++;
if (array_key_exists($indexQuestionMark, $params)) {
$st .= $this->quote($params[$indexQuestionMark]);
} else {
throw new Exception('Wrong params in query at ' . $index);
}
} else {
if (array_key_exists($key, $params)) {
$st .= $this->quote($params[$key]);
} else {
throw new Exception('Wrong params in query with key ' . $key);
}
}
} else {
$st .= $s;
$s = null;
}
}
$arr[] = $st;
}
}
return implode('', $arr);
}
Mike's answer is working good until you are using the "re-use" bind value.
For example:
SELECT * FROM `an_modules` AS `m` LEFT JOIN `an_module_sites` AS `ms` ON m.module_id = ms.module_id WHERE 1 AND `module_enable` = :module_enable AND `site_id` = :site_id AND (`module_system_name` LIKE :search OR `module_version` LIKE :search)
The Mike's answer can only replace first :search but not the second.
So, I rewrite his answer to work with multiple parameters that can re-used properly.
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
$values_limit = [];
$words_repeated = array_count_values(str_word_count($query, 1, ':_'));
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
$values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
} else {
$keys[] = '/[?]/';
$values_limit = [];
}
if (is_string($value))
$values[$key] = "'" . $value . "'";
if (is_array($value))
$values[$key] = "'" . implode("','", $value) . "'";
if (is_null($value))
$values[$key] = 'NULL';
}
if (is_array($values)) {
foreach ($values as $key => $val) {
if (isset($values_limit[$key])) {
$query = preg_replace(['/:'.$key.'/'], [$val], $query, $values_limit[$key], $count);
} else {
$query = preg_replace(['/:'.$key.'/'], [$val], $query, 1, $count);
}
}
unset($key, $val);
} else {
$query = preg_replace($keys, $values, $query, 1, $count);
}
unset($keys, $values, $values_limit, $words_repeated);
return $query;
}

Getting raw SQL query string from PDO prepared statements

Is there a way to get the raw SQL string executed when calling PDOStatement::execute() on a prepared statement? For debugging purposes this would be extremely useful.
I assume you mean that you want the final SQL query, with parameter values interpolated into it. I understand that this would be useful for debugging, but it is not the way prepared statements work. Parameters are not combined with a prepared statement on the client-side, so PDO should never have access to the query string combined with its parameters.
The SQL statement is sent to the database server when you do prepare(), and the parameters are sent separately when you do execute(). MySQL's general query log does show the final SQL with values interpolated after you execute(). Below is an excerpt from my general query log. I ran the queries from the mysql CLI, not from PDO, but the principle is the same.
081016 16:51:28 2 Query prepare s1 from 'select * from foo where i = ?'
2 Prepare [2] select * from foo where i = ?
081016 16:51:39 2 Query set #a =1
081016 16:51:47 2 Query execute s1 using #a
2 Execute [2] select * from foo where i = 1
You can also get what you want if you set the PDO attribute PDO::ATTR_EMULATE_PREPARES. In this mode, PDO interpolate parameters into the SQL query and sends the whole query when you execute(). This is not a true prepared query. You will circumvent the benefits of prepared queries by interpolating variables into the SQL string before execute().
Re comment from #afilina:
No, the textual SQL query is not combined with the parameters during execution. So there's nothing for PDO to show you.
Internally, if you use PDO::ATTR_EMULATE_PREPARES, PDO makes a copy of the SQL query and interpolates parameter values into it before doing the prepare and execute. But PDO does not expose this modified SQL query.
The PDOStatement object has a property $queryString, but this is set only in the constructor for the PDOStatement, and it's not updated when the query is rewritten with parameters.
It would be a reasonable feature request for PDO to ask them to expose the rewritten query. But even that wouldn't give you the "complete" query unless you use PDO::ATTR_EMULATE_PREPARES.
This is why I show the workaround above of using the MySQL server's general query log, because in this case even a prepared query with parameter placeholders is rewritten on the server, with parameter values backfilled into the query string. But this is only done during logging, not during query execution.
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public static function interpolateQuery($query, $params) {
$keys = array();
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
}
$query = preg_replace($keys, $params, $query, 1, $count);
#trigger_error('replaced '.$count.' keys');
return $query;
}
I modified the method to include handling output of arrays for statements like WHERE IN (?).
UPDATE: Just added check for NULL value and duplicated $params so actual $param values are not modified.
Great work bigwebguy and thanks!
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_string($value))
$values[$key] = "'" . $value . "'";
if (is_array($value))
$values[$key] = "'" . implode("','", $value) . "'";
if (is_null($value))
$values[$key] = 'NULL';
}
$query = preg_replace($keys, $values, $query);
return $query;
}
A solution is to voluntarily put an error in the query and to print the error's message:
//Connection to the database
$co = new PDO('mysql:dbname=myDB;host=localhost','root','');
//We allow to print the errors whenever there is one
$co->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//We create our prepared statement
$stmt = $co->prepare("ELECT * FROM Person WHERE age=:age"); //I removed the 'S' of 'SELECT'
$stmt->bindValue(':age','18',PDO::PARAM_STR);
try {
$stmt->execute();
} catch (PDOException $e) {
echo $e->getMessage();
}
Standard output:
SQLSTATE[42000]: Syntax error or access violation: [...] near 'ELECT * FROM Person WHERE age=18' at line 1
It is important to note that it only prints the first 80 characters of the query.
A bit late probably but now there is PDOStatement::debugDumpParams
Dumps the informations contained by a prepared statement directly on
the output. It will provide the SQL query in use, the number of
parameters used (Params), the list of parameters, with their name,
type (paramtype) as an integer, their key name or position, and the
position in the query (if this is supported by the PDO driver,
otherwise, it will be -1).
You can find more on the official php docs
Example:
<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour');
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR, 12);
$sth->execute();
$sth->debugDumpParams();
?>
Added a little bit more to the code by Mike - walk the values to add single quotes
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_array($value))
$values[$key] = implode(',', $value);
if (is_null($value))
$values[$key] = 'NULL';
}
// Walk the array to see if we can add single-quotes to strings
array_walk($values, create_function('&$v, $k', 'if (!is_numeric($v) && $v!="NULL") $v = "\'".$v."\'";'));
$query = preg_replace($keys, $values, $query, 1, $count);
return $query;
}
PDOStatement has a public property $queryString. It should be what you want.
I've just notice that PDOStatement has an undocumented method debugDumpParams() which you may also want to look at.
You can extend PDOStatement class to capture the bounded variables and store them for later use. Then 2 methods may be added, one for variable sanitizing ( debugBindedVariables ) and another to print the query with those variables ( debugQuery ):
class DebugPDOStatement extends \PDOStatement{
private $bound_variables=array();
protected $pdo;
protected function __construct($pdo) {
$this->pdo = $pdo;
}
public function bindValue($parameter, $value, $data_type=\PDO::PARAM_STR){
$this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>$value);
return parent::bindValue($parameter, $value, $data_type);
}
public function bindParam($parameter, &$variable, $data_type=\PDO::PARAM_STR, $length=NULL , $driver_options=NULL){
$this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>&$variable);
return parent::bindParam($parameter, $variable, $data_type, $length, $driver_options);
}
public function debugBindedVariables(){
$vars=array();
foreach($this->bound_variables as $key=>$val){
$vars[$key] = $val->value;
if($vars[$key]===NULL)
continue;
switch($val->type){
case \PDO::PARAM_STR: $type = 'string'; break;
case \PDO::PARAM_BOOL: $type = 'boolean'; break;
case \PDO::PARAM_INT: $type = 'integer'; break;
case \PDO::PARAM_NULL: $type = 'null'; break;
default: $type = FALSE;
}
if($type !== FALSE)
settype($vars[$key], $type);
}
if(is_numeric(key($vars)))
ksort($vars);
return $vars;
}
public function debugQuery(){
$queryString = $this->queryString;
$vars=$this->debugBindedVariables();
$params_are_numeric=is_numeric(key($vars));
foreach($vars as $key=>&$var){
switch(gettype($var)){
case 'string': $var = "'{$var}'"; break;
case 'integer': $var = "{$var}"; break;
case 'boolean': $var = $var ? 'TRUE' : 'FALSE'; break;
case 'NULL': $var = 'NULL';
default:
}
}
if($params_are_numeric){
$queryString = preg_replace_callback( '/\?/', function($match) use( &$vars) { return array_shift($vars); }, $queryString);
}else{
$queryString = strtr($queryString, $vars);
}
echo $queryString.PHP_EOL;
}
}
class DebugPDO extends \PDO{
public function __construct($dsn, $username="", $password="", $driver_options=array()) {
$driver_options[\PDO::ATTR_STATEMENT_CLASS] = array('DebugPDOStatement', array($this));
$driver_options[\PDO::ATTR_PERSISTENT] = FALSE;
parent::__construct($dsn,$username,$password, $driver_options);
}
}
And then you can use this inherited class for debugging purpouses.
$dbh = new DebugPDO('mysql:host=localhost;dbname=test;','user','pass');
$var='user_test';
$sql=$dbh->prepare("SELECT user FROM users WHERE user = :test");
$sql->bindValue(':test', $var, PDO::PARAM_STR);
$sql->execute();
$sql->debugQuery();
print_r($sql->debugBindedVariables());
Resulting in
SELECT user FROM users WHERE user = 'user_test'
Array (
[:test] => user_test
)
I spent a good deal of time researching this situation for my own needs. This and several other SO threads helped me a great deal, so I wanted to share what I came up with.
While having access to the interpolated query string is a significant benefit while troubleshooting, we wanted to be able to maintain a log of only certain queries (therefore, using the database logs for this purpose was not ideal). We also wanted to be able to use the logs to recreate the condition of the tables at any given time, therefore, we needed to make certain the interpolated strings were escaped properly. Finally, we wanted to extend this functionality to our entire code base having to re-write as little of it as possible (deadlines, marketing, and such; you know how it is).
My solution was to extend the functionality of the default PDOStatement object to cache the parameterized values (or references), and when the statement is executed, use the functionality of the PDO object to properly escape the parameters when they are injected back in to the query string. We could then tie in to execute method of the statement object and log the actual query that was executed at that time (or at least as faithful of a reproduction as possible).
As I said, we didn't want to modify the entire code base to add this functionality, so we overwrite the default bindParam() and bindValue() methods of the PDOStatement object, do our caching of the bound data, then call parent::bindParam() or parent::bindValue(). This allowed our existing code base to continue to function as normal.
Finally, when the execute() method is called, we perform our interpolation and provide the resultant string as a new property E_PDOStatement->fullQuery. This can be output to view the query or, for example, written to a log file.
The extension, along with installation and configuration instructions, are available on github:
https://github.com/noahheck/E_PDOStatement
DISCLAIMER:
Obviously, as I mentioned, I wrote this extension. Because it was developed with help from many threads here, I wanted to post my solution here in case anyone else comes across these threads, just as I did.
None of the existing answers seemed complete or safe, so I came up with this function, which has the following improvements:
works with both unnamed (?) and named (:foo) parameters.
using PDO::quote() to properly escape values which are not NULL, int, float or bool.
properly handles string values containing "?" and ":foo" without mistaking them for placeholders.
function interpolateSQL(PDO $pdo, string $query, array $params) : string {
$s = chr(2); // Escape sequence for start of placeholder
$e = chr(3); // Escape sequence for end of placeholder
$keys = [];
$values = [];
// Make sure we use escape sequences that are not present in any value
// to escape the placeholders.
foreach ($params as $key => $value) {
while( mb_stripos($value, $s) !== false ) $s .= $s;
while( mb_stripos($value, $e) !== false ) $e .= $e;
}
foreach ($params as $key => $value) {
// Build a regular expression for each parameter
$keys[] = is_string($key) ? "/$s:$key$e/" : "/$s\?$e/";
// Treat each value depending on what type it is.
// While PDO::quote() has a second parameter for type hinting,
// it doesn't seem reliable (at least for the SQLite driver).
if( is_null($value) ){
$values[$key] = 'NULL';
}
elseif( is_int($value) || is_float($value) ){
$values[$key] = $value;
}
elseif( is_bool($value) ){
$values[$key] = $value ? 'true' : 'false';
}
else{
$value = str_replace('\\', '\\\\', $value);
$values[$key] = $pdo->quote($value);
}
}
// Surround placehodlers with escape sequence, so we don't accidentally match
// "?" or ":foo" inside any of the values.
$query = preg_replace(['/\?/', '/(:[a-zA-Z0-9_]+)/'], ["$s?$e", "$s$1$e"], $query);
// Replace placeholders with actual values
$query = preg_replace($keys, $values, $query, 1, $count);
// Verify that we replaced exactly as many placeholders as there are keys and values
if( $count !== count($keys) || $count !== count($values) ){
throw new \Exception('Number of replacements not same as number of keys and/or values');
}
return $query;
}
I'm sure it can be improved further.
In my case, I eventually ended up just logging the actual "unprepared query" (i.e. SQL containing placeholders) along with JSON-encoded parameters. However, this code might come in use for some use cases where you really need to interpolate the final SQL query.
You can use sprintf(str_replace('?', '"%s"', $sql), ...$params);
Here is an example:
function mysqli_prepared_query($link, $sql, $types='', $params=array()) {
echo sprintf(str_replace('?', '"%s"', $sql), ...$params);
//prepare, bind, execute
}
$link = new mysqli($server, $dbusername, $dbpassword, $database);
$sql = "SELECT firstname, lastname FROM users WHERE userage >= ? AND favecolor = ?";
$types = "is"; //integer and string
$params = array(20, "Brown");
if(!$qry = mysqli_prepared_query($link, $sql, $types, $params)){
echo "Failed";
} else {
echo "Success";
}
Note this only works for PHP >= 5.6
The $queryString property mentioned will probably only return the query passed in, without the parameters replaced with their values. In .Net, I have the catch part of my query executer do a simple search replace on the parameters with their values which was supplied so that the error log can show actual values that were being used for the query. You should be able to enumerate the parameters in PHP, and replace the parameters with their assigned value.
I know this question is a bit old, but, I'm using this code since lot time ago (I've used response from #chris-go), and now, these code are obsolete with PHP 7.2
I'll post an updated version of these code (Credit for the main code are from #bigwebguy, #mike and #chris-go, all of them answers of this question):
/**
* Replaces any parameter placeholders in a query with the value of that
* parameter. Useful for debugging. Assumes anonymous parameters from
* $params are are in the same order as specified in $query
*
* #param string $query The sql query with parameter placeholders
* #param array $params The array of substitution parameters
* #return string The interpolated query
*/
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_array($value))
$values[$key] = implode(',', $value);
if (is_null($value))
$values[$key] = 'NULL';
}
// Walk the array to see if we can add single-quotes to strings
array_walk($values, function(&$v, $k) { if (!is_numeric($v) && $v != "NULL") $v = "\'" . $v . "\'"; });
$query = preg_replace($keys, $values, $query, 1, $count);
return $query;
}
Note the change on the code are on array_walk() function, replacing create_function by an anonymous function. This make these good piece of code functional and compatible with PHP 7.2 (and hope future versions too).
preg_replace didn't work for me and when binding_ was over 9, binding_1 and binding_10 was replaced with str_replace (leaving the 0 behind), so I made the replacements backwards:
public function interpolateQuery($query, $params) {
$keys = array();
$length = count($params)-1;
for ($i = $length; $i >=0; $i--) {
$query = str_replace(':binding_'.(string)$i, '\''.$params[$i]['val'].'\'', $query);
}
// $query = str_replace('SQL_CALC_FOUND_ROWS', '', $query, $count);
return $query;
}
Hope someone finds it useful.
I need to log full query string after bind param so this is a piece in my code. Hope, it is useful for everyone hat has the same issue.
/**
*
* #param string $str
* #return string
*/
public function quote($str) {
if (!is_array($str)) {
return $this->pdo->quote($str);
} else {
$str = implode(',', array_map(function($v) {
return $this->quote($v);
}, $str));
if (empty($str)) {
return 'NULL';
}
return $str;
}
}
/**
*
* #param string $query
* #param array $params
* #return string
* #throws Exception
*/
public function interpolateQuery($query, $params) {
$ps = preg_split("/'/is", $query);
$pieces = [];
$prev = null;
foreach ($ps as $p) {
$lastChar = substr($p, strlen($p) - 1);
if ($lastChar != "\\") {
if ($prev === null) {
$pieces[] = $p;
} else {
$pieces[] = $prev . "'" . $p;
$prev = null;
}
} else {
$prev .= ($prev === null ? '' : "'") . $p;
}
}
$arr = [];
$indexQuestionMark = -1;
$matches = [];
for ($i = 0; $i < count($pieces); $i++) {
if ($i % 2 !== 0) {
$arr[] = "'" . $pieces[$i] . "'";
} else {
$st = '';
$s = $pieces[$i];
while (!empty($s)) {
if (preg_match("/(\?|:[A-Z0-9_\-]+)/is", $s, $matches, PREG_OFFSET_CAPTURE)) {
$index = $matches[0][1];
$st .= substr($s, 0, $index);
$key = $matches[0][0];
$s = substr($s, $index + strlen($key));
if ($key == '?') {
$indexQuestionMark++;
if (array_key_exists($indexQuestionMark, $params)) {
$st .= $this->quote($params[$indexQuestionMark]);
} else {
throw new Exception('Wrong params in query at ' . $index);
}
} else {
if (array_key_exists($key, $params)) {
$st .= $this->quote($params[$key]);
} else {
throw new Exception('Wrong params in query with key ' . $key);
}
}
} else {
$st .= $s;
$s = null;
}
}
$arr[] = $st;
}
}
return implode('', $arr);
}
Mike's answer is working good until you are using the "re-use" bind value.
For example:
SELECT * FROM `an_modules` AS `m` LEFT JOIN `an_module_sites` AS `ms` ON m.module_id = ms.module_id WHERE 1 AND `module_enable` = :module_enable AND `site_id` = :site_id AND (`module_system_name` LIKE :search OR `module_version` LIKE :search)
The Mike's answer can only replace first :search but not the second.
So, I rewrite his answer to work with multiple parameters that can re-used properly.
public function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
$values_limit = [];
$words_repeated = array_count_values(str_word_count($query, 1, ':_'));
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
$values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
} else {
$keys[] = '/[?]/';
$values_limit = [];
}
if (is_string($value))
$values[$key] = "'" . $value . "'";
if (is_array($value))
$values[$key] = "'" . implode("','", $value) . "'";
if (is_null($value))
$values[$key] = 'NULL';
}
if (is_array($values)) {
foreach ($values as $key => $val) {
if (isset($values_limit[$key])) {
$query = preg_replace(['/:'.$key.'/'], [$val], $query, $values_limit[$key], $count);
} else {
$query = preg_replace(['/:'.$key.'/'], [$val], $query, 1, $count);
}
}
unset($key, $val);
} else {
$query = preg_replace($keys, $values, $query, 1, $count);
}
unset($keys, $values, $values_limit, $words_repeated);
return $query;
}

Categories