PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Decorator i Proxy w PHP – wzorce strukturalne

by Henryk Tews / poniedziałek, 23 maja 2022 / Opublikowano w Wzorce projektowe


Decorator i Proxy to dwa wzorce strukturalne które działają podobnie – oba opakowują obiekt i implementują ten sam interfejs. Różni je intencja: Decorator dodaje nową funkcjonalność, Proxy kontroluje dostęp do obiektu. Subtelna różnica, ale fundamentalna dla prawidłowego zastosowania.

Decorator

Decorator „opakowuje” obiekt w kolejne warstwy funkcjonalności. Każda warstwa dodaje coś nowego, a klient widzi ten sam interfejs przez wszystkie warstwy.

<?php

declare(strict_types=1);

// Interfejs komponentu
interface TextFormatterInterface
{
    public function format(string $text): string;
}

// Komponent bazowy - podstawowa implementacja
class PlainTextFormatter implements TextFormatterInterface
{
    public function format(string $text): string
    {
        return $text;
    }
}

// Bazowy dekorator - implementuje interfejs i opakowuje komponent
abstract class TextDecorator implements TextFormatterInterface
{
    public function __construct(
        protected TextFormatterInterface $formatter
    ) {}

    public function format(string $text): string
    {
        return $this->formatter->format($text);
    }
}

// Konkretne dekoratory
class UpperCaseDecorator extends TextDecorator
{
    public function format(string $text): string
    {
        return strtoupper(parent::format($text));
    }
}

class TrimDecorator extends TextDecorator
{
    public function format(string $text): string
    {
        return trim(parent::format($text));
    }
}

class HtmlEscapeDecorator extends TextDecorator
{
    public function format(string $text): string
    {
        return htmlspecialchars(parent::format($text), ENT_QUOTES, 'UTF-8');
    }
}

class WrapInTagDecorator extends TextDecorator
{
    public function __construct(
        TextFormatterInterface $formatter,
        private string $tag,
        private string $class = ''
    ) {
        parent::__construct($formatter);
    }

    public function format(string $text): string
    {
        $formatted = parent::format($text);
        $class     = $this->class ? " class=\"{$this->class}\"" : '';
        return "<{$this->tag}{$class}>{$formatted}tag}>";
    }
}
<?php

// Składanie dekoratorów - kolejność ma znaczenie
$userInput = '  <script>alert("xss")</script>  ';

// Najpierw trim, potem escape, potem wrap w paragraf
$formatter = new WrapInTagDecorator(
    new HtmlEscapeDecorator(
        new TrimDecorator(
            new PlainTextFormatter()
        )
    ),
    tag: 'p',
    class: 'user-content'
);

echo $formatter->format($userInput);
// <p class="user-content">&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>

// Inna kombinacja - uppercase + wrap w nagłówek
$headingFormatter = new WrapInTagDecorator(
    new UpperCaseDecorator(
        new TrimDecorator(
            new PlainTextFormatter()
        )
    ),
    tag: 'h1'
);

echo $headingFormatter->format('  witaj świecie  ');
// <h1>WITAJ ŚWIECIE</h1>

Praktyczny przykład z loggingiem – Decorator dodaje logowanie do istniejącego serwisu bez jego modyfikacji:

<?php

declare(strict_types=1);

interface UserRepositoryInterface
{
    public function findById(int $id): ?array;
    public function save(array $user): int;
    public function delete(int $id): bool;
}

class DatabaseUserRepository implements UserRepositoryInterface
{
    public function findById(int $id): ?array { /* ... */ return ['id' => $id, 'name' => 'Jan']; }
    public function save(array $user): int    { /* ... */ return 1; }
    public function delete(int $id): bool     { /* ... */ return true; }
}

// Dekorator dodający logowanie - zero zmian w DatabaseUserRepository
class LoggingUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function findById(int $id): ?array
    {
        $this->logger->debug("UserRepository::findById({$id})");
        $result = $this->repository->findById($id);
        $this->logger->debug('Result: ' . ($result ? 'found' : 'not found'));
        return $result;
    }

    public function save(array $user): int
    {
        $this->logger->info('Saving user', ['name' => $user['name'] ?? 'unknown']);
        $id = $this->repository->save($user);
        $this->logger->info("User saved with ID: {$id}");
        return $id;
    }

    public function delete(int $id): bool
    {
        $this->logger->warning("Deleting user {$id}");
        $result = $this->repository->delete($id);
        $this->logger->info("User {$id} deleted: " . ($result ? 'ok' : 'failed'));
        return $result;
    }
}

