web-dev-qa-db-fra.com

Obtention d'une chaîne de requête SQL brute à partir d'instructions préparées par PDO

Existe-t-il un moyen d’exécuter la chaîne SQL brute lors de l’appel de PDOStatement :: execute () sur une instruction préparée? Cela serait extrêmement utile pour le débogage.

116
Wilco

Je suppose que vous voulez dire que vous voulez la requête SQL finale, avec les valeurs de paramètre interpolées. Je comprends que cela serait utile pour le débogage, mais ce n'est pas ainsi que fonctionnent les instructions préparées. Les paramètres ne sont pas combinés avec une instruction préparée côté client. Par conséquent, PDO ne devrait jamais avoir accès à la chaîne de requête combinée avec ses paramètres.

L'instruction SQL est envoyée au serveur de base de données lorsque vous effectuez prepare (), et les paramètres sont envoyés séparément lorsque vous exécutez execute (). Le journal de requête général de MySQL affiche le code SQL final avec les valeurs interpolées après l'exécution (). Vous trouverez ci-dessous un extrait de mon journal de requête général. J'ai exécuté les requêtes à partir de la CLI mysql, pas de PDO, mais le principe est le même.

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

Vous pouvez également obtenir ce que vous voulez si vous définissez l'attribut PDO PDO :: ATTR_EMULATE_PREPARES. Dans ce mode, PDO interpole les paramètres dans la requête SQL et envoie la requête entière lorsque vous exécutez (). Ce n'est pas une vraie requête préparée. Vous allez contourner les avantages des requêtes préparées en interpolant les variables dans la chaîne SQL avant execute ().


Commentaire de @afilina:

Non, la requête SQL textuelle est not combinée aux paramètres lors de l'exécution. Il n'y a donc rien que PDO puisse vous montrer.

En interne, si vous utilisez PDO :: ATTR_EMULATE_PREPARES, PDO crée une copie de la requête SQL et y interpole les valeurs de paramètre avant d'effectuer la préparation et l'exécution. Mais PDO n'expose pas cette requête SQL modifiée. 

L'objet PDOStatement a la propriété $ queryString, mais celle-ci n'est définie que dans le constructeur de PDOStatement et n'est pas mise à jour lorsque la requête est réécrite avec des paramètres.

Il serait raisonnable que PDO demande aux entités d'exposer la requête réécrite. Mais même cela ne vous donnerait pas la requête "complète" sauf si vous utilisez PDO :: ATTR_EMULATE_PREPARES.

C'est pourquoi je montre la solution de contournement ci-dessus consistant à utiliser le journal de requête général du serveur MySQL, car même dans ce cas, une requête préparée avec des paramètres de substitution est réécrite sur le serveur, les valeurs des paramètres étant insérées dans la chaîne de requête. Mais cela n'est fait que pendant la journalisation, pas pendant l'exécution de la requête.

