<h2>O que é um Query Builder?</h2>
<p>Um Query Builder é uma classe ou biblioteca que constrói consultas SQL dinamicamente através de uma interface orientada a objetos. Em vez de escrever strings SQL manualmente, você encadeia métodos para montar a consulta de forma segura e legível. Isso reduz erros, previne SQL injection e torna o código mais manutenível. É uma ferramenta fundamental em ORMs modernos e frameworks PHP como Laravel, Symfony e Doctrine.</p>
<p>A grande vantagem é a abstração do banco de dados: a mesma consulta funciona com MySQL, PostgreSQL ou SQLite sem mudanças no código PHP. Você escreve uma vez e roda em qualquer banco. Além disso, a segurança é integrada: parâmetros são automaticamente escapados, eliminando vulnerabilidades comuns.</p>
<h2>Construindo um Query Builder Básico</h2>
<h3>Estrutura Principal</h3>
<p>Vamos criar um Query Builder simples do zero. A classe precisa armazenar componentes SQL (SELECT, WHERE, JOIN, etc.) e gerar a consulta final.</p>
<pre><code class="language-php"><?php
class QueryBuilder {
private $select = [];
private $from = '';
private $wheres = [];
private $bindings = [];
private $joins = [];
private $orderBy = [];
private $limit = null;
public function select(...$columns) {
$this->select = $columns ?: ['*'];
return $this;
}
public function from($table) {
$this->from = $table;
return $this;
}
public function where($column, $operator, $value) {
$this->wheres[] = "$column $operator ?";
$this->bindings[] = $value;
return $this;
}
public function join($table, $condition) {
$this->joins[] = "INNER JOIN $table ON $condition";
return $this;
}
public function orderBy($column, $direction = 'ASC') {
$this->orderBy[] = "$column $direction";
return $this;
}
public function limit($count) {
$this->limit = $count;
return $this;
}
public function build() {
$query = 'SELECT ' . implode(', ', $this->select);
$query .= ' FROM ' . $this->from;
if (!empty($this->joins)) {
$query .= ' ' . implode(' ', $this->joins);
}
if (!empty($this->wheres)) {
$query .= ' WHERE ' . implode(' AND ', $this->wheres);
}
if (!empty($this->orderBy)) {
$query .= ' ORDER BY ' . implode(', ', $this->orderBy);
}
if ($this->limit) {
$query .= ' LIMIT ' . $this->limit;
}
return $query;
}
public function getBindings() {
return $this->bindings;
}
}</code></pre>
<h3>Usando o Query Builder</h3>
<pre><code class="language-php"><?php
$query = new QueryBuilder();
$sql = $query
->select('id', 'name', 'email')
->from('users')
->where('age', '>', 18)
->where('status', '=', 'active')
->orderBy('name', 'ASC')
->limit(10)
->build();
echo $sql; // SELECT id, name, email FROM users WHERE age > ? AND status = ? ORDER BY name ASC LIMIT 10
echo "\n";
print_r($query->getBindings()); // Array ( [0] => 18 [1] => active )</code></pre>
<p>O encadeamento de métodos (method chaining) torna o código fluente e legível. Cada método retorna <code>$this</code>, permitindo chamar o próximo método imediatamente. Os valores são separados em um array <code>$bindings</code>, preparando-os para usar com prepared statements.</p>
<h2>Implementações Avançadas</h2>
<h3>Métodos Auxiliares Importantes</h3>
<p>Um Query Builder robusto precisa de métodos adicionais para casos complexos. Vamos expandir:</p>
<pre><code class="language-php"><?php
class QueryBuilder {
// ... código anterior ...
private $orWheres = [];
public function orWhere($column, $operator, $value) {
$this->orWheres[] = "$column $operator ?";
$this->bindings[] = $value;
return $this;
}
public function whereIn($column, array $values) {
$placeholders = implode(',', array_fill(0, count($values), '?'));
$this->wheres[] = "$column IN ($placeholders)";
$this->bindings = array_merge($this->bindings, $values);
return $this;
}
public function leftJoin($table, $condition) {
$this->joins[] = "LEFT JOIN $table ON $condition";
return $this;
}
public function build() {
$query = 'SELECT ' . implode(', ', $this->select);
$query .= ' FROM ' . $this->from;
if (!empty($this->joins)) {
$query .= ' ' . implode(' ', $this->joins);
}
if (!empty($this->wheres) || !empty($this->orWheres)) {
$conditions = array_merge($this->wheres, $this->orWheres);
$query .= ' WHERE ' . implode(' AND ', $this->wheres);
if (!empty($this->orWheres)) {
$query .= ' OR ' . implode(' OR ', $this->orWheres);
}
}
if (!empty($this->orderBy)) {
$query .= ' ORDER BY ' . implode(', ', $this->orderBy);
}
if ($this->limit) {
$query .= ' LIMIT ' . $this->limit;
}
return $query;
}
}</code></pre>
<h3>Integrando com PDO</h3>
<pre><code class="language-php"><?php
class QueryExecutor {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function execute(QueryBuilder $query) {
$sql = $query->build();
$bindings = $query->getBindings();
$stmt = $this->pdo->prepare($sql);
$stmt->execute($bindings);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// Uso:
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$executor = new QueryExecutor($pdo);
$query = (new QueryBuilder())
->select('id', 'name')
->from('users')
->where('age', '>', 21)
->whereIn('role', ['admin', 'moderator']);
$results = $executor->execute($query);</code></pre>
<p>Este padrão garante segurança contra SQL injection, pois todos os valores são parametrizados. O prepared statement do PDO cuida da sanitização automaticamente.</p>
<h2>Boas Práticas e Considerações</h2>
<p>Um Query Builder deve ser robusto e fácil de manter. Sempre valide entradas antes de construir a consulta. Implemente tratamento de erros adequado para casos onde a consulta gerada é inválida. Considere adicionar logging para debug em desenvolvimento. Não permita que usuários construam queries arbitrárias diretamente; sempre filtre entrada.</p>
<p>Use type hints e PHPDoc para melhorar a qualidade do código. Defina limites padrão para evitar queries sem limite que consumam muitos recursos. Crie testes unitários para validar que as queries geradas estão corretas. Em produção, monitore as queries mais lentas e otimize índices de banco de dados conforme necessário.</p>
<h2>Conclusão</h2>
<p>Um Query Builder bem implementado oferece segurança, legibilidade e manutenibilidade. Os três pontos principais aprendidos foram: <strong>(1)</strong> O encadeamento de métodos cria uma API fluente e intuitiva; <strong>(2)</strong> A separação entre construção e execução permite validação e logging; <strong>(3)</strong> Prepared statements com bindings previnem SQL injection e vulnerabilidades. Comece com uma implementação simples e expanda gradualmente conforme suas necessidades.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.php.net/manual/en/class.pdo.php" target="_blank" rel="noopener noreferrer">PHP PDO Official Documentation</a></li>
<li><a href="https://laravel.com/docs/eloquent" target="_blank" rel="noopener noreferrer">Laravel Query Builder</a></li>
<li><a href="https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html" target="_blank" rel="noopener noreferrer">Doctrine DBAL QueryBuilder</a></li>
<li><a href="https://owasp.org/www-community/attacks/SQL_Injection" target="_blank" rel="noopener noreferrer">SQL Injection Prevention</a></li>
<li><a href="https://www.oreilly.com/library/view/clean-code/9780136083238/" target="_blank" rel="noopener noreferrer">Clean Code in PHP - Robert C. Martin</a></li>
</ul>