Obteniendo una cadena de consulta SQL sin procesar de declaraciones preparadas PDO

130

¿Hay alguna manera de ejecutar la cadena SQL sin procesar al llamar a PDOStatement :: execute () en una instrucción preparada? Para propósitos de depuración esto sería extremadamente útil.

Wilco
fuente
1
Para PHP> = 5.1, eche un vistazo a php.net/manual/en/pdostatement.debugdumpparams.php
Mawg dice que reinstale a Monica el
1
Verifique la función de una línea pdo-debug .
Sliq
La forma más limpia que encontré es la biblioteca E_PDOStatement . Que acaba de hacer $stmt = $pdo->prepare($query); /* ... */ echo $stmt->fullQuery;. Funciona extendiendo la clase PDOStatement , por lo tanto, es tan elegante como lo permite la API PDO.
ComFreek

Respuestas:

110

Supongo que quiere decir que desea la consulta SQL final, con valores de parámetros interpolados en ella. Entiendo que esto sería útil para la depuración, pero no es la forma en que funcionan las declaraciones preparadas. Los parámetros no se combinan con una declaración preparada en el lado del cliente, por lo que PDO nunca debe tener acceso a la cadena de consulta combinada con sus parámetros.

La instrucción SQL se envía al servidor de la base de datos cuando prepara (), y los parámetros se envían por separado cuando ejecuta (). El registro de consultas generales de MySQL muestra el SQL final con valores interpolados después de ejecutar (). A continuación se muestra un extracto de mi registro de consultas generales. Ejecuté las consultas desde la CLI de mysql, no desde PDO, pero el principio es el mismo.

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

También puede obtener lo que desea si establece el atributo PDO PDO :: ATTR_EMULATE_PREPARES. En este modo, PDO interpola parámetros en la consulta SQL y envía la consulta completa cuando ejecuta (). Esta no es una verdadera consulta preparada. Evitará los beneficios de las consultas preparadas interpolando variables en la cadena SQL antes de ejecutar ().


Re comentario de @afilina:

No, la consulta textual de SQL no se combina con los parámetros durante la ejecución. Así que no hay nada que PDO pueda mostrarle.

Internamente, si usa PDO :: ATTR_EMULATE_PREPARES, PDO hace una copia de la consulta SQL e interpola los valores de los parámetros antes de realizar la preparación y ejecución. Pero PDO no expone esta consulta SQL modificada.

El objeto PDOStatement tiene una propiedad $ queryString, pero esto se establece solo en el constructor para PDOStatement, y no se actualiza cuando la consulta se reescribe con parámetros.

Sería una solicitud de función razonable para PDO pedirles que expongan la consulta reescrita. Pero incluso eso no le daría la consulta "completa" a menos que use PDO :: ATTR_EMULATE_PREPARES.

Es por eso que muestro la solución anterior de usar el registro de consultas generales del servidor MySQL, porque en este caso incluso una consulta preparada con marcadores de posición de parámetros se reescribe en el servidor, con los valores de los parámetros rellenados en la cadena de consulta. Pero esto solo se hace durante el registro, no durante la ejecución de la consulta.

Bill Karwin
fuente
10
¿Y cómo se obtiene la consulta de agujero cuando PDO :: ATTR_EMULATE_PREPARES está configurado en TRUE?
Yasen Zhelev
2
@Yasen Zhelev: si PDO está emulando preparaciones, interpolará los valores de los parámetros en la consulta antes de prepararla. Por lo tanto, MySQL nunca ve la versión de la consulta con marcadores de posición de parámetros. MySQL solo registra la consulta completa.
Bill Karwin
2
@ Bill: 'Los parámetros no se combinan con una declaración preparada en el lado del cliente', espere, pero ¿se combinan en el lado del servidor? ¿O cómo mysql inserta valores en DB?
Stann
1
@afilina, no, no puedes. Vea mi explicación arriba.
Bill Karwin
3
Wow, un voto negativo? Por favor, no le dispares al mensajero. Solo estoy describiendo cómo 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
fuente
66
por qué no solo usar strtr(): más rápido, más simple, los mismos resultados. strtr($query, $params);
Tony Chiboucas
¿Cuál es el uso para esto?
Solo quería pasar y ofrecer mi agradecimiento también, estaba fuera de toda una clase extra por esto que ahora he eliminado a favor de esto, ya que es pequeño y brillante :). Muy útil para depurar todas las consultas que una aplicación está haciendo en cada página registrándolas: D
NaughtySquid
Vi esta función y me hizo muy feliz, aunque, algo que no entiendo, ¿por qué verificas $keyque sea un stringy no $value? ¿Me estoy perdiendo de algo? La razón por la que pregunto esto es por esta salida, el segundo parámetro no se ve como una cadena:string(115) "INSERT INTO tokens (token_type, token_hash, user_id) VALUES ('resetpassword', hzFs5RLMpKwTeShTjP9AkTA2jtxXls86, 1);"
Kerwin Sneijders
1
Este es un buen comienzo, pero falla si el valor de $ param incluye un signo de interrogación ("?").
chickenchilli
32