100
Bill Karwin
/**
 * 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;
}
100
bigwebguy

J'ai modifié la méthode pour inclure la sortie de traitement des tableaux pour des instructions telles que WHERE IN (?). 

UPDATE: Nous venons d'ajouter une vérification de la valeur NULL et des paramètres $ dupliqués afin que les valeurs réelles des paramètres ne soient pas modifiées. 

Grand travail bigwebguy et merci!

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

PDOStatement a une propriété publique $ queryString. Ça devrait être ce que tu veux.

Je viens de remarquer que PDOStatement a une méthode non documentée debugDumpParams () que vous pouvez également consulter.

8
Glass Robot

Mike a ajouté un peu plus au code de Mike - parcourez les valeurs pour ajouter des guillemets 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;
}
7
Chris Go

Un peu en retard probablement mais maintenant il y a PDOStatement::debugDumpParams

Envoie les informations contenues dans une instruction préparée directement sur le résultat. Il fournira la requête SQL utilisée, le nombre de paramètres utilisés (Params), la liste des paramètres, avec leur nom, type (paramtype) sous forme d’entier, son nom de clé ou sa position, et le position dans la requête (si cela est pris en charge par le pilote PDO, sinon, ce sera -1).

Vous pouvez trouver plus d'informations sur le document officiel php

Exemple:

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

?>
6
Jimmy Kane

Vous pouvez étendre la classe PDOStatement pour capturer les variables liées et les stocker pour une utilisation ultérieure. Ensuite, 2 méthodes peuvent être ajoutées, une pour la vérification de variables (debugBindedVariables) et une autre pour imprimer la requête avec ces 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);
  }
}

Et vous pouvez ensuite utiliser cette classe héritée pour le débogage des utilisateurs.

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

Résultant en

SELECT utilisateur FROM utilisateurs WHERE user = 'user_test' 

Tableau ( [: Test] => test_utilisateur )

4
Otamay

J'ai passé beaucoup de temps à rechercher cette situation pour mes propres besoins. Ceci et plusieurs autres SO fils m'ont beaucoup aidé, alors je voulais partager ce que j'ai proposé.

Bien que l'accès à la chaîne de requête interpolée soit un avantage significatif lors du dépannage, nous voulions pouvoir conserver un journal de certaines requêtes uniquement (par conséquent, l'utilisation des journaux de base de données à cette fin n'était pas idéale). Nous voulions également pouvoir utiliser les journaux pour recréer l'état des tables à tout moment. Nous devions donc nous assurer que les chaînes interpolées étaient correctement échappées. Enfin, nous voulions étendre cette fonctionnalité à l’ensemble de notre code base, qui devait en réécrire le moins possible (délais, marketing, etc., vous savez comment.).

Ma solution consistait à étendre les fonctionnalités de l'objet PDOStatement par défaut pour mettre en cache les valeurs paramétrées (ou références). Lorsque l'instruction est exécutée, utilisez les fonctionnalités de l'objet PDO pour échapper correctement aux paramètres lorsqu'ils sont réinjectés dans la requête chaîne. Nous pourrions ensuite nous associer à la méthode execute de l'objet statement et enregistrer la requête exécutée à ce moment-là (ou au moins aussi fidèle que possible d'une reproduction)}.

Comme je l'ai dit, nous ne voulions pas modifier toute la base de code pour ajouter cette fonctionnalité. Nous avons donc écrasé les méthodes par défaut bindParam() et bindValue() de l'objet PDOStatement. puis appelez parent::bindParam() ou parent :: bindValue(). Cela a permis à notre base de code existante de continuer à fonctionner normalement.

Enfin, lorsque la méthode execute() est appelée, nous effectuons notre interpolation et fournissons la chaîne résultante sous la forme d'une nouvelle propriété E_PDOStatement->fullQuery. Cela peut être affiché pour afficher la requête ou, par exemple, écrit dans un fichier journal.

L'extension, ainsi que les instructions d'installation et de configuration, sont disponibles sur github:

https://github.com/noahheck/E_PDOStatement

AVERTISSEMENT:
Évidemment, comme je l’ai mentionné, j’ai écrit cette extension. Comme elle a été développée avec l'aide de nombreux threads ici, j'ai voulu poster ma solution ici au cas où quelqu'un d'autre rencontrerait ces threads, tout comme je l'ai fait.

3
myesain

Une solution consiste à mettre volontairement une erreur dans la requête et à imprimer le message de l'erreur:

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

Sortie standard:

SQLSTATE [42000]: Erreur de syntaxe ou violation d'accès: [...] près de 'ELECT * FROM Person WHERE age = 18' à la ligne 1

Il est important de noter que seuls les 80 premiers caractères de la requête sont imprimés.

2
JacopoStanchi

La propriété $ queryString mentionnée ne renverra probablement que la requête transmise, sans que les paramètres ne soient remplacés par leurs valeurs. Dans .Net, je demande à l'exécutant de la requête de faire une simple recherche sur les paramètres avec leurs valeurs fournies afin que le journal des erreurs puisse afficher les valeurs réelles utilisées pour la requête. Vous devriez pouvoir énumérer les paramètres en PHP et remplacer les paramètres par la valeur qui leur est assignée.

1
Kibbee

Vous pouvez utiliser sprintf(str_replace('?', '"%s"', $sql), ...$params);

Voici un exemple:

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

Notez que cela ne fonctionne que pour PHP> = 5.6.

0
kurdtpage

Je sais que cette question est un peu ancienne, mais j'utilise ce code depuis longtemps (j'ai utilisé la réponse de @ chris-go), et maintenant, ces codes sont obsolètes avec PHP 7.2

Je posterai une version mise à jour de ce code (le code principal provient de @bigwebguy , @mike et @ chris-go , toutes les réponses à cette question):

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

Notez que les modifications apportées au code concernent la fonction array_walk (), remplaçant create_function par une fonction anonyme. Cela rend ce bon morceau de code fonctionnel et compatible avec PHP 7.2 (et espérons les versions futures aussi).

0
Sakura Kinomoto

Quelque peu lié ... si vous essayez simplement de nettoyer une variable particulière, vous pouvez utiliser PDO :: quote . Par exemple, pour rechercher plusieurs conditions partielles LIKE si vous êtes bloqué avec un framework limité tel que CakePHP:

$pdo = $this->getDataSource()->getConnection();
$results = $this->find('all', array(
    'conditions' => array(
        'Model.name LIKE ' . $pdo->quote("%{$keyword1}%"),
        'Model.name LIKE ' . $pdo->quote("%{$keyword2}%"),
    ),
);
0
Synexis

J'ai besoin de me connecter une chaîne de requête complète après bind param, c'est donc un élément de mon code. J'espère que c'est utile pour tout le monde qui a le même problème.

/**
 * 
 * @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);
}
0
ducminh1903