Есть ли способ получить необработанную строку SQL, выполняемую при вызове PDOStatement :: execute () в подготовленном заявлении? Для целей отладки это было бы чрезвычайно полезно.
Я предполагаю, что вы имеете в виду, что вы хотите получить окончательный запрос SQL с параметрами, интерполированными в него. Я понимаю, что это было бы полезно для отладки, но это не так, как работают подготовленные заявления. Параметры не объединены с подготовленным оператором на стороне клиента, поэтому PDO никогда не должен иметь доступ к строке запроса в сочетании с ее параметрами.
Оператор SQL отправляется на сервер базы данных при подготовке (), а параметры отправляются отдельно, когда вы выполняете (). Общий журнал запросов MySQL показывает окончательный SQL со значениями, интерполированными после выполнения (). Ниже приведен фрагмент моего основного журнала запросов. Я запускал запросы из CLI mysql, а не из 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-запрос и отправляет весь запрос при выполнении (). Это не настоящий подготовленный запрос. Вы обойдете преимущества подготовленных запросов путем интерполяции переменных в строку SQL перед execute ().
Re comment от @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; }
Я модифицировал метод, чтобы включить обработку выходных массивов для операторов типа WHERE IN (?).
UPDATE: просто добавлена проверка значения 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; }
PDOStatement имеет общедоступное свойство $ queryString. Это должно быть то, что вы хотите.
Я только заметил, что у PDOStatement есть недокументированный метод debugDumpParams (), который вы также можете посмотреть.
Добавлен немного больше кода Майка – пройдите значения, чтобы добавить одинарные кавычки
/** * 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::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(); ?>
Я потратил много времени на изучение этой ситуации для своих собственных нужд. Это и несколько других потоков SO очень помогли мне, поэтому я хотел поделиться тем, что я придумал.
Хотя доступ к интерполированной строке запроса является значительным преимуществом при устранении неполадок, мы хотели иметь возможность поддерживать журнал только определенных запросов (поэтому использование журналов базы данных для этой цели было не идеальным). Мы также хотели иметь возможность использовать журналы для воссоздания условий таблиц в любой момент времени, поэтому нам нужно было убедиться, что интерполированные строки были экранированы должным образом. Наконец, мы хотели расширить эту функциональность до всей нашей базы кода, чтобы переписать ее как можно меньше (сроки, маркетинг и т. Д., Вы знаете, как это работает).
Мое решение состояло в том, чтобы расширить функциональность объекта PDOStatement по умолчанию для кэширования параметризованных значений (или ссылок), и когда оператор выполняется, используйте функциональные возможности объекта PDO для правильного выхода из параметров при их вводе обратно в запрос строка. Затем мы могли бы связать, чтобы выполнить метод объекта statement и записать фактический запрос, который был выполнен в это время ( или, по крайней мере, как можно точнее воспроизведения) .
Как я уже сказал, мы не хотели изменять всю базу кода, чтобы добавить эту функциональность, поэтому мы перезаписываем bindParam()
и bindValue()
по умолчанию объекта PDOStatement, делаем наше кэширование связанных данных, а затем вызываем parent::bindParam()
или parent :: bindValue()
. Это позволило нашей существующей базе кода продолжать функционировать нормально.
Наконец, когда вызывается метод execute()
, мы выполняем нашу интерполяцию и предоставляем результирующую строку как новое свойство E_PDOStatement->fullQuery
. Это можно выводить для просмотра запроса или, например, для записи в файл журнала.
Расширение вместе с инструкциями по установке и настройке доступно на github:
https://github.com/noahheck/E_PDOStatement
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ :
Очевидно, как я уже упоминал, я написал это расширение. Поскольку он был разработан с помощью многих потоков здесь, я хотел опубликовать свое решение здесь, если кто-то еще сталкивается с этими потоками, как и я.
Вы можете расширить класс PDOStatement, чтобы захватить ограниченные переменные и сохранить их для последующего использования. Затем могут быть добавлены 2 метода: один для переменной sanitizing (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());
В результате чего
SELECT user FROM users WHERE user = 'user_test'
Массив ([: test] => user_test)
Указанное свойство $ queryString, вероятно, только вернет запрос, переданный без изменения параметров на их значения. В .Net у меня есть часть catch, выполняющая простой запрос на замену параметров, с их значениями, которые были предоставлены, чтобы журнал ошибок отображал фактические значения, которые использовались для запроса. Вы должны иметь возможность перечислять параметры в PHP и заменять параметры своим назначенным значением.
Немного связанный … если вы просто пытаетесь дезинфицировать определенную переменную, вы можете использовать PDO :: quote . Например, для поиска нескольких частичных условий LIKE, если вы придерживаетесь ограниченной структуры, такой как CakePHP:
$pdo = $this->getDataSource()->getConnection(); $results = $this->find('all', array( 'conditions' => array( 'Model.name LIKE ' . $pdo->quote("%{$keyword1}%"), 'Model.name LIKE ' . $pdo->quote("%{$keyword2}%"), ), );
Мне нужно записать полную строку запроса после параметра bind, чтобы это было частью моего кода. Надеюсь, это полезно для всех, у кого есть такая же проблема.
/** * * @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); }
Ответ Майка работает хорошо, пока вы не используете значение привязки «повторного использования».
Например:
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; }
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;
}
Надеюсь, кто-то сочтет это полезным.