Pregunta

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

¿Fue útil?

Solución

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 SQL textual es no combinada 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 de 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 te daría el " completo " consulta 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 realiza durante el registro, no durante la ejecución de la consulta.

Otros consejos

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

Modifiqué el método para incluir la salida de manejo 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;
}

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.

Probablemente un poco tarde, 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,   escriba (paramtype) como un entero, su nombre o posición clave, y el   posición en la consulta (si esto es compatible con el controlador PDO,   de lo contrario, será -1).

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

?>

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

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 a la cadena de consulta. 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 lo más fiel posible de una reproducción) .

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

Finalmente, cuando se llama al método execute () , 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.

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 user = 'user_test'

     

Matriz (       [: prueba] = > prueba_usuario   )

Una solución es poner voluntariamente un error en la consulta e imprimir el mensaje de 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.

La propiedad $ queryString mencionada probablemente solo devolverá la consulta pasada, sin los parámetros reemplazados por sus valores. En .Net, hago que la parte de captura del ejecutador de mi consulta realice 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.

Algo relacionado ... si solo está tratando de desinfectar una variable en particular, puede usar PDO :: cita . Por ejemplo, para buscar múltiples condiciones parciales de LIKE 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}%"),
    ),
);

Necesito registrar una cadena de consulta completa después de bind param, así que esta es una parte de 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);
}

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 es de @bigwebguy , @mike y @ chris-go , todas ellas respuestas de 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 el cambio en el código está 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).

Puede 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

La respuesta de Mike funciona bien hasta que esté utilizando la "reutilización" valor de enlace.
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;
}

preg_replace no funcionó para mí y cuando vinculante_ era mayor de 9, vinculante_1 y vinculante_10 fue reemplazado por str_replace (dejando atrás el 0), 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top