StructuralPHPverifiedVerified
Decorator Pattern in PHP
Attaches additional responsibilities to an object dynamically by wrapping it in decorator objects that share the same interface.
How to Implement the Decorator Pattern in PHP
1Step 1: Define the component interface
interface DataSource
{
public function writeData(string $data): void;
public function readData(): string;
}2Step 2: Implement the base concrete component
class FileDataSource implements DataSource
{
private string $data = '';
public function writeData(string $data): void { $this->data = $data; }
public function readData(): string { return $this->data; }
}3Step 3: Implement the base decorator
abstract class DataSourceDecorator implements DataSource
{
public function __construct(protected DataSource $wrapped) {}
public function writeData(string $data): void { $this->wrapped->writeData($data); }
public function readData(): string { return $this->wrapped->readData(); }
}4Step 4: Implement concrete decorators
class EncryptionDecorator extends DataSourceDecorator
{
public function writeData(string $data): void
{
$encrypted = base64_encode($data); // Simple simulation
parent::writeData($encrypted);
}
public function readData(): string
{
$data = parent::readData();
return base64_decode($data);
}
}
class CompressionDecorator extends DataSourceDecorator
{
public function writeData(string $data): void
{
$compressed = gzcompress($data);
parent::writeData($compressed);
}
public function readData(): string
{
$data = parent::readData();
return gzuncompress($data);
}
}
// Usage — stack decorators
$source = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource()
)
);
$source->writeData('Hello World');
echo $source->readData(); // "Hello World"<?php
declare(strict_types=1);
// [step] Define the HTTP client interface
interface HttpClientInterface
{
/** @param array<string, string> $headers */
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse;
}
final readonly class HttpResponse
{
/** @param array<string, string> $headers */
public function __construct(
public int $statusCode,
public string $body,
public array $headers = [],
) {}
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
}
// [step] Implement the base HTTP client
final class CurlHttpClient implements HttpClientInterface
{
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse
{
// Simulate HTTP request
return new HttpResponse(200, '{"status":"ok"}', ['Content-Type' => 'application/json']);
}
}
// [step] Implement decorator base
abstract class HttpClientDecorator implements HttpClientInterface
{
public function __construct(
protected readonly HttpClientInterface $inner,
) {}
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse
{
return $this->inner->request($method, $url, $headers, $body);
}
}
// [step] Concrete decorators
final class LoggingDecorator extends HttpClientDecorator
{
public function __construct(
HttpClientInterface $inner,
private readonly LoggerInterface $logger,
) {
parent::__construct($inner);
}
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse
{
$start = hrtime(true);
$this->logger->info("HTTP {$method} {$url}");
$response = parent::request($method, $url, $headers, $body);
$durationMs = (hrtime(true) - $start) / 1e6;
$this->logger->info("Response {$response->statusCode}", [
'durationMs' => round($durationMs, 2),
]);
return $response;
}
}
final class RetryDecorator extends HttpClientDecorator
{
public function __construct(
HttpClientInterface $inner,
private readonly int $maxRetries = 3,
private readonly int $baseDelayMs = 100,
) {
parent::__construct($inner);
}
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse
{
$lastException = null;
for ($attempt = 0; $attempt <= $this->maxRetries; $attempt++) {
try {
$response = parent::request($method, $url, $headers, $body);
if ($response->statusCode < 500) {
return $response;
}
$lastException = new \RuntimeException("Server error: {$response->statusCode}");
} catch (\Throwable $e) {
$lastException = $e;
}
if ($attempt < $this->maxRetries) {
usleep($this->baseDelayMs * (2 ** $attempt) * 1000);
}
}
throw $lastException ?? new \RuntimeException('Request failed after retries');
}
}
final class CachingDecorator extends HttpClientDecorator
{
/** @var array<string, array{response: HttpResponse, expiresAt: int}> */
private array $cache = [];
public function __construct(
HttpClientInterface $inner,
private readonly int $ttlSeconds = 300,
) {
parent::__construct($inner);
}
public function request(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse
{
// Only cache GET requests
if ($method !== 'GET') {
return parent::request($method, $url, $headers, $body);
}
$key = md5("{$method}:{$url}");
$now = time();
if (isset($this->cache[$key]) && $this->cache[$key]['expiresAt'] > $now) {
return $this->cache[$key]['response'];
}
$response = parent::request($method, $url, $headers, $body);
$this->cache[$key] = ['response' => $response, 'expiresAt' => $now + $this->ttlSeconds];
return $response;
}
}Decorator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Decorator Pattern in the Real World
“Think of adding espresso shots and syrups to a coffee order. A plain coffee is the base component. Each addition—an espresso shot, vanilla syrup, oat milk—is a decorator that wraps the previous cup, adding its own cost and flavor. You can combine them in any order without the café needing a separate menu item for every combination.”