PHP MySQLi prepare() não monta a query como string: comportamento ou bug?

Introdução

Durante a depuração de uma aplicação PHP que utiliza mysqli com prepare() e bind_param(), é comum o desenvolvedor se deparar com um comportamento aparentemente anômalo: a impossibilidade de visualizar a query final com os valores já preenchidos.

Esse post desvenda esse comportamento com precisão técnica, mostra por que isso ocorre, como simular a query completa para debug e explica por que isso não é um bug, mas sim uma decisão arquitetural deliberada.


O que acontece quando usamos prepare() com bind_param()

Quando você escreve:

$stmt = $mysqli->prepare("SELECT * FROM usuarios WHERE nome = ? AND status = ?");
$stmt->bind_param("ss", $nome, $status);
$stmt->execute();

O que o PHP faz não é:

SELECT * FROM usuarios WHERE nome = 'João' AND status = 'ativo'

Em vez disso, o PHP:

  • Mantém os ? como placeholders binários
  • Usa bind_param() para empacotar os valores nos tipos corretos
  • Envia o statement e os parâmetros de forma separada via protocolo binário ao servidor MySQL
  • O MySQL interpreta e executa o comando diretamente no servidor

Ou seja: a query montada com valores substituídos nunca existe no PHP.


Isso é um bug?

Não. É o comportamento projetado do protocolo MySQL Native Driver (mysqlnd) utilizado pelo PHP. Ele foi feito assim por motivos de:

Segurança

  • Evita SQL Injection sem depender de validação manual
  • Protege mesmo se o desenvolvedor esquecer de escapar entradas

Performance

  • Permite reuso do statement pré-compilado no servidor
  • Envia apenas os parâmetros novos a cada execução, reduzindo overhead

Mas eu quero ver a query final com os dados

Como o PHP não a monta, você precisa simular a query para fins de debug. Aqui está uma função simples e eficaz:

function simulaQuery($sql, $params) {
    foreach ($params as $p) {
        $p = is_string($p) ? "'" . addslashes($p) . "'" : $p;
        $sql = preg_replace('/\?/', $p, $sql, 1);
    }
    return $sql;
}

Exemplo de uso:

$query = "SELECT * FROM usuarios WHERE nome = ? AND status = ?";
$params = ["João", "ativo"];

echo simulaQuery($query, $params);

Saída:

SELECT * FROM usuarios WHERE nome = 'João' AND status = 'ativo'

Essa função é útil em logs, debug, auditoria e ferramentas próprias de inspeção.


Por que isso confundiu até devs experientes?

Porque o comportamento padrão com interpolação ($sql = "SELECT ... WHERE campo = '$valor'") sempre gerava a string completa antes de enviar para o MySQL. Com prepare(), a string com os dados nunca é gerada, o que faz parecer que a função está ignorando ou corrompendo os valores, quando na verdade está apenas enviando-os separadamente.


Conclusão

O uso de prepare() com bind_param() é a forma correta, segura e eficiente de trabalhar com MySQL em PHP. No entanto, essa abordagem exige uma mudança de mentalidade na hora de debugar: não espere conseguir echoar a query com os dados substituídos. Ela nunca existiu como string no processo.

Para simular a query real, use funções auxiliares como simulaQuery() ou crie wrappers específicos para seu projeto.


Referências Técnicas

Rolar para cima