Inheritance is the simplest way to extend a class – but not always the best. When you want to add several independent features to an object, a class hierarchy quickly becomes unreadable. Decorator lets you wrap objects in layers of functionality without modifying the original and without deep inheritance.
The problem – subclass explosion
<?php
// Combination explosion through inheritance
class Logger {}
class FileLogger extends Logger {}
class DatabaseLogger extends Logger {}
class JsonFileLogger extends FileLogger {}
class JsonDatabaseLogger extends DatabaseLogger {}
class FileAndDatabaseLogger extends Logger {} // how to handle this?
With 3 independent features you already have a problem. Decorator solves this through composition instead of inheritance.
Implementing the Decorator pattern
The key element: a Decorator implements the same interface as the decorated object and holds a reference to it:
<?php
interface LoggerInterface
{
public function log(string $level, string $message, array $context = []): void;
}
class SimpleLogger implements LoggerInterface
{
public function log(string $level, string $message, array $context = []): void
{
echo "[{$level}] {$message}" . PHP_EOL;
}
}
abstract class LoggerDecorator implements LoggerInterface
{
public function __construct(protected LoggerInterface $logger) {}
public function log(string $level, string $message, array $context = []): void
{
$this->logger->log($level, $message, $context);
}
}
class TimestampDecorator extends LoggerDecorator
{
public function log(string $level, string $message, array $context = []): void
{
parent::log($level, '[' . date('Y-m-d H:i:s') . '] ' . $message, $context);
}
}
class LevelFilterDecorator extends LoggerDecorator
{
public function __construct(LoggerInterface $logger, private array $allowedLevels)
{
parent::__construct($logger);
}
public function log(string $level, string $message, array $context = []): void
{
if (in_array($level, $this->allowedLevels, true)) {
parent::log($level, $message, $context);
}
}
}
class FileDecorator extends LoggerDecorator
{
public function __construct(LoggerInterface $logger, private string $filePath)
{
parent::__construct($logger);
}
public function log(string $level, string $message, array $context = []): void
{
parent::log($level, $message, $context);
file_put_contents($this->filePath, "[{$level}] {$message}" . PHP_EOL, FILE_APPEND);
}
}
Stacking decorators – each layer adds its own feature:
<?php
$logger = new SimpleLogger();
$logger = new TimestampDecorator($logger);
$logger = new LevelFilterDecorator($logger, ['error', 'critical']);
$logger = new FileDecorator($logger, '/var/log/app.log');
$logger->log('info', 'User logged in'); // blocked by LevelFilter
$logger->log('error', 'Database connection error'); // passes through all layers
Decorator in Magento 2 – cached repository
<?php
namespace Vendor\Module\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
class CachedProductRepository implements ProductRepositoryInterface
{
private array $cache = [];
public function __construct(
private ProductRepositoryInterface $productRepository
) {}
public function getById(
int $productId,
bool $editMode = false,
?int $storeId = null,
bool $forceReload = false
): ProductInterface {
$cacheKey = "{$productId}_{$storeId}";
if (!$forceReload && isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
$product = $this->productRepository->getById($productId, $editMode, $storeId, $forceReload);
$this->cache[$cacheKey] = $product;
return $product;
}
public function get(string $sku, bool $editMode = false, ?int $storeId = null, bool $forceReload = false): ProductInterface
{
return $this->productRepository->get($sku, $editMode, $storeId, $forceReload);
}
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
{
return $this->productRepository->getList($searchCriteria);
}
public function save(ProductInterface $product): ProductInterface
{
return $this->productRepository->save($product);
}
public function delete(ProductInterface $product): bool
{
return $this->productRepository->delete($product);
}
public function deleteById(string $sku): bool
{
return $this->productRepository->deleteById($sku);
}
}
<!-- Register via preference in di.xml -->
<preference for="Magento\Catalog\Api\ProductRepositoryInterface"
type="Vendor\Module\Model\CachedProductRepository"/>
Summary
Decorator promotes composition over inheritance. Instead of a deep class hierarchy, you build an object from interchangeable layers. In Magento 2 the plugin system does exactly the same thing – automatically and through XML configuration.
