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();
Compare that to the telescoping constructor above: no counting positional arguments, no guessing what a bare 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();
Note: this is a simplified, illustrative query builder written to teach the pattern's shape — it's not meant to replace a real, parameterized query layer with proper SQL-injection protection in production code.

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.

Up next: Adapter moves into the Structural patterns — starting with how to make two incompatible interfaces work together without changing either one's source code.