Build a fluent query builder that supports method chaining
Implement a fluent query builder over a single table. select(), where() and orderBy() must be chainable in any order, and toSql() must return the assembled SQL string.
Requirements: every chainable method returns the builder itself, so the calls can be strung together; where() may be called several times and the conditions join with AND; toSql() returns a string and does not mutate the builder.
<?php
class QueryBuilder {
private array $columns = [];
private array $wheres = [];
private ?string $order = null;
public function __construct(private string $table) {}
public function select(string ...$columns): static {
// your code here
}
public function where(string $condition): static {
// your code here
}
public function orderBy(string $column): static {
// your code here
}
public function toSql(): string {
// your code here
}
}
Write the implementation.
Every chainable method mutates the builder and returns $this, typed static so a subclass keeps chaining — that is what lets calls be strung together. toSql() is the terminal method: it returns the assembled string, not the builder, so the chain ends there.
- ✗Forgetting to
return $this, so the next call in the chain lands onnull - ✗Typing the return as
selfinstead ofstatic, which breaks chaining in a subclass - ✗Making the terminal
toSql()return the builder instead of the assembled string
- →Why does
staticas a return type keep chaining working in a subclass whereselfwould not? - →How would you make the builder immutable so each call returns a modified clone?
The chain rests on one rule: every method that should continue the chain mutates state and returns $this. The return type is static, not self, so a subclass gets its own type back and the chain does not collapse to the parent.
<?php
class QueryBuilder {
private array $columns = [];
private array $wheres = [];
private ?string $order = null;
public function __construct(private string $table) {}
public function select(string ...$columns): static {
$this->columns = array_merge($this->columns, $columns);
return $this;
}
public function where(string $condition): static {
$this->wheres[] = $condition;
return $this;
}
public function orderBy(string $column): static {
$this->order = $column;
return $this;
}
public function toSql(): string {
$cols = $this->columns ? implode(', ', $this->columns) : '*';
$sql = "SELECT {$cols} FROM {$this->table}";
if ($this->wheres) {
$sql .= ' WHERE ' . implode(' AND ', $this->wheres);
}
if ($this->order !== null) {
$sql .= " ORDER BY {$this->order}";
}
return $sql;
}
}
toSql() is the terminal method: it returns a string rather than the builder, so the chain cannot continue — and that is the point. The where() conditions pile up in an array and are joined with AND, so the call order does not matter.