PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Decorator pattern in PHP – composition over inheritance, cached repository example

by Henryk Tews / Tuesday, 13 August 2019 / Published in Wzorce projektowe

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.

About Henryk Tews

What you can read next

Observer pattern in PHP and the Magento 2 event system
Adapter and Facade in PHP – structural patterns
Design patterns – introduction, 23 GoF patterns table, when to use and when not to

© 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}