0

Query Builder internals — how the fluent interface compiles to SQL

Expert5 min read·lv-13-015
laravel-srcframework

Concept

Understanding how the Query Builder compiles PHP method calls to SQL demystifies its behavior and helps debug complex queries.

Architecture layers:

  1. Illuminate\Database\Eloquent\Builder — Eloquent layer (scopes, relationships, model awareness).
  2. Illuminate\Database\Query\Builder — Core query builder (SQL construction, no model knowledge).
  3. Illuminate\Database\Connection — Executes queries via PDO.
  4. Illuminate\Database\Query\Grammars\*Grammar — DB-specific SQL rendering (MySqlGrammar, PostgresGrammar, SQLiteGrammar, SqlServerGrammar).

The Builder state: The Query\Builder object is a bag of arrays and flags: $columns, $wheres, $joins, $orders, $groups, $havings, $limit, $offset, $bindings. Each fluent method appends to these arrays. toSql() passes the builder to the Grammar to render SQL. get() passes bindings to PDO and hydrates results.

Grammars: The Grammar class has a method for each SQL clause (compileSelect, compileWheres, compileJoins, etc.). They join the compiled clauses into the final SQL string. This is how the same Builder API works with MySQL, PostgreSQL, and SQLite.

Macros on Query Builder: Builder::macro('myMethod', fn() => $this->where(...)). Available everywhere.

tap(callable $callback): Passes the Builder to a callable without breaking the chain. Useful for conditional modifications or debugging.

Code Example

php
<?php
use Illuminate\Support\Facades\DB;

// Inspecting the compiled query
$builder = DB::table('users')
    ->select('id', 'name')
    ->where('active', true)
    ->where('age', '>=', 18)
    ->orderBy('name')
    ->limit(10);

$sql = $builder->toSql();
// "select `id`, `name` from `users` where `active` = ? and `age` >= ? order by `name` asc limit 10"

$bindings = $builder->getBindings();
// [true, 18]

// Full SQL with bindings interpolated (debug only)
$fullSql = \Illuminate\Support\Str::replaceArray('?', array_map(
    fn($b) => is_string($b) ? "'$b'" : $b,
    $builder->getBindings()
), $builder->toSql());

// Dumping at any point in the chain
DB::table('users')
    ->where('active', true)
    ->tap(fn($q) => dump($q->toSql(), $q->getBindings())) // debug mid-chain
    ->orderBy('name')
    ->get();

// Query Builder macro — app-wide extension
\Illuminate\Database\Query\Builder::macro('whereLike', function(string $column, string $search) {
    return $this->where($column, 'LIKE', '%' . $search . '%');
});
// Usage:
DB::table('users')->whereLike('name', 'ali')->get();

// Grammar differences — same Builder, different SQL output
// MySQL:   `users` (backtick quoting)
// Postgres: "users" (double-quote quoting)
// SQLite:  `users` or "users"

// Checking which DB is being used
DB::connection()->getDriverName(); // 'mysql', 'pgsql', 'sqlite', 'sqlsrv'

// DB::listen — log all queries
DB::listen(function($query) {
    \Log::debug('SQL', [
        'sql'      => $query->sql,
        'bindings' => $query->bindings,
        'time_ms'  => $query->time,
    ]);
});