makinacorpus/query-builder
A fluent SQL query builder for PHP that lets you construct complex SELECT, INSERT, UPDATE, and DELETE queries programmatically through a clean, chainable API. Lightweight and framework-agnostic, it integrates easily into Laravel or any PHP application.
composer require makinacorpus/query-builder and then open README.md first, followed by src/Query.php and src/Driver/. The public surface is intentionally small: a Query object plus a set of driver/dialect classes. Because this is not a Laravel package (no service provider, no facade, no auto-discovery), you will not find config/query-builder.php — you wire it yourself.
First use case: prototype a portable SELECT in php artisan tinker and inspect the emitted SQL and bindings before wiring it into a real query path.
use Makinacorpus\QueryBuilder\Query;
use Makinacorpus\QueryBuilder\Driver\MySQL; // verify exact driver class names in src/Driver
$q = (new Query(new MySQL()))
->select('id', 'email')
->from('users')
->where('active = ?', [true]);
echo $q->getSql(); // inspect the SQL string
print_r($q->getParameters()); // inspect bindings
Then execute through Laravel's own connection so you keep Laravel's parameter binding and events:
$rows = DB::select($q->getSql(), $q->getParameters());
The mental model: this package generates SQL and bindings; Laravel's DB facade still executes them. Do not expect Eloquent models, scopes, or lazy loading here.
Dialect-bound singleton via a service provider. Bind the driver once so your app doesn't reconstruct it per query.
// app/Providers/QueryBuilderServiceProvider.php
$this->app->singleton(Query::class, function () {
$driver = match (config('database.default')) {
'mysql' => new \Makinacorpus\QueryBuilder\Driver\MySQL(),
'pgsql' => new \Makinacorpus\QueryBuilder\Driver\PostgreSQL(),
'sqlite' => new \Makinacorpus\QueryBuilder\Driver\SQLite(),
default => new \Makinacorpus\QueryBuilder\Driver\MySQL(),
};
return new Query($driver);
});
SQL + bindings DTO. Never return a bare string. Wrap it so execution helpers can't accidentally drop parameters:
final class SqlStatement {
public function __construct(public string $sql, public array $bindings) {}
}
function compile(Query $q): SqlStatement {
return new SqlStatement($q->getSql(), $q->getParameters());
}
function run(SqlStatement $s): array {
return DB::select($s->sql, $s->bindings);
}
Cross-dialect generation for reports/exports. The real day-to-day value: emit MySQL syntax for one tenant, PostgreSQL for another, without branching your PHP:
$driver = $tenant->engine === 'pgsql' ? new PostgreSQL() : new MySQL();
$sql = (new Query($driver))->select('*')->from('invoices')->where('paid = ?', [false]);
Testing / snapshotting. Because output is a deterministic string, snapshot the SQL in tests instead of hitting a database. This also documents your SQL for reviewers. Assert the string and the bindings array — a query is always the pair.
Composition over inheritance. If your application has a reporting layer, keep it returning SqlStatement values and feed them to one runner. This keeps makinacorpus out of your domain classes and makes it swappable if you later move to a different generator.
select()/from()/orderBy() strings. Validate column names against a whitelist.LIMIT/OFFSET vs TOP, RETURNING vs OUTPUT, reserved words, and boolean literal handling (true vs 1) are exactly the places a driver-agnostic builder must differ. Test the same logical query against each driver you actually target.composer.json for PHP version constraints, inspect src/Driver/ for what's actually implemented, and pin a version in composer.lock. There is no large StackOverflow corpus to lean on for edge cases.extra.laravel in this package, so no auto-discovery — you add the provider yourself, or skip the container entirely and instantiate where needed.getSql() alone is a debugging aid, not something to hand to DB::raw(). Always pass the matching bindings, or you will ship an injection or an SQLSTATE mismatch.sql and bindings side-by-side; when a query fails, 90% of the time the driver was constructed with the wrong class, not a builder bug.How can I help you explore Laravel packages today?