Decorator Pattern
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches' (d5). PHPStan can detect type mismatches when decorators don't implement the expected interface, but detecting the anti-pattern of inheritance explosion instead of composable decorators (as noted in detection_hints.code_pattern) requires careful architectural review. The detection_hints explicitly state automated=no, meaning this is not automatically caught.
Closest to 'touches multiple files / significant refactor' (e5). The quick_fix describes wrapping objects with decorators implementing the same interface — sounds simple, but refactoring from an inheritance tree (LoggingCache, CachingWithLogging) to composable decorators requires extracting interfaces, creating decorator classes, and updating instantiation points across the codebase. Not a one-liner.
Closest to 'persistent productivity tax' (b5). The pattern applies broadly (web, cli, queue-worker contexts per applies_to) and once adopted shapes how cross-cutting concerns are handled throughout the system. A decorator-based architecture becomes a convention that influences how new features are added — middleware pipelines, logging, caching all follow this shape. Not quite system-defining but more than localised.
Closest to 'notable trap' (t5). The misconception field explicitly states developers assume decorators must always implement the same interface, when opaque decorators are valid for specific cases. Common_mistakes reinforce this: confusing Decorator with Proxy (similar structure, different intent), forgetting delegation to the wrapped object. These are documented gotchas that experienced developers eventually learn, but competent newcomers regularly stumble on them.
Also Known As
TL;DR
Explanation
The Decorator pattern wraps an existing object in a new class that implements the same interface, adding behaviour before/after delegating to the wrapped object. Unlike inheritance, decorators compose at runtime and can be stacked. Classic examples: adding logging, caching, or rate limiting to a repository by wrapping it with LoggingRepository, CachingRepository, etc. — each wrapper transparent to callers. In PHP, this is the basis for middleware pipelines in frameworks and PSR-15 HTTP middleware.
Common Misconception
Why It Matters
Common Mistakes
- Not implementing the same interface as the decorated object — breaks transparent substitution.
- Decorators with too much logic — each decorator should add one distinct concern.
- Confusing Decorator (wraps an object) with Proxy (controls access) — similar structure, different intent.
- Not delegating to the wrapped object — forgetting to call parent methods breaks the chain.
Avoid When
- You need to decorate dozens of methods — every method must be proxied, creating massive boilerplate.
- The wrapped object's interface changes frequently — every signature change must be updated in all decorators.
- A simple subclass or trait would achieve the same result with less complexity.
- The decoration logic is not reusable across multiple contexts — a one-off wrapper is not worth the abstraction.
When To Use
- Adding cross-cutting concerns (logging, caching, timing, auth checks) to an existing class without modifying it.
- Building middleware pipelines where each layer wraps the next.
- Composing behaviour at runtime from interchangeable building blocks.
- Open-closed principle — extending a finalised or third-party class you cannot subclass.
Code Examples
// Behaviour added via inheritance — class explosion:
class Logger extends UserRepository {}
class CachingLogger extends Logger {}
class MetricsLogger extends CachingLogger {}
// Every combination needs a new subclass
// Decorator — composable:
$repo = new MetricsDecorator(new CachingDecorator(new UserRepository()));
interface Cache {
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): void;
}
class LoggingCache implements Cache {
public function __construct(
private Cache $inner,
private LoggerInterface $logger,
) {}
public function get(string $key): mixed {
$value = $this->inner->get($key);
$this->logger->debug('cache ' . ($value !== null ? 'hit' : 'miss') . " for $key");
return $value;
}
public function set(string $key, mixed $value, int $ttl = 3600): void {
$this->inner->set($key, $value, $ttl);
}
}
$cache = new LoggingCache(new RedisCache($redis), $logger);