在准备好的语句上调用PDOStatement :: execute()时,有没有办法使原始SQL字符串执行?对于调试目的,这将非常有用。

评论

对于PHP> = 5.1,请查看php.net/manual/en/pdostatement.debugdumpparams.php

检查单行功能pdo-debug。

我找到的最干净的方法是E_PDOStatement库。您只需执行$ stmt = $ pdo-> prepare($ query); / * ... * / echo $ stmt-> fullQuery;。它通过扩展PDOStatement类来工作,因此与PDO API允许的一样优雅。

#1 楼

我假设您的意思是您想要最终的SQL查询,并将参数值插入其中。我知道这对调试很有用,但是它不是准备好的语句的工作方式。参数不会在客户端与准备好的语句结合使用,因此PDO永远都不能访问与其参数组合在一起的查询字符串。

准备准备时,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 :: ATTR_EMULATE_PREPARES,PDO会复制SQL查询并在准备和执行之前内插参数值。但是PDO不会公开此修改后的SQL查询。

PDOStatement对象具有属性$ queryString,但这仅在PDOStatement的构造函数中设置,并且在用参数重写查询时不会更新。

它对于PDO来说,要求他们公开重写后的查询是一项合理的功能请求。但是,除非您使用PDO :: ATTR_EMULATE_PREPARES,否则即使那样也不会给您“完整”的查询。

这就是为什么我显示了上面使用MySQL服务器的一般查询日志的解决方法的原因,因为在这种情况下,即使是带有参数占位符的准备好的查询也会在服务器上重写,并将参数值回填到查询字符串中。但这仅在日志记录期间完成,而不是在查询执行期间完成。

评论


当PDO :: ATTR_EMULATE_PREPARES设置为TRUE时,如何获取孔查询?

–亚森·哲列夫(Yasen Zhelev)
2011年6月17日在11:54

@Yasen Zhelev:如果PDO正在模拟准备,那么它将在准备查询之前将参数值插入查询中。因此,MySQL永远不会看到带有参数占位符的查询版本。 MySQL仅记录完整查询。

– Bill Karwin
2011年6月17日17:19

@ Bill:“参数未在客户端与准备好的语句组合在一起”-等待-但是它们是否在服务器端组合?还是mysql如何将值插入数据库?

–斯坦
2011年7月21日在16:36

@afilina,不,你不能。请参阅上面的说明。

– Bill Karwin
2014-09-15 18:46

哇,不赞成投票吗?请不要开枪。我只是在描述它的工作原理。

– Bill Karwin
2014年9月15日20:13在

#2 楼

