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

StackOverflow https://stackoverflow.com/questions/210564

Question

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.

Était-ce utile?

La solution

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 associée à 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 lancé 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 d'exécuter ().

Commentaire de @afilina:

Non, la requête SQL textuelle n'est pas combinée aux paramètres lors de l'exécution. Il n’ya 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.

Ce serait une demande de fonctionnalité raisonnable pour PDO de lui demander d'exposer la requête réécrite. Mais même cela ne vous donnerait pas le "complet" requête sauf si vous utilisez PDO :: ATTR_EMULATE_PREPARES.

C’est pourquoi j’indique 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 de paramètre é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.

Autres conseils

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

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 de la duplication des paramètres $ afin que les valeurs réelles des paramètres ne soient pas modifiées.

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

PDOStatement a une propriété publique $ queryString. Cela devrait être ce que vous voulez.

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

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

  

Dépose les informations contenues dans une déclaration 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).

Pour en savoir plus, consultez la documentation php officielle

.

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

?>

Ajoute un peu plus au code de Mike - balayez 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;
}

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

Bien que l’accès à la chaîne de requête interpolée soit un avantage important lors du dépannage, nous voulions pouvoir conserver un journal de certaines requêtes seulement (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 avaient été correctement échappées. Enfin, nous voulions étendre cette fonctionnalité à l’ensemble de notre base de code, qui devait en réécrire le moins possible (délais, marketing, etc.).

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. à la chaîne de requête. 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 la base de code complète pour ajouter cette fonctionnalité. Nous avons donc remplacé les méthodes par défaut bindParam () et bindValue () . de l’objet PDOStatement, mettez en cache les données liées, 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 à l’aide de nombreux fils de discussion ici, j’ai voulu poster ici ma solution au cas où quelqu'un d’autre tomberait dessus, tout comme moi.

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

Ensuite, vous pouvez 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ésultat dans

  

SÉLECTIONNER l'utilisateur FROM utilisateurs WHERE user = 'test_utilisateur'

     

Tableau (       [: test] = > test_utilisateur   )

Une solution consiste à insérer volontairement une erreur dans la requête et à imprimer le message d'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.

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

Un peu en rapport ... si vous essayez simplement de nettoyer une variable particulière, vous pouvez utiliser PDO :: devis . 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}%"),
    ),
);

Je dois enregistrer la chaîne de requête complète après le paramètre bind afin que ce soit une partie 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);
}

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

Je publierai 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 ces bons morceaux de code fonctionnels et compatibles avec PHP 7.2 (et espérons également les versions futures).

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

La réponse de Mike fonctionne correctement jusqu'à ce que vous utilisiez l'option "Réutiliser". valeur de liaison.
Par exemple:

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 réponse de Mike ne peut remplacer que la première: la recherche, mais pas la seconde.
Donc, je réécris sa réponse pour travailler avec plusieurs paramètres qui peuvent être réutilisés correctement.

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 n'a pas fonctionné pour moi et lorsque binding_ a dépassé 9 ans, binding_1 et binding_10 ont été remplacés par str_replace (en laissant le 0 derrière), j'ai donc effectué les remplacements à l'envers:

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;

}

J'espère que quelqu'un le trouvera utile.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top