Observer Pattern en PHP moderno con WeakReference

Implementación del patrón Observer en PHP 8.1 con interfaces, enums para tipos de evento y WeakReference para evitar memory leaks. Ejemplo práctico con carrito de compra: múltiples observadores reaccionan a ItemAdded, OrderPlaced y StockUpdated.
				<?php
/**
 * Observer Pattern en PHP 8.1+ con WeakReference
 * Ejemplo práctico: carrito de compra con eventos
 */

declare(strict_types=1);

// ?????????????????????????????????????????????????????????????
// Enums para los tipos de evento (PHP 8.1)
// ?????????????????????????????????????????????????????????????

enum EventType: string
{
    case ItemAdded    = 'item.added';
    case OrderPlaced  = 'order.placed';
    case StockUpdated = 'stock.updated';
}

// ?????????????????????????????????????????????????????????????
// Contrato base para todos los eventos del dominio
// ?????????????????????????????????????????????????????????????

abstract class DomainEvent
{
    public readonly DateTimeImmutable $occurredAt;

    public function __construct()
    {
        $this->occurredAt = new DateTimeImmutable();
    }

    abstract public function type(): EventType;
}

class ItemAddedEvent extends DomainEvent
{
    public function __construct(
        public readonly string $productName,
        public readonly int    $quantity,
        public readonly float  $unitPrice
    ) { parent::__construct(); }

    public function type(): EventType { return EventType::ItemAdded; }
}

class OrderPlacedEvent extends DomainEvent
{
    public function __construct(
        public readonly string $orderId,
        public readonly float  $total,
        public readonly string $customerEmail
    ) { parent::__construct(); }

    public function type(): EventType { return EventType::OrderPlaced; }
}

class StockUpdatedEvent extends DomainEvent
{
    public function __construct(
        public readonly string $productName,
        public readonly int    $newStock
    ) { parent::__construct(); }

    public function type(): EventType { return EventType::StockUpdated; }
}

// ?????????????????????????????????????????????????????????????
// Interfaz del observer (listener)
// ?????????????????????????????????????????????????????????????

interface EventListener
{
    public function handle(DomainEvent $event): void;
    public function listensTo(): EventType;
}

// ?????????????????????????????????????????????????????????????
// EventEmitter con WeakReference para evitar memory leaks
// WeakReference no impide que el GC libere al listener
// si el objeto ya no tiene otras referencias fuertes.
// ?????????????????????????????????????????????????????????????

class EventEmitter
{
    /** @var array<string, list<WeakReference<EventListener>>> */
    private array $listeners = [];

    public function subscribe(EventListener $listener): void
    {
        $key = $listener->listensTo()->value;
        $this->listeners[$key][] = WeakReference::create($listener);
    }

    public function emit(DomainEvent $event): void
    {
        $key = $event->type()->value;
        if (!isset($this->listeners[$key])) return;

        foreach ($this->listeners[$key] as $i => $ref) {
            $listener = $ref->get();
            if ($listener === null) {
                // El listener fue recogido por el GC: limpiar la referencia muerta
                unset($this->listeners[$key][$i]);
                continue;
            }
            $listener->handle($event);
        }
    }
}

// ?????????????????????????????????????????????????????????????
// Listeners concretos
// ?????????????????????????????????????????????????????????????

class EmailNotificationListener implements EventListener
{
    public function listensTo(): EventType { return EventType::OrderPlaced; }

    public function handle(DomainEvent $event): void
    {
        /** @var OrderPlacedEvent $event */
        echo "[EMAIL] Enviando confirmación de pedido #{$event->orderId} "
           . "a {$event->customerEmail} — Total: {$event->total}€n";
    }
}

class StockReducerListener implements EventListener
{
    private array $stock = [];

    public function __construct(array $initialStock)
    {
        $this->stock = $initialStock;
    }

    public function listensTo(): EventType { return EventType::ItemAdded; }

    public function handle(DomainEvent $event): void
    {
        /** @var ItemAddedEvent $event */
        $name = $event->productName;
        $this->stock[$name] = ($this->stock[$name] ?? 0) - $event->quantity;
        echo "[STOCK] {$name}: stock reducido en {$event->quantity} ? disponible: {$this->stock[$name]}n";
    }

    public function getStock(string $product): int
    {
        return $this->stock[$product] ?? 0;
    }
}

class AuditLogListener implements EventListener
{
    private array $log = [];

    public function listensTo(): EventType { return EventType::OrderPlaced; }

    public function handle(DomainEvent $event): void
    {
        /** @var OrderPlacedEvent $event */
        $entry = sprintf(
            '[%s] Pedido %s — %s€ — cliente: %s',
            $event->occurredAt->format('H:i:s'),
            $event->orderId,
            $event->total,
            $event->customerEmail
        );
        $this->log[] = $entry;
        echo "[AUDIT] $entryn";
    }

    public function getLogs(): array { return $this->log; }
}

class CartStatsListener implements EventListener
{
    private int $itemsAdded = 0;
    private float $totalValue = 0.0;

    public function listensTo(): EventType { return EventType::ItemAdded; }

    public function handle(DomainEvent $event): void
    {
        /** @var ItemAddedEvent $event */
        $this->itemsAdded += $event->quantity;
        $this->totalValue += $event->quantity * $event->unitPrice;
        echo "[STATS] Carrito actualizado: {$this->itemsAdded} unidades, valor {$this->totalValue}€n";
    }
}

// ?????????????????????????????????????????????????????????????
// Demo
// ?????????????????????????????????????????????????????????????

echo "=== Observer Pattern — Carrito de Compra ===nn";

$emitter = new EventEmitter();

// Registrar listeners
$emailListener  = new EmailNotificationListener();
$stockListener  = new StockReducerListener(['Teclado mecánico' => 50, 'Monitor 4K' => 20]);
$auditListener  = new AuditLogListener();
$statsListener  = new CartStatsListener();

$emitter->subscribe($emailListener);
$emitter->subscribe($stockListener);
$emitter->subscribe($auditListener);
$emitter->subscribe($statsListener);

// Simulación: añadir productos al carrito
echo "--- Añadiendo productos al carrito ---n";
$emitter->emit(new ItemAddedEvent('Teclado mecánico', 2, 89.99));
$emitter->emit(new ItemAddedEvent('Monitor 4K', 1, 349.00));
echo "n";

// Simulación: confirmar pedido
echo "--- Confirmando pedido ---n";
$emitter->emit(new OrderPlacedEvent('ORD-2026-001', 529.98, '[email protected]'));
echo "n";

// Dos observers distintos escuchan OrderPlaced (email + audit)
echo "Logs de auditoría guardados: " . count($auditListener->getLogs()) . "n";
echo "Stock restante de 'Teclado mecánico': " . $stockListener->getStock('Teclado mecánico') . "n";

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP