SplObserver y SplSubject en PHP: patrón Observer nativo con SPL

PHP incluye en la SPL (Standard PHP Library) las interfaces SplSubject y SplObserver que implementan el patrón Observer sin necesidad de librerías externas. Es una alternativa al array de callbacks cuando el dominio tiene entidades claras de sujeto y observador.

Las interfaces SPL

<?php
// Interfaces nativas de PHP (no necesitas escribirlas)

interface SplSubject
{
    public function attach(SplObserver $observer): void;
    public function detach(SplObserver $observer): void;
    public function notify(): void;
}

interface SplObserver
{
    public function update(SplSubject $subject): void;
}
?>

Implementar el sujeto

<?php
class Pedido implements SplSubject
{
    private SplObjectStorage $observers;
    private string $estado;
    private float  $total;

    public function __construct(
        public readonly int $id,
        float $total
    ) {
        $this->observers = new SplObjectStorage();
        $this->total     = $total;
        $this->estado    = 'pendiente';
    }

    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 cambiarEstado(string $nuevoEstado): void
    {
        $this->estado = $nuevoEstado;
        $this->notify();
    }

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

Implementar observadores

<?php
class NotificadorEmail implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        /** @var Pedido $subject */
        echo "Email: pedido #{$subject->id} ahora está en estado '{$subject->getEstado()}'n";
    }
}

class ActualizadorStock implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        /** @var Pedido $subject */
        if ($subject->getEstado() === 'confirmado') {
            echo "Stock: descontando unidades del pedido #{$subject->id}n";
        }
    }
}

class RegistroLog implements SplObserver
{
    private array $log = [];

    public function update(SplSubject $subject): void
    {
        /** @var Pedido $subject */
        $this->log[] = [
            'pedido' => $subject->id,
            'estado' => $subject->getEstado(),
            'ts'     => time(),
        ];
        echo "Log: registrada transición del pedido #{$subject->id}n";
    }

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

Conectar sujeto y observadores

<?php
$pedido  = new Pedido(42, 149.99);
$email   = new NotificadorEmail();
$stock   = new ActualizadorStock();
$logger  = new RegistroLog();

$pedido->attach($email);
$pedido->attach($stock);
$pedido->attach($logger);

$pedido->cambiarEstado('confirmado');
// Email: pedido #42 ahora está en estado 'confirmado'
// Stock: descontando unidades del pedido #42
// Log: registrada transición del pedido #42

$pedido->cambiarEstado('enviado');
// Email: pedido #42 ahora está en estado 'enviado'
// Stock: (no actúa, estado !== 'confirmado')
// Log: registrada transición del pedido #42

// Desconectar un observador
$pedido->detach($stock);

$pedido->cambiarEstado('entregado');
// Email y Log actúan; Stock no recibe la notificación
?>

Comparativa: SPL Observer vs. array de listeners vs. EventDispatcher

AspectoSplObserver/SubjectArray de callbacksEventDispatcher
DependenciasNinguna (SPL nativa)Ningunasymfony/event-dispatcher
TipadoInterfaces concretasSin tipo formalInterfaces y clases evento
Múltiples eventosDifícil (un solo notify)ManualNativo
PrioridadesNoManual
stopPropagationNoNo
Adecuado paraDominios simplesClosures rápidasAplicaciones grandes

Múltiples tipos de evento con SPL

La interfaz SPL solo tiene un método update(), por lo que para varios eventos hay que pasar el tipo de evento como contexto:

<?php
class Pedido implements SplSubject
{
    private string $ultimoEvento = '';

    public function getUltimoEvento(): string { return $this->ultimoEvento; }

    public function confirmar(): void
    {
        $this->ultimoEvento = 'confirmado';
        $this->notify();
    }

    public function cancelar(string $motivo): void
    {
        $this->ultimoEvento = 'cancelado';
        $this->motivoCancelacion = $motivo;
        $this->notify();
    }
    // ...
}

class MiObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        match ($subject->getUltimoEvento()) {
            'confirmado' => $this->alConfirmar($subject),
            'cancelado'  => $this->alCancelar($subject),
            default      => null,
        };
    }
}
?>

Errores comunes

  • Olvidar detach() al destruir el observer: si el observer tiene una referencia fuerte al subject o viceversa, PHP no puede liberar la memoria. Llama a detach() explícitamente o implementa WeakReference.
  • Modificar el estado del subject dentro de update(): puede causar llamadas a notify() recursivas e infinitas.

COMPARTE ESTE ARTÍCULO

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