CreationalPHPverifiedVerified
Builder Pattern in PHP
Separates the construction of a complex object from its representation, allowing the same construction process to produce different results.
How to Implement the Builder Pattern in PHP
1Step 1: Define the product
class House
{
public int $floors = 0;
public bool $hasGarage = false;
public bool $hasPool = false;
public bool $hasGarden = false;
public function describe(): string
{
$features = ["{$this->floors} floor(s)"];
if ($this->hasGarage) $features[] = 'garage';
if ($this->hasPool) $features[] = 'pool';
if ($this->hasGarden) $features[] = 'garden';
return 'House with ' . implode(', ', $features);
}
}2Step 2: Define the builder interface
interface HouseBuilder
{
public function setFloors(int $count): static;
public function addGarage(): static;
public function addPool(): static;
public function addGarden(): static;
public function build(): House;
}3Step 3: Implement a concrete builder with fluent API
class StandardHouseBuilder implements HouseBuilder
{
private House $house;
public function __construct()
{
$this->house = new House();
}
public function setFloors(int $count): static { $this->house->floors = $count; return $this; }
public function addGarage(): static { $this->house->hasGarage = true; return $this; }
public function addPool(): static { $this->house->hasPool = true; return $this; }
public function addGarden(): static { $this->house->hasGarden = true; return $this; }
public function build(): House
{
$result = $this->house;
$this->house = new House(); // Reset for reuse
return $result;
}
}4Step 4: Use the builder
$house = (new StandardHouseBuilder())
->setFloors(2)
->addGarage()
->addGarden()
->build();
echo $house->describe();<?php
declare(strict_types=1);
// [step] Define the product with immutable properties
final readonly class HttpRequest
{
/**
* @param array<string, string> $headers
* @param array<string, string> $queryParams
*/
public function __construct(
public string $method,
public string $url,
public array $headers = [],
public array $queryParams = [],
public ?string $body = null,
public int $timeoutMs = 30000,
public int $retries = 0,
) {}
}
// [step] Define the builder with validation
final class HttpRequestBuilder
{
private string $method = 'GET';
private string $url = '';
/** @var array<string, string> */
private array $headers = [];
/** @var array<string, string> */
private array $queryParams = [];
private ?string $body = null;
private int $timeoutMs = 30000;
private int $retries = 0;
private function __construct() {}
public static function create(): self
{
return new self();
}
public function method(string $method): self
{
$allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
if (!in_array(strtoupper($method), $allowed, true)) {
throw new \InvalidArgumentException("Invalid HTTP method: {$method}");
}
$this->method = strtoupper($method);
return $this;
}
public function url(string $url): self
{
if (!filter_var($url, FILTER_VALIDATE_URL) && !str_starts_with($url, '/')) {
throw new \InvalidArgumentException("Invalid URL: {$url}");
}
$this->url = $url;
return $this;
}
public function header(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
public function bearerToken(string $token): self
{
return $this->header('Authorization', "Bearer {$token}");
}
public function queryParam(string $key, string $value): self
{
$this->queryParams[$key] = $value;
return $this;
}
public function jsonBody(mixed $data): self
{
$this->body = json_encode($data, JSON_THROW_ON_ERROR);
$this->headers['Content-Type'] = 'application/json';
return $this;
}
public function timeout(int $ms): self
{
if ($ms <= 0) {
throw new \InvalidArgumentException("Timeout must be positive, got {$ms}");
}
$this->timeoutMs = $ms;
return $this;
}
public function retries(int $count): self
{
if ($count < 0) {
throw new \InvalidArgumentException("Retries must be non-negative");
}
$this->retries = $count;
return $this;
}
// [step] Build the immutable request with validation
public function build(): HttpRequest
{
if ($this->url === '') {
throw new \LogicException('URL is required');
}
if (in_array($this->method, ['POST', 'PUT', 'PATCH'], true) && $this->body === null) {
throw new \LogicException("{$this->method} requests should include a body");
}
return new HttpRequest(
method: $this->method,
url: $this->url,
headers: $this->headers,
queryParams: $this->queryParams,
body: $this->body,
timeoutMs: $this->timeoutMs,
retries: $this->retries,
);
}
}
// [step] Director for common request presets
final class HttpRequestDirector
{
public static function apiGet(string $url, string $token): HttpRequest
{
return HttpRequestBuilder::create()
->method('GET')
->url($url)
->bearerToken($token)
->header('Accept', 'application/json')
->timeout(10000)
->retries(3)
->build();
}
public static function apiPost(string $url, string $token, mixed $body): HttpRequest
{
return HttpRequestBuilder::create()
->method('POST')
->url($url)
->bearerToken($token)
->jsonBody($body)
->timeout(15000)
->retries(1)
->build();
}
}Builder Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Builder Pattern in the Real World
“Consider ordering a custom sandwich at a deli. You tell the sandwich artist (builder) each step — bread type, protein, toppings, sauce — and they assemble it in the right order. You don’t need to know how to layer ingredients properly; you just specify what you want, and the builder hands you a finished sandwich.”