/**
 * 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);

–托尼·奇布卡斯(Tony Chiboucas)
2014年10月10日20:44

这有什么用?

–user4942382
16-2-26在19:48

我只是想停下来,同时也要感谢我,所以在这个额外的课堂之外,我已经删除了这个,因为它很小巧,才华横溢:)。因此,通过记录它们来调试应用程序在每个页面上所做的所有查询都是非常有用的:D

–NaughtySquid
17年5月3日,11:36

看到了这个函数,这让我感到非常高兴,尽管,我不了解这件事,为什么还要检查$ key是字符串而不是$ value?我想念什么吗?我问这的原因是由于此输出,第二个参数未视为字符串:string(115)“ INSERT INTO令牌(token_type,token_hash,user_id)VALUES('resetpassword',hzFs5RLMpKwTeShTjP9AkTA2jtxXls86、1);”

– Kerwin Sneijders
18 Sep 10'在0:34



这是一个很好的开始,但是如果$ param的值本身包含问号(“?”),它将失败。

–Chickenchilli
19年6月6日在4:22

#3 楼

我修改了方法,使其包括处理诸如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()。

–测试
2012年4月5日12:33



此处缺少的另一个小片段是字符串。为了捕获这些内容,请将其放在is_array检查上方:if(is_string($ value))$ values [$ key] =“'”。 $ value。 “'”;

–treeface
13年7月21日在2:17

在preg_replace中,仅将绑定值限制为一次。在$ 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 $ values_limit = []中;再次使用foreach循环$ values与isset($ values_limit [$ key])进行preg_replace

–vee
15年11月26日在6:27



例如循环$ values。 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); }

–vee
15年11月26日在6:28



这到处都是。

–乔尔·梅隆(Joel Mellon)
8月18日19:06

#4 楼

可能有点晚了,但现在有PDOStatement::debugDumpParams


直接将准备好的语句包含的信息转储到输出上。它将提供正在使用的SQL查询,使用的
参数数(Params),参数列表及其名称,整数的
type(paramtype),键名称或位置,以及查询中的
位置(如果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();

?>


评论


注意PHP错误#52384:PDOStatement :: debugDumpParams不会发出绑定参数值。

–主教
16年7月11日在15:44

为了更好的可读性:echo'
'; $ sth-> debugDumpParams();回显'
';

– SandroMarques
19-10-23在22:20



#5 楼

一种解决方案是在查询中自动添加错误并打印错误消息:

//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个字符。

评论


我不知道为什么这被否决了。它很简单,而且有效。它运作迅速。比打开日志,在日志中搜索右行,然后禁用日志,然后清理日志文件要快得多。

– Bojan Hrnkas
18-10-5在8:32

@BojanHrnkas错误样本的长度非常有限。对于这样一个简单的查询,只需手动用一个变量替换一个占位符就容易了。而且,只有启用仿真后,此方法才有效。

–您的常识
5月20日16:04

#6 楼

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类的bindParam函数,并使用PDO:PARAMS值验证该值是字符串还是整数。

–塞尔吉奥·弗洛雷斯(Sergio Flores)
13年4月23日在15:34

我们在哪里可以看到?

–莫格说要恢复莫妮卡
2014年6月3日,9:22

#7 楼

PDOStatement具有公共属性$ queryString。

我只是注意到PDOStatement有一个未记录的方法debugDumpParams(),您可能还想看看。

评论


没有记录debugDumpParams php.net/manual/en/pdostatement.debugdumpparams.php

–mloskot
2011年6月11日14:25

不。 $ queryString不显示包含的参数值。

–安德烈亚斯(Andreas)
16年4月26日在12:01

#8 楼

我花了很多时间来研究这种情况以满足自己的需求。该线程以及其他几个SO线程对我有很大帮助,所以我想分享一下我的想法。

虽然访问内插查询字符串在进行故障排除时是一个很大的好处,但我们希望只能维护某些查询的日志(因此,为此目的使用数据库日志并不理想)。我们还希望能够在任何给定时间使用日志来重新创建表的条件,因此,我们需要确保插值的字符串已正确转义。最后,我们希望将此功能扩展到我们的整个代码库,而必须尽可能少地重写(截止日期,市场营销等;您知道如何)。

我的解决方案是扩展默认PDOStatement对象的功能以缓存参数化的值(或引用),并且在执行该语句时,请使用PDO对象的功能在将参数重新注入查询字符串时正确地转义参数。然后,我们可以绑定到语句对象的执行方法,并记录当时执行的实际查询(或至少尽可能忠实地复制)。

我说过,我们没有不想修改整个代码库以添加此功能,因此我们覆盖PDOStatement对象的默认bindParam()bindValue()方法,对绑定的数据进行缓存,然后调用parent::bindParam()或parent :: bindValue()。这使我们现有的代码库可以继续正常运行。

最后,当调用execute()方法时,我们执行插值并提供结果字符串作为新属性E_PDOStatement->fullQuery。可以将其输出以查看查询,或例如将其写入日志文件。

该扩展名以及安装和配置说明可在github上找到:

https://github.com/noahheck/E_PDOStatement

免责声明:
很明显,正如我提到的,我写了这个扩展名。因为它是在这里许多线程的帮助下开发的,所以我想在这里发布我的解决方案,以防其他人遇到这些线程,就像我一样。

评论


感谢分享。没有投票,因为回答太长而代码太少

– T30
16/12/20在9:51



#9 楼

您可以扩展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());


导致


从用户中选择用户WHERE user ='user_test'

数组(
[:test] => user_test



#10 楼

我知道这个问题有点老了,但是,自从很久以前我就在使用这段代码(我使用了@ chris-go的回复),现在,这些代码已在PHP 7.2中过时了。

我将发布这些代码的更新版本(主要代码的信用来自@ bigwebguy,@ mike和@ chris-go,它们均回答了这个问题):

/**
 * 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兼容(并希望将来的版本也可以)。

#11 楼

提到的$ queryString属性可能只会返回传入的查询,而不会用参数的值替换参数。在.Net中,我让查询执行程序的catch部分对提供的参数进行简单的搜索替换,以提供错误值,以便错误日志可以显示用于查询的实际值。您应该能够枚举PHP中的参数,并将参数替换为其分配的值。

#12 楼

您可以使用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

#13 楼

有点相关...如果您只是想清理特定的变量,可以使用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}%"),
    ),
);


#14 楼

在使用“重用”绑定值之前,迈克的答案一直有效。
例如:

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)


迈克的答案只能首先替换:搜索而不是第二个。
因此,我重写了他的答案,以使用可以正确重用的多个参数。

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;
}


#15 楼

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;


}

希望有人发现它有用。

#16 楼

我需要在绑定参数之后记录完整的查询字符串,所以这是我的代码中的一部分。希望对每个有相同问题的帽子都有用。

/**
 * 
 * @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);
}