PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Adapter i Facade w PHP – wzorce strukturalne

by Henryk Tews / wtorek, 31 maja 2022 / Opublikowano w Wzorce projektowe

Adapter i Facade to dwa wzorce które rozwiązują problem złożoności interfejsów – ale z różnych stron. Adapter sprawia że niekompatybilny interfejs staje się kompatybilny. Facade upraszcza złożony interfejs do prostego. Oba pojawiają się często przy integracji z zewnętrznymi bibliotekami i API.

Adapter

Adapter konwertuje interfejs jednej klasy na interfejs którego oczekuje klient. To „przejściówka” która sprawia że dwie niekompatybilne klasy mogą ze sobą współpracować.

<?php

declare(strict_types=1);

// Target - interfejs którego oczekuje nasz kod
interface LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void;
    public function info(string $message, array $context = []): void;
    public function error(string $message, array $context = []): void;
    public function debug(string $message, array $context = []): void;
}

// Adaptee - zewnętrzna biblioteka z innym interfejsem (której nie możemy zmienić)
class LegacyLogger
{
    public function write(int $severity, string $msg): void
    {
        $levels = [1 => 'DEBUG', 2 => 'INFO', 3 => 'ERROR'];
        $label  = $levels[$severity] ?? 'UNKNOWN';
        echo "[{$label}] {$msg}\n";
    }

    public function writeWithTimestamp(int $severity, string $msg, string $timestamp): void
    {
        echo "[{$timestamp}] [{$severity}] {$msg}\n";
    }
}

// Adapter - opakowuje LegacyLogger i udostępnia LoggerInterface
class LegacyLoggerAdapter implements LoggerInterface
{
    private const LEVELS = [
        'debug' => 1,
        'info'  => 2,
        'error' => 3,
    ];

    public function __construct(
        private LegacyLogger $legacyLogger
    ) {}

    public function log(string $level, string $message, array $context = []): void
    {
        $severity = self::LEVELS[strtolower($level)] ?? 2;
        $msg      = $this->interpolate($message, $context);
        $this->legacyLogger->write($severity, $msg);
    }

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

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

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

    private function interpolate(string $message, array $context): string
    {
        foreach ($context as $key => $value) {
            $message = str_replace("{{$key}}", (string) $value, $message);
        }

        return $message;
    }
}

// Klient - używa LoggerInterface, nie wie że pod spodem jest LegacyLogger
class OrderService
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function placeOrder(int $orderId): void
    {
        $this->logger->info('Placing order {id}', ['id' => $orderId]);

        try {
            // logika zamówienia
            $this->logger->debug('Order processed', ['order_id' => $orderId]);
        } catch (\Exception $e) {
            $this->logger->error('Order failed: {message}', ['message' => $e->getMessage()]);
        }
    }
}

// Podmiana loggera przez adapter - zero zmian w OrderService
$adapter = new LegacyLoggerAdapter(new LegacyLogger());
$service = new OrderService($adapter);
$service->placeOrder(42);

Adapter z dziedziczeniem zamiast kompozycji (Class Adapter) – mniej elastyczny, ale prostszy gdy adaptee nie ma interfejsu:

<?php

// Adapter przez dziedziczenie (Class Adapter)
// Działa gdy możemy dziedziczyć po Adaptee
class LegacyLoggerClassAdapter extends LegacyLogger implements LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void
    {
        $severity = match(strtolower($level)) {
            'debug' => 1,
            'error' => 3,
            default => 2,
        };

        $this->write($severity, $message); // wywołanie metody rodzica
    }

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

Facade

Facade zapewnia uproszczony interfejs do złożonego podsystemu. Nie ukrywa podsystemu – klient nadal może go używać bezpośrednio – ale oferuje wygodniejszy punkt wejścia dla typowych przypadków użycia.

<?php

declare(strict_types=1);

// Złożony podsystem - wiele klas z których każda robi jedną rzecz
class ProductLoader
{
    public function load(int $id): array
    {
        echo "Ładowanie produktu {$id}\n";
        return ['id' => $id, 'name' => 'Produkt', 'price' => 99.99, 'stock' => 5];
    }
}

class StockChecker
{
    public function isAvailable(int $productId, int $qty): bool
    {
        echo "Sprawdzanie stanu magazynowego dla produktu {$productId}\n";
        return true;
    }
}

class PriceCalculator
{
    public function calculate(array $product, int $qty, ?string $coupon): float
    {
        echo "Obliczanie ceny\n";
        $price = $product['price'] * $qty;
        if ($coupon === 'DISCOUNT10') {
            $price *= 0.9;
        }
        return $price;
    }
}

class CartManager
{
    private array $items = [];

    public function addItem(array $product, int $qty, float $price): void
    {
        echo "Dodawanie do koszyka\n";
        $this->items[] = ['product' => $product, 'qty' => $qty, 'price' => $price];
    }

    public function getItems(): array { return $this->items; }
}

class OrderCreator
{
    public function create(array $cartItems, string $customerEmail): int
    {
        echo "Tworzenie zamówienia dla {$customerEmail}\n";
        return rand(1000, 9999);
    }
}

class PaymentProcessor
{
    public function charge(int $orderId, float $amount, string $method): bool
    {
        echo "Przetwarzanie płatności {$amount} PLN dla zamówienia {$orderId}\n";
        return true;
    }
}

class EmailSender
{
    public function sendConfirmation(string $email, int $orderId): void
    {
        echo "Wysyłanie potwierdzenia do {$email} dla zamówienia {$orderId}\n";
    }
}
<?php

// BEZ Facade - klient musi orkiestrować cały podsystem
$loader     = new ProductLoader();
$checker    = new StockChecker();
$calculator = new PriceCalculator();
$cart       = new CartManager();
$creator    = new OrderCreator();
$payment    = new PaymentProcessor();
$email      = new EmailSender();

$product = $loader->load(42);
if (!$checker->isAvailable(42, 2)) {
    throw new \RuntimeException('Out of stock');
}
$price = $calculator->calculate($product, 2, 'DISCOUNT10');
$cart->addItem($product, 2, $price);
$orderId = $creator->create($cart->getItems(), 'jan@example.com');
$payment->charge($orderId, $price, 'card');
$email->sendConfirmation('jan@example.com', $orderId);

// To samo w każdym miejscu gdzie składasz zamówienie - duplikacja logiki
<?php

// Facade - jeden spójny interfejs dla całego procesu
class OrderFacade
{
    public function __construct(
        private ProductLoader $productLoader,
        private StockChecker $stockChecker,
        private PriceCalculator $priceCalculator,
        private CartManager $cartManager,
        private OrderCreator $orderCreator,
        private PaymentProcessor $paymentProcessor,
        private EmailSender $emailSender
    ) {}

    // Prosta metoda dla typowego przypadku użycia
    public function placeOrder(
        int $productId,
        int $qty,
        string $customerEmail,
        string $paymentMethod = 'card',
        ?string $coupon = null
    ): int {
        $product = $this->productLoader->load($productId);

        if (!$this->stockChecker->isAvailable($productId, $qty)) {
            throw new \RuntimeException("Product {$productId} out of stock");
        }

        $price = $this->priceCalculator->calculate($product, $qty, $coupon);
        $this->cartManager->addItem($product, $qty, $price);

        $orderId = $this->orderCreator->create(
            $this->cartManager->getItems(),
            $customerEmail
        );

        $success = $this->paymentProcessor->charge($orderId, $price, $paymentMethod);
        if (!$success) {
            throw new \RuntimeException('Payment failed');
        }

        $this->emailSender->sendConfirmation($customerEmail, $orderId);

        return $orderId;
    }
}

// Klient - jeden prosty call zamiast orkiestracji 7 klas
$facade  = new OrderFacade(/* wstrzyknięte zależności */);
$orderId = $facade->placeOrder(42, 2, 'jan@example.com', 'card', 'DISCOUNT10');
echo "Zamówienie #{$orderId} złożone\n";

Adapter vs Facade – różnica

Aspekt Adapter Facade
Cel Konwersja interfejsu – „zrób to kompatybilne” Uproszczenie interfejsu – „zrób to łatwiejsze”
Liczba klas Zwykle jedna klasa adaptowana Wiele klas podsystemu
Dostęp do podsystemu Ukrywa adaptee za interfejsem Nie ukrywa – klient może nadal używać klas bezpośrednio
Typowe użycie Integracja z zewnętrzną biblioteką Uproszczenie złożonego API

Podsumowanie

Adapter i Facade to wzorce które ratują czytelność kodu przy integracji z zewnętrznym światem. Adapter pojawia się naturalnie gdy zmieniasz bibliotekę (np. z własnego loggera na Monolog) i nie chcesz zmieniać całego kodu – piszesz adapter. Facade pojawia się gdy masz złożony podsystem i chcesz dać klientom prostszy punkt wejścia bez ukrywania możliwości bezpośredniego dostępu.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Visitor – double dispatch, eksport CSV/PDF, walidacja bez modyfikacji klas
Wzorzec Template Method – szkielet algorytmu, hooks, abstract vs hook, porównanie ze Strategy
Wzorzec Flyweight – współdzielenie obiektów, cache instancji, Magento 2
  • 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}