The Decorator Pattern in PHP: Extending Behavior at Runtime Instead of Inheriting
AI generated
<?php
8.4
PHP · OOP Patterns · Decorator Pattern · Composition
The Decorator Pattern in PHP
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.

18 min read Decorator · Composition over Inheritance · Wrapper PHP 8.2 · 8.3 · 8.4

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.

11. FAQ: Decorator Pattern in PHP

1Decorator vs. inheritance?
Inheritance fixes combinations at compile time. Decorator combines behavior at runtime without new classes.
2Same interface mandatory?
Yes, basic requirement. Only that way can decorators be nested and swapped arbitrarily.
3Does order matter?
Yes, significantly. Caching inside and logging outside logs cache hits too, reversed it does not.
4Decorator vs. proxy?
Structurally almost identical, difference lies in intent: access control for proxy, behavior extension for decorator.
5Abstract base class purpose?
Handles forwarding all methods, concrete decorators only override what they actually want to change.
6PSR-15 middleware an example?
Yes, every middleware wraps the next and implements the same interface, exactly the decorator principle.
7Exceptions in the stack?
Propagate outward unless a decorator catches them, for example a retry decorator.
8When to prefer inheritance?
For a genuine is-a relationship. For independent additional behavior, decorator is the better choice.
9Most common mistake?
instanceof checks against concrete decorator classes break the interface's interchangeability.
10Performance impact?
Every decorator is one extra method call, usually negligible in practice.