Dziedziczenie to najprostszy sposób na rozszerzenie klasy – ale nie zawsze najlepszy. Gdy chcesz dodać kilka niezależnych funkcji do obiektu, hierarchia klas szybko staje się nieczytelna. Decorator pozwala „owijać” obiekty w kolejne warstwy funkcjonalności bez modyfikacji oryginału i bez głębokiego dziedziczenia.
Problem – eksplozja podklas
Wyobraź sobie klasę Logger, do której chcesz dodać: zapis do pliku, zapis do bazy danych i formatowanie JSON. Przez dziedziczenie potrzebujesz osobnych klas dla każdej kombinacji:
<?php
// Eksplozja kombinacji przez dziedziczenie
class Logger {}
class FileLogger extends Logger {}
class DatabaseLogger extends Logger {}
class JsonFileLogger extends FileLogger {}
class JsonDatabaseLogger extends DatabaseLogger {}
class FileAndDatabaseLogger extends Logger {} // jak to obsłużyć?
Przy 3 niezależnych funkcjach masz już problem. Decorator rozwiązuje to przez kompozycję zamiast dziedziczenia.
Implementacja wzorca Decorator
Kluczowy element: Decorator implementuje ten sam interfejs co dekorowany obiekt i trzyma referencję do niego:
<?php
// Wspólny interfejs
interface LoggerInterface
{
public function log(string $level, string $message, array $context = []): void;
}
// Konkretna bazowa implementacja
class SimpleLogger implements LoggerInterface
{
public function log(string $level, string $message, array $context = []): void
{
echo "[{$level}] {$message}" . PHP_EOL;
}
}
// Bazowy Decorator – implementuje interfejs i opakowuje inny logger
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);
}
}
Konkretne dekoratory dodają funkcjonalność przed lub po wywołaniu opakowanego obiektu:
<?php
// Dekorator dodający timestamp
class TimestampDecorator extends LoggerDecorator
{
public function log(string $level, string $message, array $context = []): void
{
$timestamp = date('Y-m-d H:i:s');
parent::log($level, "[{$timestamp}] {$message}", $context);
}
}
// Dekorator filtrujący wiadomości poniżej określonego poziomu
class LevelFilterDecorator extends LoggerDecorator
{
private array $allowedLevels;
public function __construct(LoggerInterface $logger, array $allowedLevels)
{
parent::__construct($logger);
$this->allowedLevels = $allowedLevels;
}
public function log(string $level, string $message, array $context = []): void
{
if (!in_array($level, $this->allowedLevels, true)) {
return; // pomiń wiadomości o nieodpowiednim poziomie
}
parent::log($level, $message, $context);
}
}
// Dekorator zapisujący do pliku
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
);
}
}
Składanie dekoratorów – każda warstwa dodaje swoją funkcję:
<?php
// Budujemy logger warstwowo - od wewnątrz na zewnątrz
$logger = new SimpleLogger();
$logger = new TimestampDecorator($logger);
$logger = new LevelFilterDecorator($logger, ['error', 'critical']);
$logger = new FileDecorator($logger, '/var/log/app.log');
// Teraz log() przechodzi przez wszystkie warstwy
$logger->log('info', 'Użytkownik zalogowany'); // zablokowane przez LevelFilter
$logger->log('error', 'Błąd połączenia z bazą'); // przechodzi przez wszystko
Decorator w Magento 2 – pluginy jako dekoratory
System pluginów Magento 2 to w istocie implementacja wzorca Decorator. Magento generuje klasy Interceptor, które owijają oryginalną klasę i wywołują pluginy w odpowiedniej kolejności – dokładnie tak jak dekoratory. Ale możesz też użyć klasycznego Decoratora przez DI:
<?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;
// Dekorator repozytorium produktów z wbudowanym cache
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;
}
// Pozostałe metody interfejsu delegują do oryginalnego repozytorium
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);
}
}
Rejestracja przez preference w di.xml:
<?xml version="1.0"?>
<config>
<preference for="Magento\Catalog\Api\ProductRepositoryInterface"
type="Vendor\Module\Model\CachedProductRepository"/>
</config>
Podsumowanie
Decorator to wzorzec, który promuje kompozycję nad dziedziczenie. Zamiast tworzyć głęboką hierarchię klas, budujesz obiekt z wymiennych warstw. W PHP świetnie sprawdza się przy loggerach, cache, walidatorach i transformatorach danych. W Magento 2 system pluginów robi dokładnie to samo – automatycznie i przez konfigurację XML.
