Related
I'm trying to add some users to my Ldap DB but I get some errors (invalid dn syntax) when I use some special characters like ",.". I need a function that escape all characters. I try preg_quote but I get some errors in some cases.
Thanks in advance
Code:
$user = 'Test , Name S.L';
if(!(ldap_add($ds, "cn=" . $user . ",".LDAP_DN_BASE, $info))) {
include 'error_new_account.php';
}
EDIT Jan 2013: added support for escaping leading/trailing spaces in DN strings, per RFC 4514. Thanks to Eugenio for pointing out this issue.
EDIT 2014: I added this function to PHP 5.6. The code below is now a like-for-like drop-in replacement for earlier PHP versions.
if (!function_exists('ldap_escape')) {
define('LDAP_ESCAPE_FILTER', 0x01);
define('LDAP_ESCAPE_DN', 0x02);
/**
* #param string $subject The subject string
* #param string $ignore Set of characters to leave untouched
* #param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the
* set(s) of characters to escape.
* #return string
*/
function ldap_escape($subject, $ignore = '', $flags = 0)
{
static $charMaps = array(
LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
);
// Pre-process the char maps on first call
if (!isset($charMaps[0])) {
$charMaps[0] = array();
for ($i = 0; $i < 256; $i++) {
$charMaps[0][chr($i)] = sprintf('\\%02x', $i);;
}
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
$chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
$charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
}
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
$chr = $charMaps[LDAP_ESCAPE_DN][$i];
unset($charMaps[LDAP_ESCAPE_DN][$i]);
$charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
}
}
// Create the base char map to escape
$flags = (int)$flags;
$charMap = array();
if ($flags & LDAP_ESCAPE_FILTER) {
$charMap += $charMaps[LDAP_ESCAPE_FILTER];
}
if ($flags & LDAP_ESCAPE_DN) {
$charMap += $charMaps[LDAP_ESCAPE_DN];
}
if (!$charMap) {
$charMap = $charMaps[0];
}
// Remove any chars to ignore from the list
$ignore = (string)$ignore;
for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
unset($charMap[$ignore[$i]]);
}
// Do the main replacement
$result = strtr($subject, $charMap);
// Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
if ($flags & LDAP_ESCAPE_DN) {
if ($result[0] === ' ') {
$result = '\\20' . substr($result, 1);
}
if ($result[strlen($result) - 1] === ' ') {
$result = substr($result, 0, -1) . '\\20';
}
}
return $result;
}
}
So you would do:
$user = 'Test , Name S.L';
$cn = ldap_escape($user, '', LDAP_ESCAPE_DN);
if (!ldap_add($ds, "cn={$cn}," . LDAP_DN_BASE, $info)) {
include 'error_new_account.php';
}
PHP 5.6 Beta released ldap_escape() function recently and it is in effect, However, this version is not production ready at present, you can very use it for your development purposes as of now.
Just a heads up if your not on PHP 5.6 yet, you can mirror the exact PHP 5.6 function ldap_escape() using the methods I created below, keep in mind this is meant for use in a class. The above answer doesn't perform exactly like the ldap_escape function, as in it doesn't escape all characters into a hex string if no flags have been given, so this would be more suitable for a drop in replacement for earlier versions of PHP, in an object oriented way.
I've documented every line for an easier understanding on whats going on. Scroll down for output.
Methods (Compatible with PHP 5 or greater):
/**
* Escapes the inserted value for LDAP.
*
* #param string $value The value to escape
* #param string $ignore The characters to ignore
* #param int $flags The PHP flag to use
*
* #return bool|string
*/
public function escapeManual($value, $ignore = '*', $flags = 0)
{
/*
* If a flag was supplied, we'll send the value
* off to be escaped using the PHP flag values
* and return the result.
*/
if($flags) {
return $this->escapeWithFlags($value, $ignore, $flags);
}
// Convert ignore string into an array
$ignores = str_split($ignore);
// Convert the value to a hex string
$hex = bin2hex($value);
/*
* Separate the string, with the hex length of 2,
* and place a backslash on the end of each section
*/
$value = chunk_split($hex, 2, "\\");
/*
* We'll append a backslash at the front of the string
* and remove the ending backslash of the string
*/
$value = "\\" . substr($value, 0, -1);
// Go through each character to ignore
foreach($ignores as $charToIgnore)
{
// Convert the characterToIgnore to a hex
$hexed = bin2hex($charToIgnore);
// Replace the hexed variant with the original character
$value = str_replace("\\" . $hexed, $charToIgnore, $value);
}
// Finally we can return the escaped value
return $value;
}
/**
* Escapes the inserted value with flags. Supplying either 1
* or 2 into the flags parameter will escape only certain values
*
*
* #param string $value The value to escape
* #param string $ignore The characters to ignore
* #param int $flags The PHP flag to use
* #return bool|string
*/
public function escapeWithFlags($value, $ignore = '*', $flags = 0)
{
// Convert ignore string into an array
$ignores = str_split($ignore);
$escapeFilter = ['\\', '*', '(', ')'];
$escapeDn = ['\\', ',', '=', '+', '<', '>', ';', '"', '#'];
switch($flags)
{
case 1:
// Int 1 equals to LDAP_ESCAPE_FILTER
$escapes = $escapeFilter;
break;
case 2:
// Int 2 equals to LDAP_ESCAPE_DN
$escapes = $escapeDn;
break;
case 3:
// If both LDAP_ESCAPE_FILTER and LDAP_ESCAPE_DN are used
$escapes = array_merge($escapeFilter, $escapeDn);
break;
default:
// Customize your own default return value
return false;
}
foreach($escapes as $escape)
{
// Make sure the escaped value isn't inside the ignore array
if( ! in_array($escape, $ignores))
{
$hexed = chunk_split(bin2hex($escape), 2, "\\");
$hexed = "\\" . substr($hexed, 0, -1);
$value = str_replace($escape, $hexed, $value);
}
}
return $value;
}
Tests (be aware that LDAP_ESCAPE constants are only available in PHP 5.6):
// Value to escape
$value = 'testing=+<>"";:#()*\x00';
$php = ldap_escape($value, $ignore = '*');
$man = $this->escapeManual($value, $ignore = '*');
echo $php; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30
echo $man; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30
$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_DN);
$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_DN);
echo $php; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00
echo $man; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00
$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_FILTER);
$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_FILTER);
echo $php; // testing=+<>"";:#\28\29*\5cx00
echo $man; // testing=+<>"";:#\28\29*\5cx00
Github Gist link: https://gist.github.com/stevebauman/0db9b5daa414d60fc266
Those characters must escaped to be part of the data of a distinguished name or relative distinguished name. Escape the character (as in all LDAP) with a backslash 2 hex digit, such as \2a. Anything else would not be in compliance with the standards body documents. See RFC4514 for more specific information regarding the string representation of distinguished names.
Now php can't work directly wit Postgresql array. For example, php taking postgresql array like
'{"foo","bar"}'
I need simple php function to create multidimensional postgresql array from php array.
I think that experimental pg_convert() isn't optimal because it needs of extra data to form simple array string for database output, maybe I misunderstood the idea of this function.
For example, I need to convert
$from=array( array( "par_1_1","par_1_2" ), array( "array_2_1", "array_2_2" ) );
$to='{{"par_1_1","par_1_2"},{"par_2_1","par_2_2"}}';
Can I use array_walk_recursive() to convert the deepest elements of array?
Here's a simple function for converting a PHP array to PG array.
function to_pg_array($set) {
settype($set, 'array'); // can be called with a scalar or array
$result = array();
foreach ($set as $t) {
if (is_array($t)) {
$result[] = to_pg_array($t);
} else {
$t = str_replace('"', '\\"', $t); // escape double quote
if (! is_numeric($t)) // quote only non-numeric values
$t = '"' . $t . '"';
$result[] = $t;
}
}
return '{' . implode(",", $result) . '}'; // format
}
A little edit that uses pg_escape_string for quoting and that support PHP NULLS and booleans:
/**
* Converts a php array into a postgres array (also multidimensional)
*
* Each element is escaped using pg_escape_string, only string values
* are enclosed within single quotes, numeric values no; special
* elements as php nulls or booleans are literally converted, so the
* php NULL value is written literally 'NULL' and becomes a postgres
* NULL (the same thing is done with TRUE and FALSE values).
*
* Examples :
* VARCHAR VERY BASTARD ARRAY :
* $input = array('bla bla', 'ehi "hello"', 'abc, def', ' \'VERY\' "BASTARD,\'value"', NULL);
*
* to_pg_array($input) ==>> 'ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]'
*
* try to put this value in a query (you will get a valid result):
* select unnest(ARRAY['bla bla','ehi "hello"','abc, def',' ''VERY'' "BASTARD,''value"',NULL]::varchar[])
*
* NUMERIC ARRAY:
* $input = array(1, 2, 3, 8.5, null, 7.32);
* to_pg_array($input) ==>> 'ARRAY[1,2,3,8.5,NULL,7.32]'
* try: select unnest(ARRAY[1,2,3,8.5,NULL,7.32]::numeric[])
*
* BOOLEAN ARRAY:
* $input = array(false, true, true, null);
* to_pg_array($input) ==>> 'ARRAY[FALSE,TRUE,TRUE,NULL]'
* try: select unnest(ARRAY[FALSE,TRUE,TRUE,NULL]::boolean[])
*
* MULTIDIMENSIONAL ARRAY:
* $input = array(array('abc', 'def'), array('ghi', 'jkl'));
* to_pg_array($input) ==>> 'ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]'
* try: select ARRAY[ARRAY['abc','def'],ARRAY['ghi','jkl']]::varchar[][]
*
* EMPTY ARRAY (is different than null!!!):
* $input = array();
* to_pg_array($input) ==>> 'ARRAY[]'
* try: select unnest(ARRAY[]::varchar[])
*
* NULL VALUE :
* $input = NULL;
* to_pg_array($input) ==>> 'NULL'
* the functions returns a string='NULL' (literally 'NULL'), so putting it
* in the query, it becomes a postgres null value.
*
* If you pass a value that is not an array, the function returns a literal 'NULL'.
*
* You should put the result of this functions directly inside a query,
* without quoting or escaping it and you cannot use this result as parameter
* of a prepared statement.
*
* Example:
* $q = 'INSERT INTO foo (field1, field_array) VALUES ($1, ' . to_pg_array($php_array) . '::varchar[])';
* $params = array('scalar_parameter');
*
* It is recommended to write the array type (ex. varchar[], numeric[], ...)
* because if the array is empty or contains only null values, postgres
* can give an error (cannot determine type of an empty array...)
*
* The function returns only a syntactically well-formed array, it does not
* make any logical check, you should consider that postgres gives errors
* if you mix different types (ex. numeric and text) or different dimensions
* in a multidim array.
*
* #param array $set PHP array
*
* #return string Array in postgres syntax
*/
function to_pg_array($set) {
if (is_null($set) || !is_array($set)) {
return 'NULL';
}
// can be called with a scalar or array
settype($set, 'array');
$result = array();
foreach ($set as $t) {
// Element is array : recursion
if (is_array($t)) {
$result[] = to_pg_array($t);
}
else {
// PHP NULL
if (is_null($t)) {
$result[] = 'NULL';
}
// PHP TRUE::boolean
elseif (is_bool($t) && $t == TRUE) {
$result[] = 'TRUE';
}
// PHP FALSE::boolean
elseif (is_bool($t) && $t == FALSE) {
$result[] = 'FALSE';
}
// Other scalar value
else {
// Escape
$t = pg_escape_string($t);
// quote only non-numeric values
if (!is_numeric($t)) {
$t = '\'' . $t . '\'';
}
$result[] = $t;
}
}
}
return 'ARRAY[' . implode(",", $result) . ']'; // format
}
This is the same as mstefano80's answer, but more human-readable, universal and modern (at least for me):
<?php
class Sql
{
/**
* Convert PHP-array to SQL-array
* https://stackoverflow.com/questions/5631387/php-array-to-postgres-array
*
* #param array $data
* #return string
*/
public static function toArray(array $data, $escape = 'pg_escape_string')
{
$result = [];
foreach ($data as $element) {
if (is_array($element)) {
$result[] = static::toArray($element, $escape);
} elseif ($element === null) {
$result[] = 'NULL';
} elseif ($element === true) {
$result[] = 'TRUE';
} elseif ($element === false) {
$result[] = 'FALSE';
} elseif (is_numeric($element)) {
$result[] = $element;
} elseif (is_string($element)) {
$result[] = "'" . $escape($element) . "'";
} else {
throw new \InvalidArgumentException("Unsupported array item");
}
}
return sprintf('ARRAY[%s]', implode(',', $result));
}
}
Tests:
<?php
use Sql;
class SqlTest extends \PHPUnit_Framework_TestCase
{
public function testToArray()
{
$this->assertSame("ARRAY['foo','bar']", Sql::toArray(['foo', 'bar']));
$this->assertSame("ARRAY[1,2]", Sql::toArray([1, 2]));
$this->assertSame("ARRAY[1,2]", Sql::toArray(['1', '2']));
$this->assertSame("ARRAY['foo\\\"bar','bar\'foo']", Sql::toArray(['foo"bar', 'bar\'foo'], function($str){
return addslashes($str);
}));
$this->assertSame("ARRAY[ARRAY['foo\\\"bar'],ARRAY['bar\'foo']]", Sql::toArray([['foo"bar'], ['bar\'foo']], function($str){
return addslashes($str);
}));
}
}
When using PHP's json_encode to encode an array as a JSON string, is there any way at all to prevent the function from quoting specific values in the returned string? The reason I ask is because I need javascript to interpret certain values in the object as actual variable names, for example the name of an existing javascript function.
My end goal is to use the outputted json as the configuration object for an ExtJS Menu component, so the fact that everything gets quoted prevents me from successfully setting such properties as "handler" (click event handler function) of the child items arrays.
What we do is (and that's what Zend_Json::encode() does too), is to use a special marker class that encapsulates Javascript expressions in a special class. The encoding then walks recursively through our array-to-be-encoded, replaces all marker instances with some string. After using the built-in json_encode() we simply do a string replace to replace each special string with the __toString() value of the respective marker instance.
You can either use Zend_Json directly (if that's possible) or check how they do it and adapt the code to your needs.
Bill's function almost worked, it just needed the is_assoc() function added.
But while I was sorting this out, I cleaned it up a bit. This seems to work quite well for me:
<?php
/**
* JSObject class.
*/
class JSObject {
var $jsexp = 'JSEXP:';
/**
* is_assoc function.
*
* Determines whether or not the object is an associative array
*
* #access public
* #param mixed $arr
* #return boolean
*/
function is_assoc($arr) {
return (is_array($arr) && count(array_filter(array_keys($arr),'is_string')) == count($arr));
}
/**
* Encode object
*
* Encodes the object as a json string, parsing out items that were flagged as objects so that they are not wrapped in double quotes.
*
* #param array $properties
* #return string
*/
function encode($properties = array()) {
$is_assoc = $this->is_assoc($properties);
$enc_left = $is_assoc ? '{' : '[';
$enc_right = $is_assoc ? '}' : ']';
$outputArray = array();
foreach ($properties as $prop => $value) {
if ((is_array($value) && !empty($value)) || (is_string($value) && strlen(trim(str_replace($this->jsexp, '', $value))) > 0) || is_int($value) || is_float($value) || is_bool($value)) {
$output = (is_string($prop)) ? $prop.': ' : '';
if (is_array($value)) {
$output .= $this->encode($value);
}
else if (is_string($value)) {
$output .= (substr($value, 0, strlen($this->jsexp)) == $this->jsexp) ? substr($value, strlen($this->jsexp)) : json_encode($value);
}
else {
$output .= json_encode($value);
}
$outputArray[] = $output;
}
}
$fullOutput = implode(', ', $outputArray);
return $enc_left . $fullOutput . $enc_right;
}
/**
* JS expression
*
* Prefixes a string with the JS expression flag
* Strings with this flag will not be quoted by encode() so they are evaluated as expressions
*
* #param string $str
* #return string
*/
function js($str) {
return $this->jsexp.$str;
}
}
No, json_encode can't do that. You need to construct your JS expression by hand then:
$json = "{'special':" . json_encode($string) . " + js_var,"
. "'value': 123}";
(Try to still use json_encode for fixed value parts, like in above example.)
The json_encode function does not provide any functionality for controlling the quotes. The quotes are also necessary for JavaScript to properly form the object on the JavaScript side.
In order to use the returned value to construct an object on the JavaScript side, use the json_encoded string to set flags in your association.
For example:
json_encode( array( "click_handler"=> "FOO" ) );
JavaScript side in the AJAX:
if( json.click_handler == "FOO" ) {
json.click_handler = Your_Handler;
}
After these steps you can pass your object off somewhere.
my quickfix was this:
$myobject->withquotes = 'mystring';
$myobject->withoutquotes = '##noquote## mystring ##noquote##';
and later
str_replace(array('"##noquote## ', ' ##noquote##"'), '', json_encode($myobject))
result is something like this
{"withquotes":"mystring","withoutquotes":mystring}
This is what I ended up doing, which is pretty close to what Stefan suggested above I think:
class JSObject
{
var $jsexp = 'JSEXP:';
/**
* Encode object
*
*
* #param array $properties
* #return string
*/
function encode($properties=array())
{
$output = '';
$enc_left = $this->is_assoc($properties) ? '{' : '[';
$enc_right = ($enc_left == '{') ? '}' : ']';
foreach($properties as $prop => $value)
{
//map 'true' and 'false' string values to their boolean equivalent
if($value === 'true') { $value = true; }
if($value === 'false') { $value = false; }
if((is_array($value) && !empty($value)) || (is_string($value) && strlen(trim(str_replace($this->jsexp, '', $value))) > 0) || is_int($value) || is_float($value) || is_bool($value))
{
$output .= (is_string($prop)) ? $prop.': ' : '';
if(is_array($value))
{
$output .= $this->encode($value);
}
else if(is_string($value))
{
$output .= (substr($value, 0, strlen($this->jsexp)) == $this->jsexp) ? substr($value, strlen($this->jsexp)) : '\''.$value.'\'';
}
else if(is_bool($value))
{
$output .= ($value ? 'true' : 'false');
}
else
{
$output .= $value;
}
$output .= ',';
}
}
$output = rtrim($output, ',');
return $enc_left.$output.$enc_right;
}
/**
* JS expression
*
* Prefixes a string with the JS expression flag
* Strings with this flag will not be quoted by encode() so they are evaluated as expressions
*
* #param string $str
* #return string
*/
function js($str)
{
return $this->jsexp.$str;
}
}
Following the lead of Stefan Gehrig I put this rough little class together. Example below. One must remember to use the serialize method if one has used the mark method, otherwise the markers will persist in the final json.
class json_extended {
public static function mark_for_preservation($str) {
return 'OINK' . $str . 'OINK'; // now the oinks will be next to the double quotes
}
public static function serialize($stuff) {
$json = json_encode($stuff);
$json = str_replace(array('"OINK', 'OINK"'), '', $json);
return $json;
}
}
$js_arguments['submitHandler'] = json_extended::mark_for_preservation('handle_submit');
<script>
$("form").validate(<?=json_extended::serialize($js_arguments)?>);
// produces: $("form").validate({"submitHandler":handle_submit});
function handle_submit() {alert( 'Yay, pigs!'); }
</script>
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;
}
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;
}