Domanda

Esiste un modo per eseguire la stringa SQL non elaborata quando si chiama PDOStatement :: execute () su un'istruzione preparata? Ai fini del debug questo sarebbe estremamente utile.

È stato utile?

Soluzione

Suppongo che tu voglia che desideri la query SQL finale, con i valori dei parametri interpolati in essa. Capisco che questo sarebbe utile per il debug, ma non è il modo in cui funzionano le istruzioni preparate. I parametri non sono combinati con un'istruzione preparata sul lato client, quindi PDO non dovrebbe mai avere accesso alla stringa di query combinata con i suoi parametri.

L'istruzione SQL viene inviata al server di database quando si prepara () e i parametri vengono inviati separatamente quando si esegue execute (). Il log delle query generali di MySQL mostra l'SQL finale con valori interpolati dopo aver eseguito (). Di seguito è riportato un estratto dal mio registro generale delle query. Ho eseguito le query dalla CLI mysql, non da DOP, ma il principio è lo stesso.

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

Puoi anche ottenere ciò che desideri se imposti l'attributo PDO PDO :: ATTR_EMULATE_PREPARES. In questa modalità, PDO interpola i parametri nella query SQL e invia l'intera query quando si esegue (). Questa non è una vera query preparata. Eviterai i vantaggi delle query preparate interpolando le variabili nella stringa SQL prima di execute ().


Ri commenta da @afilina:

No, la query SQL testuale è non combinata con i parametri durante l'esecuzione. Quindi non c'è niente che PDO ti mostri.

Internamente, se si utilizza PDO :: ATTR_EMULATE_PREPARES, PDO crea una copia della query SQL e interpola i valori dei parametri in essa prima di eseguire la preparazione ed esecuzione. Ma PDO non espone questa query SQL modificata.

L'oggetto PDOStatement ha una proprietà $ queryString, ma questa è impostata solo nel costruttore per PDOStatement e non viene aggiornata quando la query viene riscritta con parametri.

Sarebbe una ragionevole richiesta di funzionalità per PDO chiedere loro di esporre la query riscritta. Ma anche questo non ti darebbe il "completo" query a meno che non si usi PDO :: ATTR_EMULATE_PREPARES.

Questo è il motivo per cui mostro la soluzione sopra descritta sull'uso del registro delle query generale del server MySQL, perché in questo caso anche una query preparata con segnaposto dei parametri viene riscritta sul server, con i valori dei parametri riempiti nuovamente nella stringa della query. Ma questo viene fatto solo durante la registrazione, non durante l'esecuzione della query.

Altri suggerimenti

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

Ho modificato il metodo per includere la gestione dell'output di array per istruzioni come WHERE IN (?).

AGGIORNAMENTO: appena aggiunto verifica il valore NULL e duplicati $ params in modo che i valori effettivi $ param non vengano modificati.

Grande lavoro bigwebguy e grazie!

