PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Observer i Strategy w PHP – wzorce behawioralne

by Henryk Tews / wtorek, 07 czerwca 2022 / Opublikowano w Wzorce projektowe

Observer i Strategy to dwa z najczęściej używanych wzorców behawioralnych. Observer buduje luźno powiązany system zdarzeń – jeden obiekt zmienia stan, wiele innych reaguje. Strategy wymienia algorytmy jak klocki – ta sama operacja, różne sposoby wykonania. Oba wzorce realizują zasadę Open/Closed z SOLID.

Observer

Observer definiuje relację jeden-do-wielu: gdy jeden obiekt (Subject) zmienia stan, wszyscy jego obserwatorzy są automatycznie powiadamiani. Subject nie zna konkretnych klas obserwatorów – tylko ich wspólny interfejs.

<?php

declare(strict_types=1);

// Interfejsy wzorca
interface ObserverInterface
{
    public function update(string $event, mixed $data): void;
}

interface SubjectInterface
{
    public function subscribe(string $event, ObserverInterface $observer): void;
    public function unsubscribe(string $event, ObserverInterface $observer): void;
    public function notify(string $event, mixed $data = null): void;
}

// Subject - obiekt obserwowany
class EventEmitter implements SubjectInterface
{
    /** @var array */
    private array $listeners = [];

    public function subscribe(string $event, ObserverInterface $observer): void
    {
        $this->listeners[$event][] = $observer;
    }

    public function unsubscribe(string $event, ObserverInterface $observer): void
    {
        $this->listeners[$event] = array_filter(
            $this->listeners[$event] ?? [],
            fn($o) => $o !== $observer
        );
    }

    public function notify(string $event, mixed $data = null): void
    {
        foreach ($this->listeners[$event] ?? [] as $observer) {
            $observer->update($event, $data);
        }
    }
}
<?php

declare(strict_types=1);

// Konkretny Subject - koszyk zakupowy
class ShoppingCart extends EventEmitter
{
    private array $items = [];
    private float $total = 0.0;

    public function addItem(string $sku, float $price, int $qty = 1): void
    {
        $this->items[] = compact('sku', 'price', 'qty');
        $this->total  += $price * $qty;

        $this->notify('item.added', ['sku' => $sku, 'price' => $price, 'qty' => $qty]);
    }

    public function checkout(string $customerEmail): void
    {
        $this->notify('cart.checkout', [
            'email' => $customerEmail,
            'items' => $this->items,
            'total' => $this->total,
        ]);
    }

    public function getTotal(): float { return $this->total; }
}

// Konkretni obserwatorzy - każdy reaguje inaczej
class InventoryObserver implements ObserverInterface
{
    public function update(string $event, mixed $data): void
    {
        if ($event === 'item.added') {
            echo "Rezerwacja: {$data['qty']}x {$data['sku']} w magazynie\n";
        }
    }
}

class AnalyticsObserver implements ObserverInterface
{
    private array $events = [];

    public function update(string $event, mixed $data): void
    {
        $this->events[] = ['event' => $event, 'data' => $data, 'ts' => time()];
        echo "Analytics: zdarzenie '{$event}' zapisane\n";
    }

    public function getEvents(): array { return $this->events; }
}

class EmailObserver implements ObserverInterface
{
    public function update(string $event, mixed $data): void
    {
        if ($event === 'cart.checkout') {
            echo "Email do {$data['email']}: potwierdzenie zamówienia (total: {$data['total']} PLN)\n";
        }
    }
}

// Użycie
$cart      = new ShoppingCart();
$inventory = new InventoryObserver();
$analytics = new AnalyticsObserver();
$email     = new EmailObserver();

$cart->subscribe('item.added',    $inventory);
$cart->subscribe('item.added',    $analytics);
$cart->subscribe('cart.checkout', $analytics);
$cart->subscribe('cart.checkout', $email);

$cart->addItem('SKU-001', 29.99, 2);
$cart->addItem('SKU-002', 9.99);
$cart->checkout('jan@example.com');

PHP ma wbudowane interfejsy SplObserver i SplSubject które implementują ten sam wzorzec:

<?php

// Wbudowany Observer w PHP SPL
class StockPrice implements \SplSubject
{
    private \SplObjectStorage $observers;
    private float $price;

    public function __construct(float $initialPrice)
    {
        $this->observers = new \SplObjectStorage();
        $this->price     = $initialPrice;
    }

    public function attach(\SplObserver $observer): void { $this->observers->attach($observer); }
    public function detach(\SplObserver $observer): void { $this->observers->detach($observer); }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function setPrice(float $price): void
    {
        $this->price = $price;
        $this->notify();
    }

    public function getPrice(): float { return $this->price; }
}

Strategy

Strategy definiuje rodzinę wymiennych algorytmów, enkapsuluje każdy z nich i sprawia że są wymienne. Klient może zmieniać algorytm w runtime.

<?php

declare(strict_types=1);

// Interfejs strategii - jeden kontrakt dla wszystkich algorytmów
interface SortStrategyInterface
{
    /**
     * @param array<int|float|string> $data
     * @return array<int|float|string>
     */
    public function sort(array $data): array;
    public function getName(): string;
}

