Obtendo string de consulta SQL cru de DOP preparado declarações
-
03-07-2019 - |
Pergunta
Existe uma maneira de obter a seqüência SQL cru executada ao chamar PDOStatement :: execute () em uma declaração preparada? Para fins de depuração isso seria extremamente útil.
Solução
Eu suponho que você quer dizer que você quer que a consulta SQL final, com valores de parâmetros interpolados para ele. Eu entendo que isso seria útil para depurar, mas não é o caminho preparado declarações trabalho. Os parâmetros não são combinados com uma declaração preparada no lado do cliente, de modo DOP nunca deve ter acesso à string de consulta combinada com seus parâmetros.
A instrução SQL é enviada para o servidor de banco de dados quando você prepare (), e os parâmetros são enviados separadamente quando você execute (). log de consulta geral do MySQL não mostrar o SQL final com valores interpolados depois que você execute (). Abaixo está um trecho do meu log de consulta geral. Corri as consultas a partir do CLI mysql, não do DOP, mas o princípio é o mesmo.
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
Você também pode obter o que você quer se você definir o atributo PDO PDO :: ATTR_EMULATE_PREPARES. Neste modo, DOP parâmetros Interpolar para a consulta SQL e envia a consulta inteira quando você execute (). Esta não é uma verdadeira consulta preparada. Você vai contornar os benefícios de consultas preparadas pela interpolação variáveis ??na cadeia de SQL antes de executar ().
Re comentário de @afilina:
Não, o texto SQL consulta é não combinado com os parâmetros durante a execução. Então não há nada para DOP para mostrar a você.
Internamente, se você usar PDO :: ATTR_EMULATE_PREPARES, DOP faz uma cópia da consulta SQL e interpola parâmetro valores para ele antes de fazer a preparar e executar. Mas DOP não expõe esta consulta SQL modificado.
O objeto PDOStatement tem uma propriedade $ queryString, mas isso é definido apenas no construtor para a PDOStatement, e não é atualizada quando a consulta é reescrito com parâmetros.
Seria um pedido de recurso razoável para DOP para pedir-lhes para expor a consulta reescrito. Mas mesmo que não iria dar-lhe a consulta "completa" a menos que você use PDO :: ATTR_EMULATE_PREPARES.
É por isso que eu mostro a solução acima de usar log consulta geral do servidor MySQL, porque neste caso, mesmo uma consulta preparada com espaços reservados de parâmetro é reescrito no servidor, com valores de parâmetro preenchidos na string de consulta. Mas isso só é feito durante o registro, não durante a execução da consulta.
Outras dicas
/**
* 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;
}
Eu modifiquei o método para incluir manipulação de saída de matrizes para as demonstrações como onde IN (?).
UPDATE: verificação Apenas adicionado para o valor NULL e duplicados $ parâmetros valores param de modo real $ não são modificados.
Great trabalho bigwebguy e obrigado!
/**
* 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 tem uma propriedade pública $ queryString. Deve ser o que quiser.
Acabei de perceber que PDOStatement tem um indocumentados debugDumpParams () método que você também pode querer olhar.
Um pouco tarde, provavelmente, mas agora há PDOStatement::debugDumpParams
despeja as informações contidas por uma declaração preparada diretamente no a saída. Ele irá fornecer a consulta SQL em uso, o número de parâmetros utilizados (Parâmetros), a lista de parâmetros, com o seu nome, digitar (ParamType) como um número inteiro, o seu nome de chave ou posição, e o posição na consulta (se for suportado pelo driver DOP, caso contrário, será -1).
Você pode encontrar mais informações sobre os documentação oficial do PHP
Exemplo:
<?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();
?>
Adicionado um pouco mais para o código por Mike - a pé os valores para adicionar aspas 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;
}
Eu passei uma boa parte do tempo pesquisando esta situação para minhas próprias necessidades. Este e vários outros tópicos Então me ajudou muito, então eu queria compartilhar o que eu vim acima com.
Apesar de ter acesso à string de consulta interpolada é um benefício significativo, enquanto solução de problemas, queríamos ser capaz de manter um registro de apenas determinadas consultas (portanto, usando os logs de banco de dados para este fim não era o ideal). Nós também queria ser capaz de usar os logs para recriar a condição das mesas, em determinado momento, portanto, precisamos fazer certas as cordas interpolados foram escapou corretamente. Finalmente, queria estender essa funcionalidade para toda a nossa base de código ter que re-escrever tão pouco dele quanto possível (prazos, marketing, e tal, você sabe como é).
Minha solução foi estender a funcionalidade do objeto PDOStatement padrão para armazenar em cache os valores parametrizados (ou referências), e quando a instrução é executada, utilize a funcionalidade do objeto PDO para escapar corretamente os parâmetros quando são injetados de volta no para a string de consulta. Poderíamos, então, amarrar no para executar método do objeto declaração e log a consulta real que foi executado naquele tempo ( ou pelo menos o mais fiel de uma reprodução possível) .
Como eu disse, nós não deseja modificar toda a base de código para adicionar esta funcionalidade, por isso, substituir os bindParam()
e bindValue()
métodos padrão do objeto PDOStatement, fazer o nosso cache dos dados vinculados, então parent::bindParam()
chamada ou pai :: bindValue()
. Isto permitiu que a nossa base de código existente para continuar a funcionar como normal.
Finalmente, quando o método execute()
é chamado, realizamos nossa interpolação e fornecer a string resultante como uma nova E_PDOStatement->fullQuery
propriedade. Esta pode ser a saída para exibir a consulta ou, por exemplo, gravados em um arquivo de log.
A extensão, juntamente com as instruções de instalação e configuração, estão disponíveis no github:
https://github.com/noahheck/E_PDOStatement
AVISO LEGAL :
Obviamente, como eu mencionei, eu escrevi esta extensão. Porque foi desenvolvido com a ajuda de muitos tópicos aqui, eu queria postar minha solução aqui no caso de alguém se depara com esses tópicos, assim como eu fiz.
Você pode estender a classe PDOStatement para capturar as variáveis ??limitadas e armazená-los para uso posterior. Então 2 métodos podem ser adicionados, um para saneantes variável (debugBindedVariables) e outro para imprimir a consulta com essas variáveis ??(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 então você pode usar esta classe herdada para depurar purpouses.
$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 em
user SELECT FROM usuários ONDE user = 'user_test'
Array ( [: Teste] => user_test )
A solução é colocar voluntariamente um erro na consulta e para imprimir a mensagem do erro:
//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();
}
A saída padrão:
SQLSTATE [42000]: Erro de sintaxe ou violação de acesso: [...] perto 'ELECT * FROM Pessoa ONDE idade = 18' na linha 1
É importante notar que ele imprime apenas os primeiros 80 caracteres da consulta.
A propriedade $ queryString mencionado provavelmente só irá retornar a consulta passada, sem os parâmetros substituídos por seus valores. Na Net, eu tenho a parte captura de minha consulta executer fazer uma pesquisa simples substituir sobre os parâmetros com os valores que foram fornecidas para que o log de erro pode mostrar valores reais que estavam sendo usados ??para a consulta. Você deve ser capaz de enumerar os parâmetros em PHP, e substituir os parâmetros com o seu valor atribuído.
Um pouco relacionado ... se você está apenas tentando sanear uma variável especial que você pode usar PDO :: citação . Por exemplo, para procurar várias condições parciais como se você está preso com um quadro 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}%"),
),
);
Eu preciso logar string de consulta completo depois param ligamento de modo que este é um pedaço no meu código. Esperança, é útil para o chapéu todo mundo tem o mesmo 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);
}
Eu sei que esta questão é um pouco velho, mas, eu estou usando esse código desde tempos muito atrás (eu usei resposta de @ chris-go), e agora, estes códigos são obsoletos com PHP 7.2
Vou postar uma versão atualizada destes código (Crédito para o código principal são de @bigwebguy , @ Mike e @ chris-go , todos eles responde desta pergunta):
/**
* 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;
}
Observe a mudança no código estão em array_walk function (), substituindo create_function por uma função anônima. Este torná-los bom pedaço de código funcional e compatível com PHP 7.2 (e versões futuras esperança também).
Você pode usar sprintf(str_replace('?', '"%s"', $sql), ...$params);
Aqui está um exemplo:
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";
}
Note que este só funciona para PHP> = 5,6
resposta de Mike está trabalhando bom até que você está usando a "reutilização" ligar valor.
Por exemplo:
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)
A resposta do Mike só pode substituir em primeiro lugar: procurar, mas não o segundo
.
Então, eu reescrever sua resposta ao trabalho com vários parâmetros que podem re-utilizado corretamente.
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ão funcionou para mim e quando binding_ durou mais de 9, binding_1 e binding_10 foi substituído por str_replace (deixando a 0 trás), então eu fiz as substituições para trá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;
}
Hope alguém acha útil.