Decorator and Proxy look similar at first glance – both wrap another object and implement the same interface. The intent is different. Decorator adds new behaviour. Proxy controls access. This post covers both structural patterns in depth, with PHP implementations, concrete use cases, and a comparison that clarifies when to reach for each one.
Decorator – adding behaviour without subclassing
<?php
declare(strict_types=1);
interface LoggerInterface
{
public function log(string $message, array $context = []): void;
}
class FileLogger implements LoggerInterface
{
public function __construct(private string $path) {}
public function log(string $message, array $context = []): void
{
file_put_contents($this->path, date('Y-m-d H:i:s') . " {$message}\n", FILE_APPEND);
}
}
// Base Decorator - holds a reference to the wrapped logger
abstract class LoggerDecorator implements LoggerInterface
{
public function __construct(protected LoggerInterface $logger) {}
public function log(string $message, array $context = []): void
{
$this->logger->log($message, $context);
}
}
// Adds JSON context serialisation
class ContextDecorator extends LoggerDecorator
{
public function log(string $message, array $context = []): void
{
if (!empty($context)) {
$message .= ' ' . json_encode($context);
}
parent::log($message, $context);
}
}
// Adds log level prefix
class LevelDecorator extends LoggerDecorator
{
public function __construct(LoggerInterface $logger, private string $level = 'INFO') {
parent::__construct($logger);
}
public function log(string $message, array $context = []): void
{
parent::log("[{$this->level}] {$message}", $context);
}
}
// Filters messages below a minimum level
class FilterDecorator extends LoggerDecorator
{
private array $levels = ['DEBUG' => 0, 'INFO' => 1, 'WARNING' => 2, 'ERROR' => 3];
public function __construct(LoggerInterface $logger, private string $minLevel = 'INFO') {
parent::__construct($logger);
}
public function log(string $message, array $context = []): void
{
$msgLevel = $context['level'] ?? 'INFO';
if (($this->levels[$msgLevel] ?? 1) >= ($this->levels[$this->minLevel] ?? 1)) {
parent::log($message, $context);
}
}
}
// Stack decorators - each layer adds its feature
$logger = new FileLogger('/var/log/app.log');
$logger = new ContextDecorator($logger);
$logger = new LevelDecorator($logger, 'INFO');
$logger->log('Order placed', ['order_id' => 42, 'total' => 149.99]);
// File contains: [INFO] Order placed {"order_id":42,"total":149.99}
Proxy – controlling access
<?php
declare(strict_types=1);
interface UserRepositoryInterface
{
public function getById(int $id): array;
public function save(array $user): void;
public function delete(int $id): void;
}
class DatabaseUserRepository implements UserRepositoryInterface
{
public function getById(int $id): array { /* DB query */ return []; }
public function save(array $user): void { /* DB query */ }
public function delete(int $id): void { /* DB query */ }
}
// Caching Proxy - transparent, caller does not know about the cache
class CachingUserProxy implements UserRepositoryInterface
{
private array $cache = [];
public function __construct(
private UserRepositoryInterface $realRepository
) {}
public function getById(int $id): array
{
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->realRepository->getById($id);
}
return $this->cache[$id];
}
public function save(array $user): void
{
$this->realRepository->save($user);
unset($this->cache[$user['id']]); // invalidate cache
}
public function delete(int $id): void
{
$this->realRepository->delete($id);
unset($this->cache[$id]);
}
}
// Authorization Proxy - blocks unauthorised access
class AuthorizingUserProxy implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $realRepository,
private CurrentUser $currentUser
) {}
public function getById(int $id): array
{
return $this->realRepository->getById($id);
}
public function save(array $user): void
{
$this->requirePermission('users.write');
$this->realRepository->save($user);
}
public function delete(int $id): void
{
$this->requirePermission('users.delete');
$this->realRepository->delete($id);
}
private function requirePermission(string $permission): void
{
if (!$this->currentUser->hasPermission($permission)) {
throw new \RuntimeException("Access denied: {$permission}");
}
}
}
Decorator vs Proxy – key differences
| Aspect | Decorator | Proxy |
|---|---|---|
| Intent | Add new behaviour | Control access to existing behaviour |
| Caller awareness | Caller may know it is a decorator | Caller should not know it is a proxy |
| Object creation | Caller wraps the object | Proxy often creates the real object itself |
| Stackable? | Yes – multiple decorators | Usually single proxy per object |
| PHP example | Logger with timestamp + filter | Lazy-loading, caching, auth proxy |
| Magento 2 | Plugin system (Interceptors) | Generated \Proxy DI classes |
Summary
Both patterns use the same structural approach – implement the same interface, wrap an instance, delegate calls. The difference is intent: Decorator enriches, Proxy guards. In practice: reach for Decorator when you want to add logging, formatting, caching as a cross-cutting concern that the caller controls. Reach for Proxy when you want to intercept access transparently – the caller should not see the proxy at all.
