準備済みステートメントで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で可能な限りエレガントです。
回答:
パラメータ値が補間された最後のSQLクエリが必要だと思います。これはデバッグに役立つことを理解していますが、準備されたステートメントが機能する方法ではありません。パラメータはクライアント側で準備されたステートメントと組み合わされないので、PDOはそのパラメータと組み合わされたクエリ文字列にアクセスできません。
SQLステートメントはprepare()を実行するとデータベースサーバーに送信され、パラメーターはexecute()を実行すると個別に送信されます。MySQLの一般的なクエリログには、execute()の後に補間された値を持つ最終SQLが表示されます。以下は、私の一般的なクエリログからの抜粋です。PDOではなくmysql CLIからクエリを実行しましたが、原理は同じです。
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にとって妥当な機能要求です。しかし、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
ているためstring
ではなく$value
?何か不足していますか?この出力の、2番目のパラメータは文字列として見られていないので、私はこれを尋ねる理由は次のとおりです。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);
とthis in first else in foreach $values_limit = [];
use foreach loop $ values to again preg_replace withisset($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クエリ、使用されているパラメーターの数(Params)、名前付きのパラメーターのリスト、整数としてのタイプ(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();
?>
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]:構文エラーまたはアクセス違反:[...]の近くに'ELECT *人FROM WHERE年齢= 18'行1で
クエリの最初の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つのメソッドを追加できます。1つは変数のサニタイズ用(debugBindedVariables)、もう1つはこれらの変数を使用してクエリを出力するため(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 user FROM users WHERE user = 'user_test'
配列([:test] => user_test)
私は自分のニーズのためにこの状況を調査するのにかなりの時間を費やしました。これと他のいくつかのSOスレッドが私を大いに助けたので、私が思いついたものを共有したいと思いました。
補間されたクエリ文字列にアクセスできることはトラブルシューティング中の大きなメリットですが、特定のクエリのみのログを維持できるようにしたいと考えました(したがって、この目的でデータベースログを使用することは理想的ではありませんでした)。また、ログを使用していつでもテーブルの状態を再現できるようにしたいと考えていたため、補間された文字列が適切にエスケープされるようにする必要がありました。最後に、この機能をコードベース全体に拡張して、可能な限り書き直さなくても済むようにしました(期限、マーケティングなど、ご存知でしょう)。
私の解決策は、デフォルトのPDOStatementオブジェクトの機能を拡張してパラメーター化された値(または参照)をキャッシュし、ステートメントが実行されたときにPDOオブジェクトの機能を使用して、パラメーターがクエリに注入されたときにパラメーターを適切にエスケープすることでしたストリング。次に、statementオブジェクトの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、@マイクとクリス・ゴー@、それらのすべては、この質問の答え):
/**
* 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}%"),
),
);
「再利用」バインド値を使用するまで、マイクの答えはうまくいきます。
例えば:
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を置き換えるだけで、2番目は置き換えられません。
したがって、私は彼の答えを書き直して、適切に再利用できる複数のパラメーターを処理できるようにします。
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;
}
誰かがそれが便利だと思うことを願っています。
bind paramの後に完全なクエリ文字列を記録する必要があるので、これは私のコードの一部です。願わくば、同じ問題を抱えているすべての人に役立つでしょう。
/**
*
* @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);
}