Builder Pattern
debt(d7/e5/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate phpstan as the tool but automated detection is explicitly 'no' — phpstan cannot reliably detect misuse of the builder pattern (e.g. missing validation in build(), reused state, or forgotten $this returns). These issues only surface through careful code review or when runtime behavior produces invalid objects silently. The code_pattern hint (6+ optional params) describes when to use it, not a tool that flags misuse.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes when to use a builder, but correcting misuse — e.g. retrofitting validation into build(), resetting state between builds, or replacing a misused builder with plain constructors — typically requires changes across the builder class and all its call sites. It's not a single-line patch but also not a full cross-cutting refactor, landing at e5.
Closest to 'localised tax' (b3). The builder pattern is scoped to a specific object's construction logic. Its reach is limited to callers of that builder and the builder class itself. It applies across web/cli/queue contexts but doesn't impose a gravitational pull on the entire codebase — only the component constructing that particular complex object pays the tax.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field directly identifies the trap: developers treat it as merely verbose syntactic sugar for constructors, missing its core value in enforcing construction order and validating intermediate state. Common mistakes reinforce this — not validating in build() silently produces invalid objects, and forgetting $this breaks fluent chains. These are well-documented gotchas that most intermediate developers eventually encounter.
Also Known As
TL;DR
Explanation
The Builder pattern addresses constructors with many parameters (especially optional ones) by providing a step-by-step construction API: $query = QueryBuilder::select('users')->where('active', true)->orderBy('name')->limit(10)->build(). Each setter returns $this (fluent interface), and build() returns the final immutable object. In PHP, Builders appear in query builders (Doctrine DBAL), HTTP client requests, and test fixture factories. Compared to long constructors or setters, Builders make invalid intermediate states visible and enable validation at build() time.
Diagram
flowchart LR
subgraph Without_Builder
CONS[new QueryBuilder<br/>table conditions joins<br/>order limit offset columns]
PROB[Constructor with 8 params<br/>positional - error prone]
end
subgraph With_Builder
B[QueryBuilder::for users]
B -->|chained| W[->where id > 5]
W -->|chained| J[->join orders on user_id]
J -->|chained| O[->orderBy created_at]
O -->|chained| L[->limit 20]
L -->|build| QUERY[SELECT ... built query]
end
style PROB fill:#f85149,color:#fff
style QUERY fill:#238636,color:#fff
style B fill:#1f6feb,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using a builder for simple objects with 2-3 parameters — a plain constructor or named arguments is cleaner.
- Not validating required fields in the build() method — the builder produces an invalid object silently.
- Forgetting to return $this from each setter — breaks the fluent chain.
- Reusing the same builder instance for multiple objects without resetting state between builds.
Code Examples
// Telescoping constructor — hard to read and extend:
function __construct(
string $name, ?string $email = null, ?string $phone = null,
?string $address = null, bool $active = true, int $role = 1
) {} // 6 params, optional ones hard to manage — use a builder
class QueryBuilder {
private array $wheres = [];
private ?int $limit = null;
private string $orderBy = 'id';
public function where(string $col, mixed $val): static {
$this->wheres[] = [$col, $val];
return $this;
}
public function limit(int $n): static {
$this->limit = $n;
return $this;
}
public function orderBy(string $col): static {
$this->orderBy = $col;
return $this;
}
public function build(): string { /* assemble SQL */ }
}
$sql = (new QueryBuilder())
->where('status', 'active')
->where('role', 'admin')
->orderBy('created_at')
->limit(10)
->build();