준비된 명령문에서 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 문은 준비 ()를 수행 할 때 데이터베이스 서버로 전송되고 매개 변수는 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 () 전에 변수를 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
을 되 string
하지 $value
? 뭔가 빠졌습니까? 내가 묻는 이유는이 출력 때문에 두 번째 매개 변수는 문자열로 표시되지 않습니다.string(115) "INSERT INTO tokens (token_type, token_hash, user_id) VALUES ('resetpassword', hzFs5RLMpKwTeShTjP9AkTA2jtxXls86, 1);"
WHERE IN (?)과 같은 명령문에 대한 배열 출력 처리를 포함하도록 메소드를 수정했습니다.
업데이트 : 방금 NULL 값 검사와 $ params 복제를 추가하여 실제 $ param 값이 수정되지 않았습니다.
대단한 일 webwebguy와 감사합니다!
/**
* 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 에있는 경우이 라인을 먼저 추가 한 후 foreach에 $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
있는 다른 $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 쿼리, 사용 된 매개 변수 수 (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] : 구문 오류 또는 액세스 위반 : 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 클래스를 확장하여 경계 변수를 캡처하고 나중에 사용할 수 있도록 저장할 수 있습니다. 그런 다음 두 가지 메소드를 추가 할 수 있습니다. 하나는 변수 살균 (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);
}
}
그런 다음 상속 된 클래스를 사용하여 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());
를 야기하는
어디에서 사용자를 선택하십시오. user = 'user_test'
배열 ([: test] => user_test)
나는 내 자신의 필요를 위해이 상황을 연구하는데 많은 시간을 보냈다. 이것과 다른 몇 가지 SO 스레드가 크게 도움이되었으므로 내가 생각해 낸 것을 공유하고 싶었습니다.
보간 된 쿼리 문자열에 액세스하면 문제를 해결하는 동안 큰 이점을 얻을 수 있지만 특정 쿼리 만 로그를 유지할 수 있기를 원했습니다 (따라서이 목적으로 데이터베이스 로그를 사용하는 것이 이상적이지 않았습니다). 또한 주어진 시간에 로그를 사용하여 테이블의 조건을 재현 할 수 있기를 원했기 때문에 보간 된 문자열이 올바르게 이스케이프되도록해야했습니다. 마지막으로, 우리는이 기능을 전체 코드베이스까지 확장하여 가능한 적은 부분 만 다시 작성해야했습니다 (마감일, 마케팅 등).
내 솔루션은 기본 PDOStatement 객체의 기능을 확장하여 매개 변수가 지정된 값 (또는 참조)을 캐시하고 명령문이 실행될 때 PDO 객체의 기능을 사용하여 매개 변수가 쿼리에 다시 주입 될 때 매개 변수를 올바르게 이스케이프 처리합니다. 끈. 그런 다음 명령문 객체의 메소드를 실행하고 그 시점에 실행 된 실제 쿼리 ( 또는 가능한 한 충실한 재생산)를 기록 할 수 있습니다 .
내가 말했듯이, 우리는이 기능을 추가하기 위해 전체 코드베이스를 수정하고 싶지 않기 때문에 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 및 @ 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와 호환하게 만듭니다 (그리고 미래 버전도 희망합니다).
다소 관련이 있습니다 ... 특정 변수를 위생 처리하려는 경우 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)
Mike의 답변은 첫 번째 검색 만 대체 할 수 있지만 두 번째 검색은 대체 할 수 없습니다.
따라서 올바르게 재사용 할 수있는 여러 매개 변수로 작업하기 위해 그의 대답을 다시 씁니다.
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);
}