PHP / Magento Dev Blog

  • Home

Decorator pattern – dynamic behaviour, PHP, Magento 2 plugin system

by Henryk Tews / Friday, 21 August 2026 / Published in Wzorce projektowe

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.

About Henryk Tews

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}