Wzorzec Decorator pozwala dynamicznie dodawać zachowanie do obiektu bez modyfikacji jego klasy i bez dziedziczenia. Każdy dekorator opakowuje oryginalny obiekt i rozszerza lub modyfikuje jego działanie. W PHP i Magento 2 to jeden z najczęściej stosowanych wzorców – system pluginów Magento to de facto Decorator zaimplementowany przez framework DI.
Implementacja podstawowa
<?php
declare(strict_types=1);
// Interfejs komponentu
interface PriceCalculatorInterface
{
public function calculate(float $basePrice): float;
public function getDescription(): string;
}
// Konkretny komponent
class BasePriceCalculator implements PriceCalculatorInterface
{
public function calculate(float $basePrice): float
{
return $basePrice;
}
public function getDescription(): string
{
return "Cena bazowa";
}
}
// Abstrakcyjny dekorator
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();
}
}
// Konkretne dekoratory
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
{
$price = parent::calculate($basePrice);
return $price * (1 - $this->discountPercent / 100);
}
public function getDescription(): string
{
return parent::getDescription() . " - {$this->discountPercent}% rabat";
}
}
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() . " + dostawa {$this->shippingCost} PLN";
}
}
// Kompozycja dekoratorów
$calculator = new BasePriceCalculator();
$calculator = new DiscountDecorator($calculator, 10.0); // -10%
$calculator = new TaxDecorator($calculator, 0.23); // +23% VAT
$calculator = new ShippingDecorator($calculator, 19.99); // +dostawa
echo $calculator->calculate(100.0); // 100 * 0.9 * 1.23 + 19.99 = 130.69
echo $calculator->getDescription();
// "Cena bazowa - 10% rabat + VAT 23% + dostawa 19.99 PLN"
Decorator w Magento 2 – system pluginów
<?php
// Magento 2 around plugin = Decorator pattern przez DI framework
// Oryginalny serwis
class ProductRepository implements ProductRepositoryInterface
{
public function getById(int $id): ProductInterface
{
return $this->resource->load($id);
}
}
// Plugin (Decorator) - dodaje cache
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); // wywołaj oryginalną metodę
$this->cache->save(serialize($product), $cacheKey, [], 3600);
return $product;
}
}
// di.xml - rejestracja pluginu (DI robi resztę)
// <type name="Magento\Catalog\Model\ProductRepository">
// <plugin name="caching" type="Vendor\Module\Plugin\CachingProductRepositoryPlugin"/>
// </type>
Kiedy Decorator, kiedy dziedziczenie
| Kryterium | Decorator | Dziedziczenie |
|---|---|---|
| Kombinowanie zachowań | Tak – dowolna kompozycja | Nie – jeden łańcuch |
| Runtime decyzja | Tak | Nie – compile time |
| Modyfikacja klasy bazowej | Nie potrzeba | Może wymagać |
| Złożoność | Więcej klas | Prostsza hierarchia |
Podsumowanie
Decorator to otoczenie obiektu w kolejne warstwy zachowania bez modyfikacji oryginału. PHP i Magento 2 stosują go powszechnie – każdy around plugin to Decorator. Kluczowa zaleta: dekoratory można komponować w dowolnej kolejności i kombinacji w runtime. Wada: wiele małych klas – trudniejsze debugowanie gdy łańcuch jest długi.