Modifiqué el método para incluir el manejo de la salida de matrices para declaraciones como WHERE IN (?).

ACTUALIZACIÓN: Acabo de agregar un cheque para el valor NULO y $ params duplicados para que los valores reales de $ param no se modifiquen.

Gran trabajo bigwebguy y gracias!

/**
 * 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;
}
Miguel
fuente
2
Creo que tienes que hacer en $values = $params;lugar de $values = array().
prueba el
Otra pequeña pieza que se pierde aquí son las cuerdas. Para capturarlos, ponga esto encima del is_arraycheque:if (is_string($value)) $values[$key] = "'" . $value . "'";
treeface
Esto es solo un valor de enlace limitado a solo una vez en preg_replace. agregue esta línea después de $values = $params; $values_limit = []; $words_repeated = array_count_values(str_word_count($sql, 1, ':_')); agregar esto dentro primero si está en foreach $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);y esto primero en otra cosa en foreach, $values_limit = [];use foreach loop $ values ​​nuevamente para preg_replace withisset($values_limit[$key])
vee
por ejemplo, bucle $ valores. 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

Un poco tarde probablemente pero ahora hay PDOStatement::debugDumpParams

Vuelca la información contenida en una declaración preparada directamente en la salida. Proporcionará la consulta SQL en uso, el número de parámetros utilizados (Parámetros), la lista de parámetros, con su nombre, tipo (parámetro) como un entero, su nombre o posición clave y la posición en la consulta (si esto es compatible con el controlador PDO, de lo contrario, será -1).

Puedes encontrar más en los documentos oficiales de php

Ejemplo:

<?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
fuente
y para una mejor legibilidad:echo '<pre>'; $sth->debugDumpParams(); echo '</pre>';
SandroMarques
10

Una solución es poner voluntariamente un error en la consulta e imprimir el mensaje del error:

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

Salida estándar:

SQLSTATE [42000]: error de sintaxis o infracción de acceso: [...] cerca de 'ELECT * FROM Person WHERE age = 18' en la línea 1

Es importante tener en cuenta que solo imprime los primeros 80 caracteres de la consulta.

JacopoStanchi
fuente
No sé por qué esto fue rechazado. Es simple y funciona. Funciona rapido. Mucho más rápido que encender el registro, buscar la línea correcta en el registro, luego deshabilitar el registro y luego limpiar los archivos de registro.
Bojan Hrnkas
@BojanHrnkas la longitud de la muestra de error es muy limitada. Para una consulta tan simple, es más fácil reemplazar un marcador de posición con una variable solo manualmente. Y este método solo funciona si habilita la emulación.
Su sentido común
9

Mike agregó un poco más al código: recorra los valores para agregar comillas 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
fuente
1
Muy útilmente, hice algunas modificaciones para anular la función bindParam de la clase PDOStatement y validar si el valor es una cadena o un entero con los valores PDO: PARAMS .
Sergio Flores
¿Dónde podemos ver eso?
Mawg dice que reinstalar a Monica el
8

PDOStatement tiene una propiedad pública $ queryString. Debería ser lo que quieres.

Acabo de notar que PDOStatement tiene un método indocumentado debugDumpParams () que también es posible que desee ver.

Robot de vidrio
fuente
1
DebugDumpParams no está documentado php.net/manual/en/pdostatement.debugdumpparams.php
mloskot
No $ queryString no muestra los valores de parámetro incluidos.
Andreas
5

Puede ampliar la clase PDOStatement para capturar las variables limitadas y almacenarlas para su uso posterior. Luego se pueden agregar 2 métodos, uno para la desinfección de variables (debugBindedVariables) y otro para imprimir la consulta con esas variables (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);
  }
}

Y luego puede usar esta clase heredada para depurar propósitos.

$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 en

SELECCIONAR usuario DESDE usuarios DONDE usuario = 'prueba_usuario'

Matriz ([: prueba] => prueba_usuario)

Otamay
fuente
4

Pasé mucho tiempo investigando esta situación para mis propias necesidades. Este y varios otros hilos SO me ayudaron mucho, así que quería compartir lo que se me ocurrió.

Si bien tener acceso a la cadena de consulta interpolada es un beneficio significativo durante la resolución de problemas, queríamos poder mantener un registro de solo ciertas consultas (por lo tanto, usar los registros de la base de datos para este propósito no era ideal). También queríamos poder usar los registros para recrear la condición de las tablas en cualquier momento dado, por lo tanto, necesitábamos asegurarnos de que las cadenas interpoladas se escaparan correctamente. Finalmente, queríamos extender esta funcionalidad a toda nuestra base de código teniendo que volver a escribir la menor cantidad posible (fechas límite, marketing y demás; ya sabes cómo es).

Mi solución fue extender la funcionalidad del objeto PDOStatement predeterminado para almacenar en caché los valores parametrizados (o referencias), y cuando se ejecuta la declaración, usar la funcionalidad del objeto PDO para escapar adecuadamente de los parámetros cuando se vuelven a inyectar en la consulta cuerda. Luego podríamos vincularnos para ejecutar el método del objeto de declaración y registrar la consulta real que se ejecutó en ese momento ( o al menos tan fiel a la reproducción como sea posible) .

Como dije, no queríamos modificar todo el código base para agregar esta funcionalidad, por lo que sobrescribimos el método predeterminado bindParam()y los bindValue()métodos del objeto PDOStatement, hacemos nuestro almacenamiento en caché de los datos enlazados, luego llamamos parent::bindParam()o parent :: bindValue(). Esto permitió que nuestra base de código existente continuara funcionando normalmente.

Finalmente, cuando execute()se llama al método, realizamos nuestra interpolación y proporcionamos la cadena resultante como una nueva propiedad E_PDOStatement->fullQuery. Esto puede enviarse para ver la consulta o, por ejemplo, escribirse en un archivo de registro.

La extensión, junto con las instrucciones de instalación y configuración, están disponibles en github:

https://github.com/noahheck/E_PDOStatement

DESCARGO DE RESPONSABILIDAD :
Obviamente, como mencioné, escribí esta extensión. Debido a que fue desarrollado con la ayuda de muchos hilos aquí, quería publicar mi solución aquí en caso de que alguien más se encuentre con estos hilos, tal como lo hice.

myesain
fuente
Gracias por compartir. Sin voto a favor porque la respuesta es demasiado larga con muy poco código
T30
1

La propiedad $ queryString mencionada probablemente solo devolverá la consulta pasada, sin que los parámetros se reemplacen con sus valores. En .Net, hago que la parte de captura de mi ejecutor de consultas haga una simple búsqueda de reemplazo en los parámetros con sus valores que se proporcionaron para que el registro de errores pueda mostrar los valores reales que se estaban utilizando para la consulta. Debería poder enumerar los parámetros en PHP y reemplazar los parámetros con su valor asignado.

Kibbee
fuente
1

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

Aquí hay un ejemplo:

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

Tenga en cuenta que esto solo funciona para PHP> = 5.6

kurdtpage
fuente
0

Sé que esta pregunta es un poco antigua, pero estoy usando este código desde hace mucho tiempo (he usado la respuesta de @ chris-go), y ahora, este código está obsoleto con PHP 7.2

Publicaré una versión actualizada de este código (el crédito para el código principal proviene de @bigwebguy , @mike y @ chris-go , todas ellas respuestas a esta pregunta):

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

Tenga en cuenta que los cambios en el código están en la función array_walk (), reemplazando create_function por una función anónima. Esto hace que este buen código sea funcional y compatible con PHP 7.2 (y espero versiones futuras también).

Sakura Kinomoto
fuente
-1

Algo relacionado ... si solo está tratando de desinfectar una variable en particular, puede usar PDO :: quote . Por ejemplo, para buscar múltiples condiciones parciales como si está atascado con un marco limitado como 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
fuente
-1

La respuesta de Mike funciona bien hasta que esté utilizando el valor de enlace "reutilizar".
Por ejemplo:

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)

La respuesta de Mike solo puede reemplazar primero: buscar pero no la segunda.
Entonces, reescribo su respuesta para trabajar con múltiples parámetros que pueden reutilizarse correctamente.

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
fuente
-1

preg_replace no funcionó para mí y cuando vinculante_ fue superior a 9, vinculante_1 y vinculante_10 fue reemplazado por str_replace (dejando el 0 atrás), así que hice los reemplazos al revés:

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 alguien lo encuentre útil.

Markos F
fuente
-1

Necesito registrar una cadena de consulta completa después de bind param, así que esta es una pieza en mi código. Espero que sea útil para todos los que tienen el mismo 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
fuente