PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Decorator and Proxy in PHP – structural patterns

by Henryk Tews / Monday, 23 May 2022 / Published in Wzorce projektowe

Decorator and Proxy look similar at first glance – both wrap another object and implement the same interface. The intent is different. Decorator adds new behaviour. Proxy controls access. This post covers both structural patterns in depth, with PHP implementations, concrete use cases, and a comparison that clarifies when to reach for each one.

Decorator – adding behaviour without subclassing

<?php

declare(strict_types=1);

interface LoggerInterface
{
    public function log(string $message, array $context = []): void;
}

class FileLogger implements LoggerInterface
{
    public function __construct(private string $path) {}

    public function log(string $message, array $context = []): void
    {
        file_put_contents($this->path, date('Y-m-d H:i:s') . " {$message}\n", FILE_APPEND);
    }
}

// Base Decorator - holds a reference to the wrapped logger
abstract class LoggerDecorator implements LoggerInterface
{
    public function __construct(protected LoggerInterface $logger) {}

    public function log(string $message, array $context = []): void
    {
        $this->logger->log($message, $context);
    }
}

// Adds JSON context serialisation
class ContextDecorator extends LoggerDecorator
{
    public function log(string $message, array $context = []): void
    {
        if (!empty($context)) {
            $message .= ' ' . json_encode($context);
        }
        parent::log($message, $context);
    }
}

// Adds log level prefix
class LevelDecorator extends LoggerDecorator
{
    public function __construct(LoggerInterface $logger, private string $level = 'INFO') {
        parent::__construct($logger);
    }

    public function log(string $message, array $context = []): void
    {
        parent::log("[{$this->level}] {$message}", $context);
    }
}

// Filters messages below a minimum level
class FilterDecorator extends LoggerDecorator
{
    private array $levels = ['DEBUG' => 0, 'INFO' => 1, 'WARNING' => 2, 'ERROR' => 3];

    public function __construct(LoggerInterface $logger, private string $minLevel = 'INFO') {
        parent::__construct($logger);
    }

    public function log(string $message, array $context = []): void
    {
        $msgLevel = $context['level'] ?? 'INFO';
        if (($this->levels[$msgLevel] ?? 1) >= ($this->levels[$this->minLevel] ?? 1)) {
            parent::log($message, $context);
        }
    }
}

// Stack decorators - each layer adds its feature
$logger = new FileLogger('/var/log/app.log');
$logger = new ContextDecorator($logger);
$logger = new LevelDecorator($logger, 'INFO');

$logger->log('Order placed', ['order_id' => 42, 'total' => 149.99]);
// File contains: [INFO] Order placed {"order_id":42,"total":149.99}

Proxy – controlling access

<?php

declare(strict_types=1);

interface UserRepositoryInterface
{
    public function getById(int $id): array;
    public function save(array $user): void;
    public function delete(int $id): void;
}

class DatabaseUserRepository implements UserRepositoryInterface
{
    public function getById(int $id): array { /* DB query */ return []; }
    public function save(array $user): void  { /* DB query */ }
    public function delete(int $id): void    { /* DB query */ }
}

// Caching Proxy - transparent, caller does not know about the cache
class CachingUserProxy implements UserRepositoryInterface
{
    private array $cache = [];

    public function __construct(
        private UserRepositoryInterface $realRepository
    ) {}

    public function getById(int $id): array
    {
        if (!isset($this->cache[$id])) {
            $this->cache[$id] = $this->realRepository->getById($id);
        }
        return $this->cache[$id];
    }

    public function save(array $user): void
    {
        $this->realRepository->save($user);
        unset($this->cache[$user['id']]); // invalidate cache
    }

    public function delete(int $id): void
    {
        $this->realRepository->delete($id);
        unset($this->cache[$id]);
    }
}

// Authorization Proxy - blocks unauthorised access
class AuthorizingUserProxy implements UserRepositoryInterface
{
    public function __construct(
        private UserRepositoryInterface $realRepository,
        private CurrentUser $currentUser
    ) {}

    public function getById(int $id): array
    {
        return $this->realRepository->getById($id);
    }

    public function save(array $user): void
    {
        $this->requirePermission('users.write');
        $this->realRepository->save($user);
    }

    public function delete(int $id): void
    {
        $this->requirePermission('users.delete');
        $this->realRepository->delete($id);
    }

    private function requirePermission(string $permission): void
    {
        if (!$this->currentUser->hasPermission($permission)) {
            throw new \RuntimeException("Access denied: {$permission}");
        }
    }
}

Decorator vs Proxy – key differences

Aspect Decorator Proxy
Intent Add new behaviour Control access to existing behaviour
Caller awareness Caller may know it is a decorator Caller should not know it is a proxy
Object creation Caller wraps the object Proxy often creates the real object itself
Stackable? Yes – multiple decorators Usually single proxy per object
PHP example Logger with timestamp + filter Lazy-loading, caching, auth proxy
Magento 2 Plugin system (Interceptors) Generated \Proxy DI classes

Summary

Both patterns use the same structural approach – implement the same interface, wrap an instance, delegate calls. The difference is intent: Decorator enriches, Proxy guards. In practice: reach for Decorator when you want to add logging, formatting, caching as a cross-cutting concern that the caller controls. Reach for Proxy when you want to intercept access transparently – the caller should not see the proxy at all.

About Henryk Tews

What you can read next

Iterator and Generator – lazy processing, yield, IteratorAggregate, memory benchmark
GoF patterns in Magento 2 – where to find them and how they work
Decorator pattern in PHP – composition over inheritance, cached repository example

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