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
| Aspecto | SplObserver/Subject | Array de callbacks | EventDispatcher |
|---|---|---|---|
| Dependencias | Ninguna (SPL nativa) | Ninguna | symfony/event-dispatcher |
| Tipado | Interfaces concretas | Sin tipo formal | Interfaces y clases evento |
| Múltiples eventos | Difícil (un solo notify) | Manual | Nativo |
| Prioridades | No | Manual | Sí |
| stopPropagation | No | No | Sí |
| Adecuado para | Dominios simples | Closures rápidas | Aplicaciones 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 implementaWeakReference. - Modificar el estado del subject dentro de update(): puede causar llamadas a
notify()recursivas e infinitas.
