O que Todo Dev Deve Saber sobre Query Builder Simples: Construindo Consultas Dinâmicas em PHP Já leu

O que é um Query Builder? 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. 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. Construindo um Query Builder Básico Estrutura Principal Vamos criar um Query Builder simples do zero. A classe precisa armazenar componentes SQL (SELECT, WHERE, JOIN, etc.) e gerar a consulta final. Usando o Query Builder O encadeamento de métodos (method chaining) torna o

O que é um Query Builder?

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.

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.

Construindo um Query Builder Básico

Estrutura Principal

Vamos criar um Query Builder simples do zero. A classe precisa armazenar componentes SQL (SELECT, WHERE, JOIN, etc.) e gerar a consulta final.

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

Usando o Query Builder

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

O encadeamento de métodos (method chaining) torna o código fluente e legível. Cada método retorna $this, permitindo chamar o próximo método imediatamente. Os valores são separados em um array $bindings, preparando-os para usar com prepared statements.

Implementações Avançadas

Métodos Auxiliares Importantes

Um Query Builder robusto precisa de métodos adicionais para casos complexos. Vamos expandir:

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

Integrando com PDO

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

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.

Boas Práticas e Considerações

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.

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.

Conclusão

Um Query Builder bem implementado oferece segurança, legibilidade e manutenibilidade. Os três pontos principais aprendidos foram: (1) O encadeamento de métodos cria uma API fluente e intuitiva; (2) A separação entre construção e execução permite validação e logging; (3) Prepared statements com bindings previnem SQL injection e vulnerabilidades. Comece com uma implementação simples e expanda gradualmente conforme suas necessidades.

Referências


Artigos relacionados