Decorator — adding behaviour without inheritance
Concept
Decorator pattern adds behavior to an object without modifying it or using subclassing. Decorators implement the same interface as the component they wrap, and can be stacked — wrapping a decorator in another decorator.
The problem: You have a Cache class. You want to add logging to it. Then encryption. Then compression. Creating LoggingCache extends Cache, EncryptedCache extends Cache, CompressedCache extends Cache, and all combinations thereof leads to a class explosion. Decorators avoid this.
Structure:
- Component Interface (
CacheInterface): The interface both the real component and decorators implement. - Concrete Component (
FileCache,RedisCache): The real implementation. - Decorator: Implements
CacheInterface, takes aCacheInterfaceas a constructor parameter, delegates to it, and adds behavior before or after.
Stack decorators: $cache = new LoggingDecorator(new CompressionDecorator(new RedisCache())). Each decorator wraps the previous. Calls pass inward, return outward.
PHP real-world examples:
- PSR-7 Response decorators.
- Logger decorators (add context, channel routing).
- Stream decorators in Guzzle.
- Doctrine's DBAL connection decorators.
- Laravel:
Pipelinemiddleware IS the Decorator pattern applied to request handling.
Decorator vs Inheritance: Decorators compose behavior at runtime; inheritance bakes it in at compile time. Decorators can be combined in any order. Multiple inheritance (which PHP doesn't have) would be needed to combine multiple inheritance-based extensions.
Code Example
<?php
// Component Interface
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): void;
public function delete(string $key): void;
}
// Concrete Component
class RedisCache implements CacheInterface
{
public function get(string $key): mixed { /* fetch from Redis */ return null; }
public function set(string $key, mixed $value, int $ttl = 3600): void { /* store in Redis */ }
public function delete(string $key): void { /* delete from Redis */ }
}
// Abstract Decorator — DRY base that delegates to wrapped component
abstract class CacheDecorator implements CacheInterface
{
public function __construct(protected readonly CacheInterface $inner) {}
public function get(string $key): mixed { return $this->inner->get($key); }
public function set(string $key, mixed $value, int $ttl = 3600): void { $this->inner->set($key, $value, $ttl); }
public function delete(string $key): void { $this->inner->delete($key); }
}
// Concrete Decorators
class LoggingCacheDecorator extends CacheDecorator
{
public function __construct(CacheInterface $inner, private readonly \Psr\Log\LoggerInterface $logger)
{
parent::__construct($inner);
}
public function get(string $key): mixed
{
$value = parent::get($key);
$this->logger->debug('Cache ' . ($value !== null ? 'hit' : 'miss'), ['key' => $key]);
return $value;
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
$this->logger->debug('Cache set', ['key' => $key, 'ttl' => $ttl]);
parent::set($key, $value, $ttl);
}
}
class CompressingCacheDecorator extends CacheDecorator
{
public function get(string $key): mixed
{
$compressed = parent::get($key);
return $compressed !== null ? unserialize(gzuncompress($compressed)) : null;
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
parent::set($key, gzcompress(serialize($value)), $ttl);
}
}
class EncryptingCacheDecorator extends CacheDecorator
{
public function __construct(CacheInterface $inner, private readonly string $key)
{
parent::__construct($inner);
}
public function get(string $key): mixed
{
$encrypted = parent::get($key);
return $encrypted !== null ? $this->decrypt($encrypted) : null;
}
public function set(string $cacheKey, mixed $value, int $ttl = 3600): void
{
parent::set($cacheKey, $this->encrypt($value), $ttl);
}
private function encrypt(mixed $value): string { return base64_encode(serialize($value) . $this->key); }
private function decrypt(string $value): mixed { return unserialize(base64_decode($value)); }
}
// Composing decorators — order matters!
$cache = new LoggingCacheDecorator(
new CompressingCacheDecorator(
new EncryptingCacheDecorator(
new RedisCache(),
key: env('CACHE_ENCRYPTION_KEY')
)
),
logger: app(\Psr\Log\LoggerInterface::class)
);
// get() calls: Log → Compress.get → Encrypt.get → Redis.get → decrypt → decompress → log result