The Decorator pattern allows you to dynamically add behaviour to an object without modifying its class and without inheritance. Each decorator wraps the original object and extends or modifies its behaviour. In PHP and Magento 2 it is one of the most frequently used patterns – Magento’s plugin system is effectively a Decorator implemented by the DI framework.
Basic implementation
<?php
declare(strict_types=1);
interface PriceCalculatorInterface
{
public function calculate(float $basePrice): float;
public function getDescription(): string;
}
class BasePriceCalculator implements PriceCalculatorInterface
{
public function calculate(float $basePrice): float { return $basePrice; }
public function getDescription(): string { return "Base price"; }
}
abstract class PriceDecorator implements PriceCalculatorInterface
{
public function __construct(protected PriceCalculatorInterface $calculator) {}
public function calculate(float $basePrice): float { return $this->calculator->calculate($basePrice); }
public function getDescription(): string { return $this->calculator->getDescription(); }
}
class TaxDecorator extends PriceDecorator
{
public function __construct(PriceCalculatorInterface $calculator, private float $taxRate = 0.23)
{ parent::__construct($calculator); }
public function calculate(float $basePrice): float
{ return parent::calculate($basePrice) * (1 + $this->taxRate); }
public function getDescription(): string
{ return parent::getDescription() . " + VAT " . ($this->taxRate * 100) . "%"; }
}
class DiscountDecorator extends PriceDecorator
{
public function __construct(PriceCalculatorInterface $calculator, private float $discountPercent)
{ parent::__construct($calculator); }
public function calculate(float $basePrice): float
{ return parent::calculate($basePrice) * (1 - $this->discountPercent / 100); }
public function getDescription(): string
{ return parent::getDescription() . " - {$this->discountPercent}% discount"; }
}
class ShippingDecorator extends PriceDecorator
{
public function __construct(PriceCalculatorInterface $calculator, private float $shippingCost)
{ parent::__construct($calculator); }
public function calculate(float $basePrice): float
{ return parent::calculate($basePrice) + $this->shippingCost; }
public function getDescription(): string
{ return parent::getDescription() . " + shipping {$this->shippingCost}"; }
}
$calculator = new BasePriceCalculator();
$calculator = new DiscountDecorator($calculator, 10.0);
$calculator = new TaxDecorator($calculator, 0.23);
$calculator = new ShippingDecorator($calculator, 19.99);
echo $calculator->calculate(100.0); // 100 * 0.9 * 1.23 + 19.99 = 130.69
echo $calculator->getDescription();
// "Base price - 10% discount + VAT 23% + shipping 19.99"
Decorator in Magento 2 – plugin system
<?php
// Magento 2 around plugin = Decorator pattern via DI framework
class CachingProductRepositoryPlugin
{
public function __construct(
private \Magento\Framework\Cache\FrontendInterface $cache
) {}
public function aroundGetById(
ProductRepositoryInterface $subject,
callable $proceed,
int $id
): ProductInterface {
$cacheKey = "product_{$id}";
$cached = $this->cache->load($cacheKey);
if ($cached) {
return unserialize($cached);
}
$product = $proceed($id);
$this->cache->save(serialize($product), $cacheKey, [], 3600);
return $product;
}
}
When Decorator, when inheritance
| Criterion | Decorator | Inheritance |
|---|---|---|
| Combining behaviours | Yes – any composition | No – one chain |
| Runtime decision | Yes | No – compile time |
| Modifying base class | Not needed | May require it |
| Complexity | More classes | Simpler hierarchy |
Summary
Decorator wraps an object in successive layers of behaviour without modifying the original. PHP and Magento 2 use it widely – every around plugin is a Decorator. Key advantage: decorators can be composed in any order and combination at runtime. Drawback: many small classes – harder debugging when the chain is long.
