PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Observer and Strategy in PHP – behavioural patterns

by Henryk Tews / Tuesday, 07 June 2022 / Published in Wzorce projektowe

Observer and Strategy are two of the most widely used behavioural patterns. Observer defines a publish-subscribe mechanism where subjects notify observers of state changes. Strategy defines a family of interchangeable algorithms. Both appear throughout Magento 2 – I show GoF implementations and the platform’s concrete use of each.

Observer – event-driven notification

<?php

declare(strict_types=1);

interface EventObserverInterface
{
    public function onEvent(string $eventName, array $eventData): void;
}

class EventEmitter
{
    private array $listeners = [];

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

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

    public function emit(string $event, array $data = []): void
    {
        foreach ($this->listeners[$event] ?? [] as $observer) {
            $observer->onEvent($event, $data);
        }
    }
}

// Concrete observers
class EmailObserver implements EventObserverInterface
{
    public function onEvent(string $eventName, array $data): void
    {
        if ($eventName === 'order.placed') {
            echo "Sending confirmation email to: {$data['email']}\n";
        }
    }
}

class InventoryObserver implements EventObserverInterface
{
    public function onEvent(string $eventName, array $data): void
    {
        if ($eventName === 'order.placed') {
            foreach ($data['items'] as $item) {
                echo "Reserving {$item['qty']} of SKU {$item['sku']}\n";
            }
        }
    }
}

class AuditLogObserver implements EventObserverInterface
{
    public function onEvent(string $eventName, array $data): void
    {
        file_put_contents('/var/log/audit.log',
            date('Y-m-d H:i:s') . " {$eventName}: " . json_encode($data) . "\n",
            FILE_APPEND
        );
    }
}

// Wire it up
$emitter = new EventEmitter();
$emitter->on('order.placed', new EmailObserver());
$emitter->on('order.placed', new InventoryObserver());
$emitter->on('order.placed', new AuditLogObserver());

// Emit the event - all three observers react automatically
$emitter->emit('order.placed', [
    'order_id' => 42,
    'email'    => 'customer@example.com',
    'items'    => [['sku' => 'MG-001', 'qty' => 2]],
]);

Observer in Magento 2

<!-- etc/events.xml -->
<config>
    <event name="checkout_submit_all_after">
        <observer name="vendor_send_confirmation"
                  instance="Vendor\Module\Observer\SendOrderConfirmation"/>
    </event>
    <event name="catalog_product_save_after">
        <observer name="vendor_sync_to_pim"
                  instance="Vendor\Module\Observer\SyncProductToPim"/>
    </event>
</config>
<?php

namespace Vendor\Module\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class SyncProductToPim implements ObserverInterface
{
    public function __construct(
        private \Vendor\Module\Model\PimSyncService $pimSync,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function execute(Observer $observer): void
    {
        $product = $observer->getData('product');
        try {
            $this->pimSync->sync((int) $product->getId());
        } catch (\Exception $e) {
            $this->logger->error('PIM sync failed', ['product_id' => $product->getId()]);
        }
    }
}

Strategy – interchangeable algorithms

<?php

declare(strict_types=1);

interface SortStrategyInterface
{
    public function sort(array $items): array;
}

class BubbleSortStrategy implements SortStrategyInterface
{
    public function sort(array $items): array
    {
        $n = count($items);
        for ($i = 0; $i < $n - 1; $i++) {
            for ($j = 0; $j < $n - $i - 1; $j++) {
                if ($items[$j] > $items[$j + 1]) {
                    [$items[$j], $items[$j + 1]] = [$items[$j + 1], $items[$j]];
                }
            }
        }
        return $items;
    }
}

class QuickSortStrategy implements SortStrategyInterface
{
    public function sort(array $items): array
    {
        if (count($items) <= 1) return $items;
        $pivot  = $items[0];
        $left   = array_filter(array_slice($items, 1), fn($x) => $x <= $pivot);
        $right  = array_filter(array_slice($items, 1), fn($x) => $x > $pivot);
        return [...$this->sort(array_values($left)), $pivot, ...$this->sort(array_values($right))];
    }
}

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

// Context uses strategy without knowing the algorithm
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
    {
        return $this->strategy->sort($data);
    }
}

$sorter = new DataSorter(new NativeSortStrategy());
print_r($sorter->sort([5, 3, 8, 1, 9, 2])); // [1, 2, 3, 5, 8, 9]

$sorter->setStrategy(new QuickSortStrategy()); // swap algorithm at runtime

Strategy in Magento 2 – shipping calculations

<?php

// Each shipping carrier is a Strategy implementing AbstractCarrier
// Magento's context (ShippingModel) uses whichever is configured
namespace Vendor\Module\Model\Carrier;

use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;

class FlatRateCarrier extends AbstractCarrier implements CarrierInterface
{
    protected $_code = 'vendor_flatrate';

    public function collectRates(
        \Magento\Quote\Model\Quote\Address\RateRequest $request
    ): \Magento\Shipping\Model\Rate\Result {
        $result = $this->_rateResultFactory->create();
        $method = $this->_rateMethodFactory->create();

        $method->setCarrier($this->_code);
        $method->setMethod('standard');
        $method->setMethodTitle('Standard Delivery');
        $method->setPrice((float) $this->getConfigData('price'));

        $result->append($method);
        return $result;
    }

    public function getAllowedMethods(): array
    {
        return ['standard' => 'Standard Delivery'];
    }
}

Observer vs Strategy – when to use which

Criterion Observer Strategy
Relationship One-to-many (subject to observers) One-to-one (context to algorithm)
Coupling Very loose – subject does not know observers Context knows it has a strategy
When to use Reacting to events across modules Varying algorithms or business rules
Magento events.xml observers Shipping carriers, payment methods

Summary

Observer and Strategy both reduce coupling between components but in different ways. Observer separates event producers from consumers completely – the subject never knows who is listening. Strategy separates a context from its algorithm – the context knows it has a strategy but not its concrete type. Master both and you have the right tool for the two most common extensibility needs in Magento 2: reacting to platform events and plugging in custom business logic.

About Henryk Tews

What you can read next

Flyweight pattern – object sharing, instance cache, Magento 2
Adapter and Facade in PHP – structural patterns
Memento pattern – undo/redo, price history, DB persistence as audit log

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