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

130

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

Wilco
источник
1
Для PHP> = 5.1 взгляните на php.net/manual/en/pdostatement.debugdumpparams.php
Mawg говорит восстановить Монику
1
Проверьте однострочную функцию pdo-debug .
Sliq
Самый чистый способ, который я нашел, - это библиотека E_PDOStatement . Просто сделай $stmt = $pdo->prepare($query); /* ... */ echo $stmt->fullQuery;. Он работает, расширяя класс PDOStatement , следовательно, настолько элегантен, насколько позволяет PDO API.
ComFreek

Ответы:

110

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


Ответ от @afilina:

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

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

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

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

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

Билл Карвин
источник
10
И как получить запрос дыр, если для PDO :: ATTR_EMULATE_PREPARES установлено значение TRUE?
Ясен Желев
2
@Yasen Zhelev: Если PDO эмулирует подготовку, он будет интерполировать значения параметров в запрос до того, как подготовит запрос. Таким образом, MySQL никогда не видит версию запроса с заполнителями параметров. MySQL регистрирует только полный запрос.
Билл Карвин
2
@ Bill: «Параметры не объединяются с подготовленным оператором на стороне клиента» - подождите - но объединяются ли они на стороне сервера? Или как mysql вставляет значения в БД?
Stann
1
@afilina, нет, нельзя. Смотрите мое объяснение выше.
Билл Карвин
3
Вау, голос против? Пожалуйста, не стреляйте в посыльного. Я просто описываю, как это работает.
Билл Карвин
107
/**
 * 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;
}
bigwebguy
источник
6
почему бы просто не использовать strtr(): быстрее, проще, те же результаты. strtr($query, $params);
Тони Шибукас
Какая польза от этого?
Я просто хотел зайти и поблагодарить меня, я был вне целого дополнительного класса, за который я сейчас удалился, потому что он крошечный и блестящий :). Так чертовски полезно для отладки всех запросов, которые приложение делает на каждой странице, регистрируя их: D
NaughtySquid
Увидел эту функцию, и это сделало меня очень счастливым, хотя, что-то, что я не понимаю, почему вы проверяете, что вы $key, stringа не $value? Я что-то упускаю? Причина, по которой я спрашиваю об этом, заключается в том, что из-за этих выходных данных второй параметр не string(115) "INSERT INTO tokens (token_type, token_hash, user_id) VALUES ('resetpassword', hzFs5RLMpKwTeShTjP9AkTA2jtxXls86, 1);"
Kerwin Sneijders
1
Это хорошее начало, но оно терпит неудачу, если значение самого $ param включает знак вопроса («?»).
курица
32

Я изменил метод, чтобы включить обработку вывода массивов для операторов вроде 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;
}
Майк
источник
2
Я думаю, что вы должны сделать $values = $params;вместо $values = array().
тестирование
Еще один маленький кусочек, который здесь пропущен - это струны. Чтобы поймать их, поставьте над is_arrayпроверкой:if (is_string($value)) $values[$key] = "'" . $value . "'";
treeface
Это ограниченное значение привязки только один раз в preg_replace. добавьте эту строку после $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);и в первую очередь в foreach, $values_limit = [];снова используйте цикл $ foreach $ values ​​в preg_replace сisset($values_limit[$key])
vee
например, цикл $ values. 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); }
Ви
12

Немного поздно, наверное, но сейчас есть 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>';
SandroMarques
10

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

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

JacopoStanchi
источник
Я не знаю, почему это было понижено. Это просто и работает. Работает быстро. Гораздо быстрее, чем включить журнал, поиск правой строки в журнале, затем отключение журнала, а затем очистка файлов журнала.
Боян Хрнкас
@ BojanHrnkas длина образца ошибки очень ограничена. Для такого простого запроса проще заменить заполнитель переменной просто вручную. И этот метод работает, только если вы включите эмуляцию.
Ваш здравый смысл
9

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

/**
 * 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;
}
Крис Го
источник
1
Очень полезно, я сделал некоторые модификации , чтобы переопределить bindParam функцию PDOStatement класса и проверки , если значение является строкой или целым числом с PDO: Params значения.
Серхио Флорес
1
где мы можем это увидеть?
Mawg просит восстановить Монику
8

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

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

Стеклянный робот
источник
1
DebugDumpParams не документирован php.net/manual/en/pdostatement.debugdumpparams.php
mloskot
Нет. $ queryString не показывает включенные значения параметров.
Андреас
5

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

В результате чего

ВЫБРАТЬ пользователя ИЗ пользователей WHERE user = 'user_test'

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

Otamay
источник
4

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

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

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

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

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

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

https://github.com/noahheck/E_PDOStatement

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

myesain
источник
Спасибо, что поделился. Нет голосов за, потому что слишком длинный ответ и слишком мало кода
T30
1

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

Kibbee
источник
1

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

kurdtpage
источник
0

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

Сакура Киномото
источник
-1

В некоторой степени связано ... если вы просто пытаетесь очистить конкретную переменную, вы можете использовать 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}%"),
    ),
);
Synexis
источник
-1

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

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;
}
ви
источник
-1

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;

}

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

Маркос Ф
источник
-1

Мне нужно записать полную строку запроса после параметра привязки, так что это часть моего кода. Надеюсь, это полезно для всех, у кого есть одна и та же проблема.

/**
 * 
 * @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);
}
ducminh1903
источник