在准备好的语句上调用PDOStatement :: execute()时,有没有办法使原始SQL字符串执行?对于调试目的,这将非常有用。
$stmt = $pdo->prepare($query); /* ... */ echo $stmt->fullQuery;
。它通过扩展PDOStatement类来工作,因此与PDO API允许的一样优雅。
在准备好的语句上调用PDOStatement :: execute()时,有没有办法使原始SQL字符串执行?对于调试目的,这将非常有用。
$stmt = $pdo->prepare($query); /* ... */ echo $stmt->fullQuery;
。它通过扩展PDOStatement类来工作,因此与PDO API允许的一样优雅。
Answers:
我假设您的意思是您想要最终的SQL查询,并将参数值插入其中。我知道这对调试很有用,但是它不是准备好的语句的工作方式。参数不会与客户端上的预处理语句组合在一起,因此PDO绝对不能访问与其参数组合在一起的查询字符串。
当您执行prepare()时,SQL语句将发送到数据库服务器,而当您执行execute()时,将分别发送参数。MySQL的常规查询日志的确显示了最终SQL,其中包含execute()之后插入的值。以下是我的一般查询日志的摘录。我从mysql CLI(不是从PDO)运行查询,但是原理是相同的。
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
如果您设置PDO属性PDO :: ATTR_EMULATE_PREPARES,您也可以得到想要的东西。在这种模式下,PDO将参数插值到SQL查询中,并在您执行execute()时发送整个查询。 这不是一个真正的准备好的查询。 通过在execute()之前将变量插值到SQL字符串中,可以避免准备查询的好处。
来自@afilina的评论:
不,执行期间文本SQL查询不会与参数组合。因此,PDO没有任何东西可以向您显示。
在内部,如果使用PDO :: ATTR_EMULATE_PREPARES,则PDO会在执行准备和执行之前复制SQL查询并向其中插入参数值。但是PDO不会公开此修改后的SQL查询。
PDOStatement对象具有属性$ queryString,但这仅在PDOStatement的构造函数中设置,并且在用参数重写查询时不会更新该属性。
对于PDO来说,要求他们公开重写的查询是合理的功能请求。但是,除非您使用PDO :: ATTR_EMULATE_PREPARES,否则即使那样也不会给您“完整”的查询。
这就是为什么我显示了上面使用MySQL服务器的常规查询日志的解决方法的原因,因为在这种情况下,即使是带有参数占位符的准备好的查询也会在服务器上重写,并将参数值回填到查询字符串中。但这仅在日志记录期间完成,而不在查询执行期间完成。
/**
* 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;
}
strtr()
:更快,更简单,同样的结果。strtr($query, $params);
$key
a string
和not $value
?我想念什么吗?我问这的原因是因为此输出,第二个参数未视为字符串:string(115) "INSERT INTO tokens (token_type, token_hash, user_id) VALUES ('resetpassword', hzFs5RLMpKwTeShTjP9AkTA2jtxXls86, 1);"
我修改了方法,使其包括处理诸如WHERE IN(?)之类的语句的数组输出。
更新:只是添加了对NULL值的检查并复制了$ params,因此实际的$ param值不会被修改。
bigwebguy做得好,谢谢!
/**
* 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;
}
$values = $params;
代替$values = array()
。
is_array
检查上方:if (is_string($value)) $values[$key] = "'" . $value . "'";
$values = $params;
$values_limit = []; $words_repeated = array_count_values(str_word_count($sql, 1, ':_'));
第一个如果的foreach添加这里面$values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
中的foreach而这第一个else $values_limit = [];
使用foreach循环$值再次到了preg_replaceisset($values_limit[$key])
if (is_array($values)) { foreach ($values as $key => $val) { if (isset($values_limit[$key])) { $sql = preg_replace(['/:'.$key.'/'], [$val], $sql, $values_limit[$key], $count); } } unset($key, $val); } else { $sql = preg_replace($keys, $values, $sql, 1, $count); }
可能有点晚了,但现在有 PDOStatement::debugDumpParams
将准备好的语句包含的信息直接转储到输出中。它将提供使用中的SQL查询,使用的参数数量(参数),参数列表及其名称,类型(参数类型)(整数),键名称或位置以及查询中的位置(如果由PDO驱动程序支持,否则为-1)。
您可以在官方php文档中找到更多信息
例:
<?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();
?>
echo '<pre>'; $sth->debugDumpParams(); echo '</pre>';
一种解决方案是自愿在查询中放入错误并打印错误消息:
//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();
}
标准输出:
SQLSTATE [42000]:语法错误或访问冲突:[...] 在第1行的“ ELECT * FROM Person WHERE age = 18”附近
重要的是要注意,它仅打印查询的前80个字符。
迈克(Mike)在代码中添加了更多内容-遍历值以添加单引号
/**
* 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具有公共属性$ queryString。它应该是您想要的。
我刚刚注意到PDOStatement有一个未记录的方法debugDumpParams(),您可能还想看看。
您可以扩展PDOStatement类以捕获有界变量并将其存储以供以后使用。然后可以添加2种方法,一种用于变量清理(debugBindedVariables),另一种用于打印带有那些变量的查询(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);
}
}
然后,您可以使用此继承的类调试目的。
$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());
导致
SELECT用户FROM用户WHERE用户='user_test'
数组([:test] => user_test)
我花了很多时间研究这种情况,以满足自己的需要。这个和其他几个SO线程对我有很大帮助,所以我想分享自己的想法。
尽管在故障排除时可以访问插值查询字符串是一个很大的好处,但我们希望仅维护某些查询的日志(因此,为此目的使用数据库日志并不理想)。我们还希望能够在任何给定时间使用日志来重新创建表的条件,因此,我们需要确保插值的字符串已正确转义。最后,我们希望将此功能扩展到整个代码库,而必须尽可能少地重写(截止日期,市场营销等;您知道怎么回事)。
我的解决方案是扩展默认PDOStatement对象的功能以缓存参数化的值(或引用),并在执行该语句时,使用PDO对象的功能在将参数重新注入查询时正确地转义参数。串。然后,我们可以绑定到语句对象的execute方法,并记录当时执行的实际查询(或至少尽可能忠实于复制)。
就像我说的,我们不想修改整个代码库来添加此功能,所以我们覆盖了PDOStatement对象的默认值bindParam()
和bindValue()
方法,对绑定的数据进行了缓存,然后调用parent::bindParam()
或parent ::bindValue()
。这使我们现有的代码库可以继续正常运行。
最后,当execute()
调用该方法时,我们执行插值并将结果字符串作为新属性提供E_PDOStatement->fullQuery
。可以将其输出以查看查询,或例如将其写入日志文件。
该扩展以及安装和配置说明可在github上获得:
https://github.com/noahheck/E_PDOStatement
免责声明:
显然,正如我提到的,我写了这个扩展名。因为它是在这里许多线程的帮助下开发的,所以我想在这里发布我的解决方案,以防其他人像我一样遇到这些线程。
您可以使用 sprintf(str_replace('?', '"%s"', $sql), ...$params);
这是一个例子:
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";
}
请注意,这仅适用于PHP> = 5.6
我知道这个问题有点老了,但是,自从很久以前我就在使用这段代码(我使用了@ chris-go的响应),现在,这些代码已在PHP 7.2中过时了
我会后的这些代码的更新版本(信贷的主要代码是从@bigwebguy,@mike和@克里斯-走,他们都回答了这个问题的):
/**
* 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;
}
请注意,代码上的更改是在array_walk()函数上,用匿名函数代替了create_function。这使这些好的代码段可以正常工作并与PHP 7.2兼容(并希望将来的版本也可以)。
有点相关...如果您只是想清理特定的变量,可以使用PDO :: quote。例如,如果您受限于CakePHP之类的有限框架,则要搜索多个部分LIKE条件:
$pdo = $this->getDataSource()->getConnection();
$results = $this->find('all', array(
'conditions' => array(
'Model.name LIKE ' . $pdo->quote("%{$keyword1}%"),
'Model.name LIKE ' . $pdo->quote("%{$keyword2}%"),
),
);
在您使用“重用”绑定值之前,Mike的答案一直很好。
例如:
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)
迈克的答案只能代替第一个:search,而不能代替第二个。
因此,我重写了他的答案以使用可以正确重用的多个参数。
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;
}
preg_replace对我不起作用,并且binding_超过9时,binding_1和binding_10被替换为str_replace(将0留在后面),所以我向后进行了替换:
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;
}
希望有人觉得它有用。
我需要在绑定参数之后记录完整的查询字符串,所以这是我的代码中的一部分。希望对每个有相同问题的人有用。
/**
*
* @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);
}