/**
 * 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 ha una proprietà pubblica $ queryString. Dovrebbe essere quello che vuoi.

Ho appena notato che PDOStatement ha un metodo non documentato debugDumpParams () che potresti anche voler guardare.

Probabilmente un po 'in ritardo ma ora c'è PDOStatement::debugDumpParams

  

Scarica le informazioni contenute in una dichiarazione preparata direttamente su   L'output. Fornirà la query SQL in uso, il numero di   parametri utilizzati (Params), l'elenco dei parametri, con il loro nome,   digitare (paramtype) come numero intero, il nome o la posizione della chiave e il   posizione nella query (se supportata dal driver PDO,   altrimenti, sarà -1).

Puoi trovare ulteriori informazioni sui documenti php ufficiali

Esempio:

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

?>

Aggiunto un po 'di più al codice di Mike - cammina i valori per aggiungere virgolette singole

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

Ho trascorso molto tempo a ricercare questa situazione per le mie esigenze. Questo e molti altri thread SO mi hanno aiutato moltissimo, quindi volevo condividere ciò che mi è venuto in mente.

Pur avendo accesso alla stringa di query interpolata è un vantaggio significativo durante la risoluzione dei problemi, volevamo essere in grado di mantenere un registro di solo alcune query (pertanto, utilizzare i registri del database per questo scopo non era l'ideale). Volevamo anche essere in grado di utilizzare i log per ricreare la condizione delle tabelle in qualsiasi momento, quindi, dovevamo assicurarci che le stringhe interpolate fossero sfuggite correttamente. Infine, volevamo estendere questa funzionalità a tutta la nostra base di codice, riscrivendone il meno possibile (scadenze, marketing e simili; sai com'è).

La mia soluzione era quella di estendere la funzionalità dell'oggetto PDOStatement predefinito per memorizzare nella cache i valori (o riferimenti) parametrizzati e, quando viene eseguita l'istruzione, utilizzare la funzionalità dell'oggetto PDO per sfuggire correttamente ai parametri quando vengono reimmessi in alla stringa di query. Potremmo quindi collegarci per eseguire il metodo dell'oggetto istruzione e registrare la query effettiva eseguita in quel momento ( o almeno il più fedele possibile di una riproduzione) .

Come ho detto, non volevamo modificare l'intera base di codice per aggiungere questa funzionalità, quindi sovrascriviamo i metodi predefiniti bindParam () e bindValue () dell'oggetto PDOStatement, esegui la memorizzazione nella cache dei dati associati, quindi chiama parent :: bindParam () o parent :: bindValue () . Ciò ha consentito alla nostra base di codice esistente di continuare a funzionare normalmente.

Alla fine, quando viene chiamato il metodo execute () , eseguiamo la nostra interpolazione e forniamo la stringa risultante come una nuova proprietà E_PDOStatement- > fullQuery . Questo può essere emesso per visualizzare la query o, ad esempio, scritto in un file di registro.

L'estensione, insieme alle istruzioni di installazione e configurazione, sono disponibili su github:

https://github.com/noahheck/E_PDOStatement

NEGAZIONE :
Ovviamente, come ho già detto, ho scritto questa estensione. Poiché è stato sviluppato con l'aiuto di molti thread qui, volevo pubblicare qui la mia soluzione nel caso in cui qualcuno incontrasse questi thread, proprio come ho fatto io.

È possibile estendere la classe PDOStatement per acquisire le variabili limitate e memorizzarle per un uso successivo. Quindi possono essere aggiunti 2 metodi, uno per la sanificazione delle variabili (debugBindedVariables) e un altro per stampare la query con quelle variabili (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 quindi puoi usare questa classe ereditata per il debug degli scopi.

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

Risultato in

  

SELEZIONA utente DA utenti DOVE utente = 'test_utente'

     

Array (       [: test] = > user_test   )

Una soluzione è inserire volontariamente un errore nella query e stampare il messaggio di errore:

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

Output standard:

  

SQLSTATE [42000]: errore di sintassi o violazione di accesso: [...] vicino a 'ELECT * FROM Person WHERE age = 18' alla riga 1

È importante notare che stampa solo i primi 80 caratteri della query.

La proprietà $ queryString menzionata probabilmente restituirà solo la query passata, senza i parametri sostituiti con i loro valori. In .Net, ho la parte catch del mio programma di esecuzione di query fare una semplice ricerca sostituire i parametri con i loro valori che è stato fornito in modo che il registro degli errori possa mostrare i valori effettivi utilizzati per la query. Dovresti essere in grado di enumerare i parametri in PHP e sostituirli con il loro valore assegnato.

In qualche modo correlato ... se stai solo cercando di disinfettare una particolare variabile puoi usare PDO :: citazione . Ad esempio, per cercare più condizioni LIKE parziali se sei bloccato con un framework limitato come CakePHP:

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

Devo registrare la stringa di query completa dopo il parametro bind, quindi questo è un pezzo nel mio codice. Spero che sia utile per tutti coloro che hanno lo stesso 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);
}

So che questa domanda è un po 'vecchia, ma sto usando questo codice da molto tempo fa (ho usato la risposta di @ chris-go) e ora questi codici sono obsoleti con PHP 7.2

Pubblicherò una versione aggiornata di questo codice (i crediti per il codice principale provengono da @bigwebguy , @mike e @ chris-go , tutte risposte a questa domanda):

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

Nota che le modifiche al codice sono sulla funzione array_walk (), che sostituisce create_function con una funzione anonima. Questo rende questo buon pezzo di codice funzionale e compatibile con PHP 7.2 (e spero anche le versioni future).

Puoi usare sprintf (str_replace ('?', '"% s "', $ sql), ... $ params);

Ecco un esempio:

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

Nota che funziona solo per PHP > = 5.6

La risposta di Mike funziona bene fino a quando non si utilizza il "riutilizzo" " valore di bind.
Ad esempio:

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 risposta di Mike può sostituire solo la prima: cerca ma non la seconda.
Quindi, riscrivo la sua risposta per lavorare con più parametri che possono essere riutilizzati correttamente.

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 non ha funzionato per me e quando binding_ era superiore a 9, binding_1 e binding_10 sono stati sostituiti con str_replace (lasciando indietro lo 0), quindi ho effettuato le sostituzioni al contrario:

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;

}

Spero che qualcuno lo trovi utile.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top