Получение необработанной строки SQL-запроса из подготовленных PDO инструкций

StackOverflow https://stackoverflow.com/questions/210564

Вопрос

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

Это было полезно?

Решение

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

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

Я модифицировал метод, чтобы включить обработку выходных данных массивов для операторов типа WHERE IN (?).

Обновить:Просто добавлена проверка на НУЛЕВОЕ значение и продублированы $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(), на который вы, возможно, также захотите взглянуть.

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

?>

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

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

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

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

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

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

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

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

https://github.com/noahheck/E_PDOStatement

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

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

Приводящий к

ВЫБЕРИТЕ пользователя ИЗ СПИСКА users, ГДЕ user = 'user_test'

Массив ( [:test] => user_test )

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

//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 символов запроса.

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

В некотором роде связанные...если вы просто пытаетесь очистить определенную переменную, вы можете использовать PDO::цитата.Например, для поиска нескольких условий partial 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);
}

Я знаю, что этот вопрос немного устарел, но я использую этот код уже давно (я использовал ответ от @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 (и, надеюсь, с будущими версиями тоже).

Вы можете использовать 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

Ответ Майка работает хорошо до тех пор, пока вы не используете значение привязки "повторно использовать".
Например:

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;

}

Надеюсь, кто-нибудь сочтет это полезным.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top