// Konkretne strategie - każda implementuje inny algorytm
class BubbleSortStrategy implements SortStrategyInterface
{
    public function sort(array $data): array
    {
        $n = count($data);
        for ($i = 0; $i < $n - 1; $i++) {
            for ($j = 0; $j < $n - $i - 1; $j++) {
                if ($data[$j] > $data[$j + 1]) {
                    [$data[$j], $data[$j + 1]] = [$data[$j + 1], $data[$j]];
                }
            }
        }
        return $data;
    }

    public function getName(): string { return 'Bubble Sort O(n²)'; }
}

class QuickSortStrategy implements SortStrategyInterface
{
    public function sort(array $data): array
    {
        if (count($data) <= 1) {
            return $data;
        }

        $pivot  = $data[0];
        $left   = array_filter(array_slice($data, 1), fn($x) => $x <= $pivot);
        $right  = array_filter(array_slice($data, 1), fn($x) => $x > $pivot);

        return [...$this->sort(array_values($left)), $pivot, ...$this->sort(array_values($right))];
    }

    public function getName(): string { return 'Quick Sort O(n log n)'; }
}

class NativeSortStrategy implements SortStrategyInterface
{
    public function sort(array $data): array
    {
        sort($data);
        return $data;
    }

    public function getName(): string { return 'PHP sort() - Timsort'; }
}

// Kontekst - korzysta ze strategii nie wiedząc co to za algorytm
class DataSorter
{
    private SortStrategyInterface $strategy;

    public function __construct(SortStrategyInterface $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(SortStrategyInterface $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function sort(array $data): array
    {
        echo "Sortuję " . count($data) . " elementów używając: {$this->strategy->getName()}\n";
        return $this->strategy->sort($data);
    }
}

// Podmiana strategii w runtime
$sorter = new DataSorter(new NativeSortStrategy());
$data   = [64, 34, 25, 12, 22, 11, 90];

$result = $sorter->sort($data);

// Zmień strategię bez modyfikacji DataSorter
$sorter->setStrategy(new BubbleSortStrategy());
$result = $sorter->sort($data);

Praktyczny przykład – strategia cenowa w sklepie:

<?php

declare(strict_types=1);

interface PricingStrategyInterface
{
    public function calculatePrice(float $basePrice, int $quantity): float;
}

class RegularPricingStrategy implements PricingStrategyInterface
{
    public function calculatePrice(float $basePrice, int $quantity): float
    {
        return $basePrice * $quantity;
    }
}

class BulkDiscountStrategy implements PricingStrategyInterface
{
    public function __construct(
        private int $minQuantity,
        private float $discountPercent
    ) {}

    public function calculatePrice(float $basePrice, int $quantity): float
    {
        $total = $basePrice * $quantity;

        if ($quantity >= $this->minQuantity) {
            $total *= (1 - $this->discountPercent / 100);
        }

        return $total;
    }
}

class SeasonalSaleStrategy implements PricingStrategyInterface
{
    public function __construct(
        private float $salePercent
    ) {}

    public function calculatePrice(float $basePrice, int $quantity): float
    {
        return $basePrice * (1 - $this->salePercent / 100) * $quantity;
    }
}

// Wybór strategii dynamicznie - np. na podstawie konfiguracji w bazie
function getPricingStrategy(string $type): PricingStrategyInterface
{
    return match($type) {
        'bulk'     => new BulkDiscountStrategy(minQuantity: 10, discountPercent: 15),
        'seasonal' => new SeasonalSaleStrategy(salePercent: 20),
        default    => new RegularPricingStrategy(),
    };
}

$strategy = getPricingStrategy('bulk');
$price    = $strategy->calculatePrice(basePrice: 10.0, quantity: 15);
echo "Cena: {$price} PLN\n"; // 127.50 PLN (15% rabat)

Observer vs Strategy – kiedy który?

Aspekt Observer Strategy
Relacja Jeden Subject, wiele Observerów Jeden Context, jedna aktywna Strategy
Komunikacja Subject powiadamia Observerów o zdarzeniu Context deleguje algorytm do Strategy
Typowy problem Reagowanie na zdarzenia bez twardego sprzężenia Wymiana algorytmu bez zmiany kodu klienta
Liczba „graczy” Jeden-do-wielu Jeden-do-jednego (aktywna strategia)

Podsumowanie

Observer i Strategy to wzorce które naturalnie wchodzą do kodu gdy zaczynacie myśleć w kategoriach SOLID. Observer eliminuje twarde sprzężenia przy obsłudze zdarzeń – zamiast wołać bezpośrednio EmailService z CartService, emitujesz zdarzenie i każdy kto chce może zareagować. Strategy eliminuje rosnące if-else przy wymiennych algorytmach – nowa strategia cenowa to nowa klasa, nie modyfikacja istniejącej logiki.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Repository – interfejs, implementacja, SearchCriteria, testowanie z mockiem
Chain of Responsibility – łańcuch walidatorów, konfiguracja przez di.xml z sortOrder
Wzorce GoF w Magento 2 – gdzie je znaleźć i jak działają
  • 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}