Obtendo a sequência de consultas SQL bruta a partir de instruções preparadas pelo PDO

130

Existe uma maneira de obter a seqüência SQL bruta executada ao chamar PDOStatement :: execute () em uma instrução preparada? Para fins de depuração, isso seria extremamente útil.

Wilco
fonte
1
Para PHP> = 5.1, dê uma olhada em php.net/manual/en/pdostatement.debugdumpparams.php
Mawg diz para reinstalar Monica em
1
Verifique a função de uma linha pdo-debug .
Sliq
A maneira mais limpa que encontrei é a biblioteca E_PDOStatement . Você acabou de fazer $stmt = $pdo->prepare($query); /* ... */ echo $stmt->fullQuery;. Ele funciona estendendo a classe PDOStatement , portanto, é tão elegante quanto a API do PDO permite.
ComFreek

Respostas:

110

Suponho que você queira dizer a consulta SQL final, com valores de parâmetros interpolados nela. Entendo que isso seria útil para depuração, mas não é assim que as instruções preparadas funcionam. Os parâmetros não são combinados com uma instrução preparada no lado do cliente; portanto, o PDO nunca deve ter acesso à sequência de consultas combinada com seus parâmetros.

A instrução SQL é enviada ao servidor de banco de dados quando você prepara () e os parâmetros são enviados separadamente quando você executa (). O log de consultas gerais do MySQL mostra o SQL final com valores interpolados após você executar (). Abaixo está um trecho do meu log de consultas gerais. Eu executei as consultas da CLI do mysql, não da DOP, mas o princípio é o mesmo.

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

Você também pode obter o que deseja se definir o atributo PDO PDO :: ATTR_EMULATE_PREPARES. Nesse modo, o PDO interpola parâmetros na consulta SQL e envia a consulta inteira quando você executa (). Esta não é uma consulta preparada verdadeira. Você contornará os benefícios das consultas preparadas interpolando variáveis ​​na string SQL antes de executar ().


Re comentário de @afilina:

Não, a consulta SQL textual não é combinada com os parâmetros durante a execução. Portanto, não há nada para a DOP mostrar.

Internamente, se você usar PDO :: ATTR_EMULATE_PREPARES, o PDO fará uma cópia da consulta SQL e interpolará os valores dos parâmetros nela antes de preparar e executar. Mas o PDO não expõe essa consulta SQL modificada.

O objeto PDOStatement possui uma propriedade $ queryString, mas isso é definido apenas no construtor da PDOStatement e não é atualizado quando a consulta é reescrita com parâmetros.

Seria uma solicitação de recurso razoável para a DOP solicitar que eles expusessem a consulta reescrita. Mas mesmo isso não forneceria a consulta "completa", a menos que você use PDO :: ATTR_EMULATE_PREPARES.

É por isso que mostro a solução alternativa acima ao usar o log de consultas gerais do servidor MySQL, porque, neste caso, mesmo uma consulta preparada com espaços reservados para parâmetros é reescrita no servidor, com os valores dos parâmetros preenchidos na cadeia de consulta. Mas isso é feito apenas durante o log, não durante a execução da consulta.

Bill Karwin
fonte
10
E como você obtém a consulta do furo quando PDO :: ATTR_EMULATE_PREPARES está definido como TRUE?
Yasen Zhelev
2
@Yasen Zhelev: Se o PDO estiver emulando as preparações, ele interpolará os valores dos parâmetros na consulta antes de prepará-la. Portanto, o MySQL nunca vê a versão da consulta com espaços reservados para parâmetros. O MySQL registra apenas a consulta completa.
Bill Karwin
2
@ Bill: 'Os parâmetros não são combinados com uma declaração preparada no lado do cliente' - espere - mas eles combinam no lado do servidor? Ou como o mysql insere valores no banco de dados?
Stann
1
@afilina, não, você não pode. Veja minha explicação acima.
Bill Karwin
3
Uau, um voto negativo? Por favor, não atire no mensageiro. Estou apenas descrevendo como isso funciona.
Bill Karwin
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
fonte
6
por que não usar strtr(): mais rápido, mais simples, mesmos resultados. strtr($query, $params);
Tony Chiboucas
Qual é o uso para isso?
Só queria dar uma passada e agradecer também, estava fora de toda uma aula extra por isso, que agora removi em favor disso, pois é pequena e brilhante :). Então maldita útil para debbuging todas as consultas de um aplicativo está fazendo em cada página, registando-los: D
NaughtySquid
Vi essa função e fiquei muito feliz, embora, algo que eu não entendo, por que você verifica se $keyé um stringe não $value? Estou esquecendo de algo? A razão pela qual pergunto isso é por causa dessa saída, o segundo parâmetro não é visto como uma string: #string(115) "INSERT INTO tokens (token_type, token_hash, user_id) VALUES ('resetpassword', hzFs5RLMpKwTeShTjP9AkTA2jtxXls86, 1);"
Kerwin Sneijders
1
Este é um bom começo, mas falha se o valor de um $ param incluir um ponto de interrogação ("?").
chickenchilli
32

