Builder
Some objects have so many optional pieces of configuration that a single constructor call becomes unreadable — a wall of positional arguments where it's impossible to tell, at a glance, which value means what. The Builder pattern separates the step-by-step construction of a complex object from its final representation, so the same building process can be driven one clear, named step at a time, and can produce different final results depending on which steps were called.
The Problem: The Telescoping Constructor
Picture a Pizza class where size, crust, and every topping are all constructor
parameters. As more optional toppings get added, the constructor keeps growing:
// Bad: a "telescoping constructor" — six-plus positional parameters
class Pizza {
public function __construct(
string $size,
string $crust,
bool $cheese,
bool $pepperoni,
bool $mushrooms,
bool $extraSauce,
bool $glutenFree
) { /* assign all seven properties */ }
}
// At the call site, what do true/false/true actually mean here?
$pizza = new Pizza('large', 'thin', true, false, true, false, false);
Every new optional topping adds another parameter, every existing call site risks passing arguments in the wrong order, and named arguments only paper over the underlying design problem: this one constructor is trying to do too much.
The Builder Version: A Fluent, Chainable Interface
A PizzaBuilder exposes one clearly named method per optional piece, each returning
$this so calls can be chained, ending in a build() call that assembles the
final, immutable Pizza.
<?php
class Pizza {
public function __construct(
public readonly string $size,
public readonly string $crust,
public readonly array $toppings
) {}
}
class PizzaBuilder {
private string $size = 'medium';
private string $crust = 'regular';
private array $toppings = [];
public function setSize(string $size): static {
$this->size = $size;
return $this;
}
public function setCrust(string $crust): static {
$this->crust = $crust;
return $this;
}
public function addTopping(string $topping): static {
$this->toppings[] = $topping;
return $this;
}
public function build(): Pizza {
return new Pizza($this->size, $this->crust, $this->toppings);
}
}
// Reads like a sentence — no ambiguity about what each value means
$pizza = (new PizzaBuilder())
->setSize('large')
->setCrust('thin')
->addTopping('mushrooms')
->addTopping('extra sauce')
->build();
true means, and adding a new optional topping only means adding one
new chainable method — every existing call site keeps working untouched.
A Second Example Closer to Everyday PHP: Query Building
The same shape shows up constantly in database code, where a query has many optional clauses
(WHERE, ORDER BY, LIMIT) that aren't all used every time.
class QueryBuilder {
private string $table;
private array $wheres = [];
private ?string $orderBy = null;
private ?int $limit = null;
public function __construct(string $table) {
$this->table = $table;
}
public function where(string $condition): static {
$this->wheres[] = $condition;
return $this;
}
public function orderBy(string $column): static {
$this->orderBy = $column;
return $this;
}
public function limit(int $n): static {
$this->limit = $n;
return $this;
}
public function build(): string {
$sql = "SELECT * FROM {$this->table}";
if ($this->wheres) {
$sql .= ' WHERE ' . implode(' AND ', $this->wheres);
}
if ($this->orderBy) {
$sql .= " ORDER BY {$this->orderBy}";
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
}
$sql = (new QueryBuilder('orders'))
->where('status = "paid"')
->orderBy('created_at DESC')
->limit(10)
->build();
Where You'll See This in Real Frameworks
This exact fluent-chaining style is everywhere in real PHP code: Laravel's Eloquent query builder
(DB::table('orders')->where(...)->orderBy(...)->get()) and Doctrine's
QueryBuilder class both work precisely this way — chained method calls incrementally
configure a query object, and a final call (get(), getQuery()) builds and
executes it.