PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec Decorator w PHP – kompozycja zamiast dziedziczenia, przykład z cache repository

by Henryk Tews / wtorek, 13 sierpnia 2019 / Opublikowano w Wzorce projektowe

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.

About Henryk Tews

Co możesz przeczytać następne

Chain of Responsibility – łańcuch walidatorów, konfiguracja przez di.xml z sortOrder
Wzorzec Observer w PHP i system zdarzeń Magento 2
Command i Chain of Responsibility w PHP – wzorce behawioralne
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}