Modifiquei o método para incluir manipulação de saída de matrizes para instruções como WHERE IN (?).

UPDATE: Acabei de adicionar um valor NULL e duplicados $ params, para que os valores reais de $ param não sejam modificados.

Grande trabalho bigwebguy e obrigado!

/**
 * 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;
}
Mike
fonte
2
Eu acho que você tem que fazer em $values = $params;vez de $values = array().
testando
Outro pequeno pedaço que está faltando aqui são as cordas. Para capturá-los, coloque isso acima da is_arrayverificação:if (is_string($value)) $values[$key] = "'" . $value . "'";
treeface
Este é apenas o valor de ligação limitado apenas uma vez em preg_replace. adicione esta linha depois $values = $params; $values_limit = []; $words_repeated = array_count_values(str_word_count($sql, 1, ':_')); adicione-a dentro primeiro se for foreach $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);e isso primeiro em foreach $values_limit = [];use foreach loop $ values ​​novamente para preg_replace withisset($values_limit[$key])
vee
por exemplo, loop $ 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); }
vee
12

Um pouco tarde, provavelmente, mas agora há PDOStatement::debugDumpParams

Despeja as informações contidas em uma declaração preparada diretamente na saída. Ele fornecerá a consulta SQL em uso, o número de parâmetros usados ​​(Params), a lista de parâmetros, com nome, tipo (paramtype) como um número inteiro, nome da chave ou posição e posição na consulta (se esta é suportado pelo driver PDO; caso contrário, será -1).

Você pode encontrar mais informações sobre os documentos oficiais do php

Exemplo:

<?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();

?>
Jimmy Kane
fonte
e para melhor legibilidade:echo '<pre>'; $sth->debugDumpParams(); echo '</pre>';
SandroMarques
10

Uma solução é colocar voluntariamente um erro na consulta e imprimir a mensagem do erro:

//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();
}

Saída padrão:

SQLSTATE [42000]: Erro de sintaxe ou violação de acesso: [...] próximo a 'ELECT * FROM Pessoa WHERE idade = 18' na linha 1

É importante observar que ele imprime apenas os primeiros 80 caracteres da consulta.

JacopoStanchi
fonte
Não sei por que isso foi rebaixado. É simples e funciona. Funciona rápido. Muito mais rápido do que ativar o logon, procurando a linha certa no log, desativando o log e limpando os arquivos de log.
Bojan Hrnkas
@BojanHrnkas o tamanho da amostra de erro é muito limitado. Para uma consulta tão simples, é mais fácil substituir um espaço reservado por uma variável manualmente. E esse método só funciona se você ativar a emulação.
Seu senso comum
9

Adicionado um pouco mais ao código por Mike - passe os valores para adicionar aspas simples

/**
 * 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;
}
Chris Go
fonte
1
Muito útil, eu fiz algumas modificações para substituir o bindParam função do PDOStatement classe e validar se o valor é uma string ou inteiro com o DOP: Parâmetros valores.
Sergio Flores
onde podemos ver isso?
Mawg diz que restabelece Monica
8

PDOStatement possui uma propriedade pública $ queryString. Deve ser o que você quer.

Acabei de perceber que o PDOStatement tem um método não documentado debugDumpParams () que você também pode querer olhar.

Robô de vidro
fonte
1
Os debugDumpParams não está documentado php.net/manual/en/pdostatement.debugdumpparams.php
mloskot
Não. $ queryString não mostra os valores dos parâmetros incluídos.
Andreas
5

Você pode estender a classe PDOStatement para capturar as variáveis ​​delimitadas e armazená-las para uso posterior. Em seguida, dois métodos podem ser adicionados, um para limpeza de variáveis ​​(debugBindedVariables) e outro para imprimir a consulta com essas variáveis ​​(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);
  }
}

E então você pode usar essa classe herdada para depuração de 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());

Resultando em

SELECT user FROM users WHERE user = 'user_test'

Matriz ([: teste] => teste_do_usuário)

Otamay
fonte
4

Passei muito tempo pesquisando essa situação para minhas próprias necessidades. Esse e vários outros tópicos do SO me ajudaram bastante, então eu queria compartilhar o que descobri.

Embora ter acesso à cadeia de consulta interpolada seja um benefício significativo durante a solução de problemas, queríamos manter um log de apenas determinadas consultas (portanto, não era ideal usar os logs do banco de dados para esse fim). Também queríamos poder usar os logs para recriar a condição das tabelas a qualquer momento, portanto, precisávamos garantir que as seqüências de caracteres interpoladas fossem escapadas corretamente. Finalmente, queríamos estender essa funcionalidade para toda a nossa base de código, tendo que reescrever o mínimo possível (prazos, marketing e outros; você sabe como é).

Minha solução foi estender a funcionalidade do objeto PDOStatement padrão para armazenar em cache os valores parametrizados (ou referências) e, quando a instrução for executada, use a funcionalidade do objeto PDO para escapar adequadamente dos parâmetros quando eles forem injetados novamente na consulta corda. Poderíamos então ligar-nos para executar o método do objeto de instrução e registrar a consulta real que foi executada naquele momento ( ou pelo menos o mais fiel possível de uma reprodução) .

Como eu disse, não queremos modificar toda a base de código para adicionar essa funcionalidade, portanto, substituímos o padrão bindParam()e os bindValue()métodos do objeto PDOStatement, fazemos o cache dos dados vinculados e depois chamamos parent::bindParam()ou parent :: bindValue(). Isso permitiu que nossa base de códigos existente continuasse funcionando normalmente.

Finalmente, quando o execute()método é chamado, executamos nossa interpolação e fornecemos a sequência resultante como uma nova propriedade E_PDOStatement->fullQuery. Isso pode ser produzido para visualizar a consulta ou, por exemplo, gravada em um arquivo de log.

A extensão, junto com as instruções de instalação e configuração, estão disponíveis no github:

https://github.com/noahheck/E_PDOStatement

AVISO LEGAL :
Obviamente, como mencionei, escrevi esta extensão. Como ele foi desenvolvido com a ajuda de vários threads aqui, eu queria postar minha solução aqui, caso mais alguém os encontrasse, como eu fiz.

myesain
fonte
Obrigado por compartilhar. Sem voto positivo porque resposta muito longa com muito pouco código
T30
1

A propriedade $ queryString mencionada provavelmente retornará apenas a consulta passada, sem que os parâmetros sejam substituídos por seus valores. No .Net, solicito que a parte catch do executor de consulta faça uma pesquisa simples substituindo os parâmetros pelos valores fornecidos, para que o log de erros possa mostrar valores reais que estavam sendo usados ​​para a consulta. Você deve poder enumerar os parâmetros no PHP e substituí-los pelo valor atribuído.

Kibbee
fonte
1

Você pode usar sprintf(str_replace('?', '"%s"', $sql), ...$params);

Aqui está um exemplo:

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";
}

Observe que isso funciona apenas para PHP> = 5.6

kurdtpage
fonte
0

Sei que essa pergunta é um pouco antiga, mas estou usando esse código há muito tempo (usei a resposta de @ chris-go) e agora esses códigos estão obsoletos no PHP 7.2

Vou postar uma versão atualizada desse código (o crédito para o código principal é de @bigwebguy , @mike e @ chris-go , todos eles respostas desta pergunta):

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

Observe que a alteração no código está na função array_walk (), substituindo create_function por uma função anônima. Isso torna esses bons trechos de código funcionais e compatíveis com o PHP 7.2 (e esperamos versões futuras também).

Sakura Kinomoto
fonte
-1

Um pouco relacionado ... se você está apenas tentando higienizar uma variável específica, pode usar o PDO :: quote . Por exemplo, para pesquisar várias condições LIKE parciais, se você estiver preso a uma estrutura limitada como o 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
fonte
-1

A resposta de Mike está funcionando bem até você usar o valor de ligação "reutilizar".
Por exemplo:

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)

A resposta do Mike só pode substituir o primeiro: pesquise, mas não o segundo.
Então, reescrevi sua resposta para trabalhar com vários parâmetros que podem ser reutilizados corretamente.

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;
}
vee
fonte
-1

preg_replace não funcionou para mim e quando binding_ tinha mais de 9 anos, binding_1 e binding_10 foram substituídos por str_replace (deixando o 0 para trás), então fiz as substituições ao contrário:

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;

}

Espero que alguém ache útil.

Markos F
fonte
-1

Eu preciso registrar a seqüência de caracteres de consulta completa após ligar o parâmetro para que este seja um pedaço do meu código. Espero que seja útil para todos que têm o mesmo problema.

/**
 * 
 * @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
fonte