Proxy

Proxy stawia pośrednika między klientem a prawdziwym obiektem. Pośrednik implementuje ten sam interfejs i może: opóźniać inicjalizację (lazy), kontrolować dostęp (protection), keszować wyniki (caching), logować wywołania (logging).

<?php

declare(strict_types=1);

interface ImageInterface
{
    public function display(): void;
    public function getSize(): int;
}

// Prawdziwy obiekt - ciężka inicjalizacja (ładowanie pliku z dysku)
class RealImage implements ImageInterface
{
    private string $imageData;

    public function __construct(private string $filename)
    {
        echo "Ładowanie pliku: {$filename}\n"; // symulacja ciężkiej operacji
        $this->imageData = file_get_contents($filename) ?: '';
    }

    public function display(): void
    {
        echo "Wyświetlam: {$this->filename} (" . strlen($this->imageData) . " bajtów)\n";
    }

    public function getSize(): int
    {
        return strlen($this->imageData);
    }
}

// Lazy Proxy - tworzy prawdziwy obiekt dopiero przy pierwszym użyciu
class LazyImageProxy implements ImageInterface
{
    private ?RealImage $realImage = null;

    public function __construct(
        private string $filename
    ) {
        // Konstruktor lekki - nic nie ładuje
        echo "Proxy dla: {$filename} (plik nie załadowany)\n";
    }

    private function load(): RealImage
    {
        if ($this->realImage === null) {
            $this->realImage = new RealImage($this->filename);
        }

        return $this->realImage;
    }

    public function display(): void
    {
        $this->load()->display(); // ładuje plik dopiero tutaj
    }

    public function getSize(): int
    {
        return $this->load()->getSize();
    }
}
<?php

// Proxy kontrolujące dostęp - Protection Proxy
class SecureUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private string $currentUserRole
    ) {}

    public function findById(int $id): ?array
    {
        // Każdy może czytać
        return $this->repository->findById($id);
    }

    public function save(array $user): int
    {
        // Zapis tylko dla adminów i edytorów
        if (!in_array($this->currentUserRole, ['admin', 'editor'], true)) {
            throw new \RuntimeException('Access denied: cannot save user');
        }

        return $this->repository->save($user);
    }

    public function delete(int $id): bool
    {
        // Usuwanie tylko dla adminów
        if ($this->currentUserRole !== 'admin') {
            throw new \RuntimeException('Access denied: cannot delete user');
        }

        return $this->repository->delete($id);
    }
}

// Proxy keszujące - Caching Proxy
class CachingUserRepository implements UserRepositoryInterface
{
    private array $cache = [];

    public function __construct(
        private UserRepositoryInterface $repository
    ) {}

    public function findById(int $id): ?array
    {
        if (!isset($this->cache[$id])) {
            $this->cache[$id] = $this->repository->findById($id);
        }

        return $this->cache[$id];
    }

    public function save(array $user): int
    {
        $id = $this->repository->save($user);
        unset($this->cache[$id]); // unieważnij cache po zapisie
        return $id;
    }

    public function delete(int $id): bool
    {
        $result = $this->repository->delete($id);
        unset($this->cache[$id]);
        return $result;
    }
}

Decorator vs Proxy – kluczowa różnica

Aspekt Decorator Proxy
Cel Dodaje nową funkcjonalność Kontroluje dostęp do istniejącej
Inicjalizacja obiektu Dostaje gotowy obiekt z zewnątrz Może sam tworzyć opakowany obiekt
Liczba warstw Wiele – warstwy się składają Zwykle jedna
Typowe użycie Logging, formatowanie, walidacja Lazy loading, cache, auth

Podsumowanie

Decorator i Proxy to eleganckie wzorce które pozwalają dodawać zachowanie do obiektów bez modyfikacji ich kodu. Decorator skaluje się przez składanie warstw – możesz mieć dziesięć dekoratorów na jednym obiekcie. Proxy jest bardziej wyspecjalizowane – zazwyczaj jeden pośrednik z konkretnym zadaniem: opóźnienie inicjalizacji, cache albo kontrola uprawnień.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Memento – undo/redo, historia zmian cen, persystencja do bazy jako audit log
Iterator i Generator – leniwe przetwarzanie, yield, IteratorAggregate, benchmark pamięci
Event Sourcing – Domain Events, Aggregate Root, Event Store, połączenie z CQRS
  • 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}