Получение необработанной строки запроса SQL из подготовленных операторов PDO - PullRequest
120 голосов
/ 17 октября 2008

Есть ли способ получить необработанную строку SQL, выполняемую при вызове PDOStatement :: execute () для подготовленного оператора? В целях отладки это было бы чрезвычайно полезно.

Ответы [ 15 ]

104 голосов
/ 04 сентября 2009
/**
 * 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;
}
104 голосов
/ 17 октября 2008

Полагаю, вы имеете в виду, что вам нужен окончательный SQL-запрос со значениями параметров, вставленными в него. Я понимаю, что это было бы полезно для отладки, но это не то, как работают подготовленные операторы. Параметры не объединяются с подготовленным оператором на стороне клиента, поэтому PDO никогда не должен иметь доступа к строке запроса в сочетании с ее параметрами.

Оператор SQL отправляется на сервер базы данных, когда вы выполняете prepare (), а параметры отправляются отдельно, когда вы выполняете execute (). Общий журнал запросов 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 ().


Комментарий от @afilina:

Нет, текстовый SQL-запрос не в сочетании с параметрами во время выполнения. Так что PDO ничего вам не покажет.

Внутренне, если вы используете PDO :: ATTR_EMULATE_PREPARES, PDO создает копию запроса SQL и интерполирует в него значения параметров перед выполнением подготовки и выполнения. Но PDO не предоставляет этот модифицированный SQL-запрос.

Объект PDOStatement имеет свойство $ queryString, но оно устанавливается только в конструкторе для PDOStatement и не обновляется, когда запрос перезаписывается с параметрами.

Было бы разумным запросом функции для PDO попросить их предоставить переписанный запрос. Но даже это не даст вам «завершенного» запроса, если вы не используете PDO :: ATTR_EMULATE_PREPARES.

Вот почему я показываю вышеописанный обходной путь использования общего журнала запросов сервера MySQL, поскольку в этом случае даже подготовленный запрос с заполнителями параметров перезаписывается на сервере, а значения параметров возвращаются в строку запроса. Но это делается только во время регистрации, а не во время выполнения запроса.

28 голосов
/ 06 декабря 2011

Я изменил метод, чтобы включить обработку вывода массивов для операторов типа 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;
}
8 голосов
/ 23 июля 2014

Возможно, немного поздно, но сейчас есть 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();

?>
8 голосов
/ 17 октября 2008

PDOStatement имеет открытое свойство $ queryString. Это должно быть то, что вы хотите.

Я только что заметил, что PDOStatement имеет недокументированный метод debugDumpParams (), который вы также можете посмотреть.

7 голосов
/ 18 августа 2012

Добавил немного больше кода от Майка - пройдитесь по значениям, чтобы добавить одинарные кавычки

/**
 * 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;
}
4 голосов
/ 09 февраля 2017

Вы можете расширить класс 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());

В результате

ВЫБЕРИТЕ пользователя ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ user = 'user_test'

Array ( [: test] => user_test )

4 голосов
/ 18 мая 2014

Я потратил много времени на изучение этой ситуации для собственных нужд. Этот и несколько других SO-потоков мне очень помогли, поэтому я хотел поделиться тем, что у меня получилось.

Хотя доступ к интерполированной строке запроса является существенным преимуществом при устранении неполадок, мы хотели иметь возможность вести журнал только определенных запросов (поэтому использование журналов базы данных для этой цели не было идеальным). Мы также хотели иметь возможность использовать журналы для воссоздания состояния таблиц в любой момент времени, поэтому нам нужно было убедиться, что интерполированные строки были экранированы правильно. Наконец, мы хотели расширить эту функциональность до всей нашей кодовой базы, чтобы переписать ее как можно меньше (сроки, маркетинг и тому подобное; вы знаете, как это).

Мое решение состояло в том, чтобы расширить функциональные возможности объекта PDOStatement по умолчанию для кэширования параметризованных значений (или ссылок), и при выполнении инструкции используйте функциональные возможности объекта PDO для правильного экранирования параметров, когда они вводятся обратно в к строке запроса. Затем мы могли бы присоединиться к методу выполнения объекта оператора и зарегистрировать фактический запрос, который был выполнен в это время ( или, по крайней мере, настолько точно, насколько это возможно при воспроизведении) .

Как я уже сказал, мы не хотели изменять всю кодовую базу, чтобы добавить эту функциональность, поэтому мы перезаписываем методы bindParam() и bindValue() по умолчанию для объекта PDOStatement, делаем наше кэширование связанных данных, затем Позвоните parent::bindParam() или родитель :: bindValue(). Это позволило нашей существующей кодовой базе продолжать функционировать как обычно.

Наконец, когда вызывается метод execute(), мы выполняем нашу интерполяцию и предоставляем результирующую строку в качестве нового свойства E_PDOStatement->fullQuery. Это может быть вывод для просмотра запроса или, например, запись в файл журнала.

Расширение вместе с инструкциями по установке и настройке доступно на github:

https://github.com/noahheck/E_PDOStatement

ОТКАЗ
Очевидно, как я уже упоминал, я написал это расширение. Поскольку это было разработано с помощью многих потоков здесь, я хотел опубликовать свое решение здесь на случай, если кто-то еще сталкивался с этими потоками, так же, как я сделал.

3 голосов
/ 07 марта 2018

Решение состоит в том, чтобы добровольно внести ошибку в запрос и напечатать сообщение об ошибке:

//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]: синтаксическая ошибка или нарушение прав доступа: [...] около 'ВЫБРАТЬ * ОТ ЛИЦА, ГДЕ возраст = 18' в строке 1

Важно отметить, что он печатает только первые 80 символов запроса.

1 голос
/ 17 октября 2008

Упомянутое свойство $ queryString, вероятно, будет возвращать только переданный запрос, без замены параметров их значениями. В .Net у меня есть часть catch моего исполнителя запросов, которая выполняет простую поисковую замену параметров с их значениями, которые были предоставлены, чтобы журнал ошибок мог показывать фактические значения, которые использовались для запроса. Вы должны иметь возможность перечислять параметры в PHP и заменять параметры на присвоенные им значения.

...