StructuralPHPverifiedVerified
Proxy Pattern in PHP
Provides a surrogate or placeholder for another object to control access, add lazy initialization, caching, logging, or access control.
How to Implement the Proxy Pattern in PHP
1Step 1: Define the subject interface
interface Image
{
public function display(): string;
}2Step 2: Implement the real (heavy) subject
class HighResImage implements Image
{
private string $data;
public function __construct(private readonly string $filename)
{
$this->data = "Loaded: {$filename}"; // Simulates expensive loading
echo "HighResImage loaded: {$filename}\n";
}
public function display(): string
{
return $this->data;
}
}3Step 3: Implement the lazy-loading proxy
class ImageProxy implements Image
{
private ?HighResImage $realImage = null;
public function __construct(
private readonly string $filename,
) {}
public function display(): string
{
// Lazy initialization — only load when needed
if ($this->realImage === null) {
$this->realImage = new HighResImage($this->filename);
}
return $this->realImage->display();
}
}
// Usage — image is not loaded until display() is called
$image = new ImageProxy('photo.jpg');
echo "Proxy created, image not loaded yet\n";
echo $image->display(); // Now the image is loaded<?php
declare(strict_types=1);
// [step] Define the service interface
interface ApiClientInterface
{
/** @return array<string, mixed> */
public function get(string $endpoint): array;
/** @return array<string, mixed> */
public function post(string $endpoint, array $data): array;
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
}
// [step] Implement the real service
final class RealApiClient implements ApiClientInterface
{
public function __construct(
private readonly string $baseUrl,
private readonly string $apiKey,
) {}
public function get(string $endpoint): array
{
return ['endpoint' => $endpoint, 'method' => 'GET', 'data' => []];
}
public function post(string $endpoint, array $data): array
{
return ['endpoint' => $endpoint, 'method' => 'POST', 'data' => $data];
}
}
// [step] Implement a caching proxy with TTL
final class CachingProxy implements ApiClientInterface
{
/** @var array<string, array{data: array<string, mixed>, expiresAt: int}> */
private array $cache = [];
public function __construct(
private readonly ApiClientInterface $client,
private readonly int $ttlSeconds = 300,
private readonly LoggerInterface $logger,
) {}
public function get(string $endpoint): array
{
$key = "GET:{$endpoint}";
$now = time();
if (isset($this->cache[$key]) && $this->cache[$key]['expiresAt'] > $now) {
$this->logger->info("Cache hit", ['endpoint' => $endpoint]);
return $this->cache[$key]['data'];
}
$this->logger->info("Cache miss", ['endpoint' => $endpoint]);
$result = $this->client->get($endpoint);
$this->cache[$key] = ['data' => $result, 'expiresAt' => $now + $this->ttlSeconds];
return $result;
}
public function post(string $endpoint, array $data): array
{
// POST requests are never cached
$result = $this->client->post($endpoint, $data);
// Invalidate related GET cache entries
$getKey = "GET:{$endpoint}";
unset($this->cache[$getKey]);
return $result;
}
}
// [step] Implement an access-control proxy
final class AccessControlProxy implements ApiClientInterface
{
/** @param string[] $allowedEndpoints */
public function __construct(
private readonly ApiClientInterface $client,
private readonly string $userRole,
private readonly array $allowedEndpoints,
private readonly LoggerInterface $logger,
) {}
public function get(string $endpoint): array
{
$this->checkAccess($endpoint);
return $this->client->get($endpoint);
}
public function post(string $endpoint, array $data): array
{
$this->checkAccess($endpoint);
return $this->client->post($endpoint, $data);
}
private function checkAccess(string $endpoint): void
{
$allowed = false;
foreach ($this->allowedEndpoints as $pattern) {
if (fnmatch($pattern, $endpoint)) {
$allowed = true;
break;
}
}
if (!$allowed) {
$this->logger->warning("Access denied", [
'endpoint' => $endpoint,
'role' => $this->userRole,
]);
throw new \RuntimeException("Access denied to {$endpoint} for role {$this->userRole}");
}
}
}Proxy Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Proxy Pattern in the Real World
“A corporate receptionist acts as a proxy for the CEO. When someone wants to meet the CEO, the receptionist checks credentials, schedules the meeting, and logs the visit before granting access. The visitor interacts with the receptionist using the same protocol they would use with the CEO—the receptionist simply adds control and record-keeping around that access.”