Extending Behavior at Runtime Instead of Inheriting
The decorator pattern solves a problem that plain inheritance cannot cleanly solve: combining several independent additional behaviors, for example logging, caching and validation, flexibly and in any order, without building a separate subclass for every combination. This article shows how the decorator pattern in PHP preserves interface conformance and when it is preferable to inflating inheritance hierarchies.
Table of Contents
- 1. What problem the decorator pattern actually solves
- 2. Basic structure: component interface, concrete component, decorator
- 3. Practical example: logging and caching decorator for a service
- 4. Stacking decorators: order and its consequences
- 5. Distinguishing it from inheritance: why composition wins
- 6. The abstract decorator as a base class
- 7. Decorator pattern in practice: PSR middleware and HTTP clients
- 8. Common mistakes with the decorator pattern
- 9. Decorator compared with related patterns
- 10. Summary
- 11. FAQ
1. What problem the decorator pattern actually solves
The decorator pattern solves a very concrete problem: how do you add extra behavior to an object without changing the class itself and without creating a separate subclass for every possible combination of extra behavior? A classic example is a payment service that should optionally be logged, cached and validated. With plain inheritance you would need classes like LoggingCachingPaymentService, CachingPaymentService, LoggingPaymentService and so on, a combinatorial explosion that gets worse with every new additional behavior.
The decorator pattern sidesteps this problem by modeling additional behavior as standalone, interchangeable wrapper objects that all implement the same interface as the object they wrap. Every decorator holds a reference to an object of the same interface type, calls its method, and adds its own behavior before, after, or around the call. From the caller's perspective, a decorated object is indistinguishable from an undecorated one, both implement the same interface.
The name comes from the analogy to decorating: a Christmas tree stays a tree no matter how many string lights and baubles you add, and every decoration can be removed independently without changing the tree itself. The decorator pattern transfers this principle to objects and makes composition over inheritance tangible, rather than just something you cite as an abstract principle.
2. Basic structure: component interface, concrete component, decorator
The structure of the decorator pattern consists of three parts. First, a component interface that defines the method to be decorated. Second, a concrete component class that implements this interface with the actual base functionality. Third, one or more decorator classes that also implement the component interface, but additionally accept an instance of the same interface type in their constructor and store it internally.
The crucial trick is that a decorator itself counts as a component interface again. This allows decorators to be nested arbitrarily deep, a decorator can wrap another decorator object, which in turn wraps the concrete component. This nesting capability is the core of the decorator pattern and distinguishes it from a mere wrapper class that only provides one fixed, non combinable extra function.
<?php
declare(strict_types=1);
/**
* Component interface shared by the concrete service and every decorator.
*/
interface PaymentServiceInterface
{
public function charge(int $amountCents, string $currency): bool;
}
/**
* ConcreteComponent: the actual base implementation.
*/
final class StripePaymentService implements PaymentServiceInterface
{
public function charge(int $amountCents, string $currency): bool
{
// Simplified: real implementation talks to the Stripe API
return $amountCents > 0;
}
}
These two classes form the foundation. Without any decorator, StripePaymentService already works fully on its own. Only in the next section do the actual decorators come in, adding extra behavior around this base implementation without touching it at all.
3. Practical example: logging and caching decorator for a service
The practical benefit of the decorator pattern is most obvious in a concrete example with two independent additional behaviors. A logging decorator records every call along with its parameters and result, regardless of which concrete service sits behind it. A caching decorator avoids repeated expensive calls by memoizing results for identical parameters. Both decorators implement the same PaymentServiceInterface as the service itself and can be applied independently to any concrete service.
What matters with the decorator pattern is that every decorator communicates with the wrapped object exclusively through the interface, never through concrete classes. That way, a logging decorator stays fully independent of whether it wraps a real StripePaymentService or another decorator. This interface conformance is the basic requirement that lets decorators be combined arbitrarily.
<?php
declare(strict_types=1);
/**
* Decorator: adds logging around any PaymentServiceInterface implementation.
*/
final class LoggingPaymentDecorator implements PaymentServiceInterface
{
public function __construct(
private readonly PaymentServiceInterface $inner,
) {
}
public function charge(int $amountCents, string $currency): bool
{
error_log(sprintf('Charging %d %s', $amountCents, $currency));
$result = $this->inner->charge($amountCents, $currency);
error_log(sprintf('Charge result: %s', $result ? 'success' : 'failure'));
return $result;
}
}
/**
* Decorator: caches successful charges for identical parameters.
*/
final class CachingPaymentDecorator implements PaymentServiceInterface
{
/** @var array<string, bool> */
private array $cache = [];
public function __construct(
private readonly PaymentServiceInterface $inner,
) {
}
public function charge(int $amountCents, string $currency): bool
{
$key = $amountCents . '|' . $currency;
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
}
return $this->cache[$key] = $this->inner->charge($amountCents, $currency);
}
}
// Decorators are stacked around the concrete service
$service = new LoggingPaymentDecorator(
new CachingPaymentDecorator(
new StripePaymentService(),
),
);
$service->charge(2500, 'EUR');
At the end, the caller only instantiates a chain of constructor calls, but only ever sees the shared PaymentServiceInterface. Whether one, two or five decorators are involved makes no difference to code that calls $service->charge(...). This transparency is exactly the central advantage of the decorator pattern over any inheritance based solution.
4. Stacking decorators: order and its consequences
The order in which decorators are stacked is not a formality, it changes the actual behavior. In the example above, caching sits inside and logging sits outside, meaning every call is logged, even when the result comes from the cache, but the actual StripePaymentService call is skipped on a cache hit. If you reversed the order, logging inside and caching outside, a cached result would never even reach the logging decorator, because the caching decorator skips the inner call entirely.
This order dependency needs to be planned deliberately every time you use the decorator pattern. As a rule of thumb, decorators that might prevent the actual call entirely, like caching or rate limiting, should sit further inside than decorators providing observation behavior like logging or metrics, unless the intended behavior is explicitly that skipped calls should also not be observed.
A second detail concerns exceptions. If the innermost service throws an exception, it propagates outward through every wrapping decorator, unless a decorator explicitly catches it. A retry decorator, for instance, would do exactly that: catch the exception from the inner call, retry the call several times, and only rethrow the exception once the attempts are exhausted. Here too, the position in the stack determines which other decorators are affected by this retry behavior.
5. Distinguishing it from inheritance: why composition wins
A direct comparison to inheritance makes clear why the decorator pattern is the better choice in most cases. With inheritance, the set of possible combinations is fixed at compile time, every new combination requires a new class. With the decorator pattern, the combination arises at runtime through the order of constructor calls, without any new classes at all. A configuration file or a DI container can even decide which decorators are applied in which order, without the PHP code itself needing to change.
Inheritance still works well for an is-a relationship, where a subclass really is a more specialized variant of the parent class. The decorator pattern is suited to a has-a relationship, where extra behavior exists independently of the core functionality and should potentially be applied to any implementation of the same interface. Anyone who confuses these two situations and models additional behavior through inheritance instead of the decorator pattern ends up with rigid class hierarchies that become harder to extend with every new requirement.
6. The abstract decorator as a base class
As soon as a project needs several decorators for the same interface, an abstract base class that handles delegating to the wrapped object for every method that is not overridden becomes worthwhile. This matters especially for interfaces with multiple methods, because without a base class every decorator would have to explicitly forward all interface methods, even if it only actually wants to change a single one.
<?php
declare(strict_types=1);
interface NotifierInterface
{
public function send(string $recipient, string $message): bool;
public function supports(string $channel): bool;
}
/**
* Abstract decorator: forwards every call by default, subclasses override selectively.
*/
abstract class NotifierDecorator implements NotifierInterface
{
public function __construct(
protected readonly NotifierInterface $inner,
) {
}
public function send(string $recipient, string $message): bool
{
return $this->inner->send($recipient, $message);
}
public function supports(string $channel): bool
{
return $this->inner->supports($channel);
}
}
/**
* Concrete decorator: only overrides send(), supports() is inherited unchanged.
*/
final class RateLimitedNotifierDecorator extends NotifierDecorator
{
private int $sentInLastMinute = 0;
public function send(string $recipient, string $message): bool
{
if ($this->sentInLastMinute >= 10) {
return false;
}
$this->sentInLastMinute++;
return parent::send($recipient, $message);
}
}
This abstract base class saves considerable code in larger systems with many decorators, because every concrete decorator only needs to override the methods it actually wants to change. The decorator pattern itself remains unchanged in principle, the base class is purely an implementation aid.
7. Decorator pattern in practice: PSR middleware and HTTP clients
The decorator pattern is ubiquitous in modern PHP ecosystems, even when it is not always explicitly called that. PSR-15 middleware in frameworks like Slim or Mezzio follows this exact pattern: every middleware implements the same interface, wraps the next middleware in the chain, and can add behavior before or after the actual request handling, for example authentication, CORS headers, or request logging. Guzzle handler stacks also use the decorator pattern to wrap middleware like retry logic or request signing around the actual HTTP client.
This widespread use shows that the decorator pattern is not an academic construct, but the foundation of many production PHP libraries. Anyone who understands how a simple logging decorator works also understands the basic structure of PSR-15 middleware pipelines, just with different method names and a ServerRequestInterface instead of a simple charge() call.
8. Common mistakes with the decorator pattern
The most common mistake is a decorator offering additional public methods outside the interface that the caller ends up depending on. As soon as code explicitly accesses the concrete decorator class instead of the interface, you lose the interchangeability that justifies the whole pattern in the first place. A second mistake is holding state in the decorator that actually belongs to the concrete component, for example configuration values that should be valid regardless of how many decorators are currently active.
A third, more subtle mistake concerns instanceof type checks against a concrete decorator class inside caller code. That breaks the abstraction, because the caller then has to know which concrete decorators currently sit in the stack instead of relying exclusively on the interface. If a caller really needs to know whether a particular decorator is active, that is often a sign that this information should instead be carried through the interface itself, for example an additional method.
9. Decorator compared with related patterns
The decorator pattern is frequently confused with structurally similar patterns that pursue different goals. The following table contrasts the most important differences.
| Pattern | Goal | Same interface? | Combinable |
|---|---|---|---|
| Decorator | Add behavior at runtime | Yes | Stackable arbitrarily |
| Adapter | Bridge incompatible interfaces | No | Usually one-off |
| Proxy | Access control, lazy loading | Yes | Usually one-off |
| Strategy | Make an algorithm swappable | Own strategy interface | Not stackable |
| Middleware pipeline | Extend request processing | Yes | Stackable arbitrarily |
The key difference between decorator and proxy is intent: a proxy typically controls access to an object, for example through lazy loading or access rights, while a decorator deliberately adds extra behavior. Technically, both patterns are structured almost identically, the difference lies in the purpose, not the structure.
Mironsoft
PHP architecture, object design and maintainable backend systems
Inflated inheritance hierarchies instead of flexible composition?
We analyze existing PHP class hierarchies and build decorator based solutions for logging, caching, retry logic and validation that can be combined flexibly.
Architecture review
Check inheritance hierarchies for decorator potential
Decorator implementation
Build clean logging, caching and retry decorators
DI container configuration
Wire the decorator stack centrally and make it configurable
10. Summary
The decorator pattern solves the combinatorial explosion that plain inheritance creates with several independent additional behaviors. Instead of building a separate subclass for every combination, all decorators implement the same interface as the concrete component and can be stacked around it in any order. The order in the stack is not a formality, it determines which decorator actually affects which other, especially with caching, rate limiting, and retry logic.
An abstract decorator base class saves code for interfaces with many methods by handling forwarding to the wrapped object. In modern PHP applications, the decorator pattern shows up practically everywhere PSR-15 middleware or Guzzle handler stacks are used. Anyone who understands the decorator pattern also understands the basic structure of these widely used tools.
The Decorator Pattern in PHP — Key Takeaways
Basic structure
Component interface, concrete component and one or more decorators, all implementing the same interface.
Composition over inheritance
Additional behavior is combined at runtime instead of creating one subclass per combination.
Order matters
The stacking order determines which decorator affects which other, especially with caching and retry.
Real-world relevance
PSR-15 middleware and Guzzle handler stacks are built on the same principle.