Decorator Design Pattern in PHP
Imagine a coffee shop app where a cup can have milk, sugar, whipped cream, and caramel — in any
combination, or none at all. If you tried to model every combination as its own subclass, you'd need
a class for CoffeeWithMilk, CoffeeWithMilkAndSugar,
CoffeeWithMilkAndSugarAndWhippedCream, and so on — a combinatorial explosion of
subclasses just to represent optional add-ons. The Decorator pattern solves this by
letting you wrap an object in layers, each layer adding one small piece of behavior, and stacking
layers to combine behaviors freely at runtime.
What Is the Decorator Pattern?
Decorator is a structural pattern that attaches new behavior to an individual object dynamically, by wrapping it in another object that implements the same interface. The wrapper (decorator) forwards calls to the object it wraps and adds its own behavior before or after the call. Because a decorator implements the same interface as what it wraps, decorators can be nested — a decorator wrapped in another decorator — and the object being decorated never has to be modified, and other instances of the same class are completely unaffected.
The Problem It Solves: Class Explosion
Using plain inheritance to represent every combination of optional features requires one subclass per combination. With just four independent add-ons (milk, sugar, whipped cream, caramel), that's up to 16 subclasses to cover every combination — and adding a fifth add-on doubles it again. Decorator avoids this entirely: each add-on becomes one small decorator class, and you combine them by nesting objects at runtime instead of writing a new subclass for every permutation.
Worked Example: Building a Coffee Order
Start with a shared interface that both the base drink and every decorator will implement:
<?php
interface Coffee
{
public function getDescription(): string;
public function getCost(): float;
}
?>
The base component — a plain cup of coffee with no add-ons:
<?php
class SimpleCoffee implements Coffee
{
public function getDescription(): string
{
return 'Coffee';
}
public function getCost(): float
{
return 2.00;
}
}
?>
An abstract base decorator holds the wrapped Coffee and, by default, just forwards to
it — concrete decorators only need to override what they actually change:
<?php
abstract class CoffeeDecorator implements Coffee
{
protected Coffee $wrapped;
public function __construct(Coffee $coffee)
{
$this->wrapped = $coffee;
}
public function getDescription(): string
{
return $this->wrapped->getDescription();
}
public function getCost(): float
{
return $this->wrapped->getCost();
}
}
?>
Now two concrete decorators, each adding one behavior on top of whatever they wrap:
<?php
class MilkDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return parent::getDescription() . ', Milk';
}
public function getCost(): float
{
return parent::getCost() + 0.50;
}
}
class SugarDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return parent::getDescription() . ', Sugar';
}
public function getCost(): float
{
return parent::getCost() + 0.25;
}
}
?>
Stack the decorators to combine behaviors — in any order, in any number:
<?php
$order = new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()
)
);
echo $order->getDescription(); // "Coffee, Milk, Sugar"
echo $order->getCost(); // 2.75
?>
$order is still just a Coffee as far as any calling code is concerned — it can be passed anywhere a Coffee is expected, with no idea it's actually three nested objects.Contrast With Plain Inheritance
With subclassing, adding a third optional add-on (say, whipped cream) to the mix above would require
writing CoffeeWithMilkAndSugarAndCream in addition to every combination that omits it —
the subclass count grows exponentially with the number of independent options. With Decorator, adding
whipped cream means writing exactly one new class, WhippedCreamDecorator, which can then
be combined with any existing decorator or base coffee without touching a single existing class.
Decorator vs. Adapter
It's easy to confuse these two because both "wrap" another object, but they solve different problems.
Adapter changes an object's interface into a
different one that client code expects — the wrapped class and the adapter usually have
unrelated method signatures. Decorator keeps the exact same interface as what it wraps and
simply adds behavior around it — a MilkDecorator is still a Coffee, indistinguishable
from the outside except for what getCost() and getDescription() now return.
Where You've Seen This Before
PHP's own SPL stream filters and libraries like Guzzle's HTTP handler stacks or PSR-15 middleware pipelines all use the same idea: wrap a core object in layers, each layer adding logging, caching, retries, or authentication, without changing the core object or the layers around it.