Una guía práctica sobre cuándo usar cada enfoque y por qué
Cuando ocurre algo importante en tu aplicación (un usuario se registra, se procesa un pago, se crea un pedido), tienes que decidir cómo reaccionar. Hay dos caminos:
| Llamada directa | Evento | |
|---|---|---|
| ¿Qué es? | Llamas funciones/métodos explícitamente | Emites una señal; otros escuchan |
| Control | Total y explícito | Desacoplado, implícito |
| Complejidad | Baja | Media |
| Escalabilidad | Se complica con el tiempo | Escala bien |
El código que detecta el evento llama directamente a las funciones que deben ejecutarse. Simple y explícito.
// OrderController.php
public function store(OrderRequest $request)
{
$order = Order::create($request->validated());
// Llamadas directas, en secuencia
$this->sendConfirmationEmail($order);
$this->notifyWarehouse($order);
$this->updateStats($order);
return response()->json($order, 201);
}
private function sendConfirmationEmail(Order $order): void
{
Mail::to($order->customer->email)->send(new ConfirmationMail($order));
}
private function notifyWarehouse(Order $order): void
{
WarehouseService::notify($order);
}Tres meses después, el negocio pide más cosas al crear un pedido:
// El mismo método, 3 meses después...
public function store(OrderRequest $request)
{
$order = Order::create($request->validated());
$this->sendConfirmationEmail($order); // original
$this->notifyWarehouse($order); // original
$this->updateStats($order); // original
$this->addLoyaltyPoints($order); // nuevo - marketing
$this->syncWithERP($order); // nuevo - contabilidad
$this->alertFraudSystem($order); // nuevo - seguridad
$this->createShipmentLabel($order); // nuevo - logística
return response()->json($order, 201);
}Clase PHP simple que transporta los datos del momento.
Clase que reacciona y ejecuta una acción concreta.
Configuración que conecta eventos con listeners.
Paso 1 — Definir el evento
// app/Events/OrderCreated.php
class OrderCreated
{
public function __construct(
public readonly Order $order
) {}
}Paso 2 — Los listeners (uno por responsabilidad)
// app/Listeners/SendConfirmationEmail.php
class SendConfirmationEmail
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->customer->email)
->send(new ConfirmationMail($event->order));
}
}
// app/Listeners/NotifyWarehouse.php
class NotifyWarehouse
{
public function handle(OrderCreated $event): void
{
WarehouseService::notify($event->order);
}
}
// app/Listeners/AddLoyaltyPoints.php — añadido después, sin tocar nada
class AddLoyaltyPoints
{
public function handle(OrderCreated $event): void
{
LoyaltyService::award($event->order->customer, 100);
}
}Paso 3 — Registrar en el Provider
// app/Providers/EventServiceProvider.php
protected $listen = [
OrderCreated::class => [
SendConfirmationEmail::class,
NotifyWarehouse::class,
UpdateOrderStats::class,
AddLoyaltyPoints::class, // nuevo: solo añadir aquí
SyncWithERP::class, // sin tocar el controller
AlertFraudSystem::class,
],
];Paso 4 — El controller queda limpio para siempre
// app/Http/Controllers/OrderController.php
public function store(OrderRequest $request)
{
$order = Order::create($request->validated());
event(new OrderCreated($order)); // su trabajo termina aquí
return response()->json($order, 201);
}
// Este método nunca necesita cambiar, sin importar cuántos listeners se añadanUna ventaja exclusiva de los eventos: convertir cualquier listener en asíncrono es trivial.
// Solo añadir 'implements ShouldQueue'
class SendConfirmationEmail implements ShouldQueue
{
public string $queue = 'emails'; // cola específica (opcional)
public int $tries = 3; // reintentos en caso de fallo
public function handle(OrderCreated $event): void
{
// Esto se ejecuta en background, sin bloquear la respuesta HTTP
Mail::to($event->order->customer->email)
->send(new ConfirmationMail($event->order));
}
public function failed(OrderCreated $event, Throwable $exception): void
{
// Se llama si los 3 intentos fallan
Log::error('Email fallido para orden: ' . $event->order->id);
}
}ShouldQueue, si el listener de email falla, el de warehouse y el de loyalty points se ejecutan igualmente. Con llamadas directas, una excepción en el email cortaría toda la cadena.
// Tienes que mockear todos los servicios
public function test_order_creation()
{
Mail::fake();
$this->mock(
WarehouseService::class,
fn($m) => $m->shouldReceive('notify')
);
$this->mock(
LoyaltyService::class,
fn($m) => $m->shouldReceive('award')
);
// ...un mock por cada llamada directa
$this->post('/orders', $data)
->assertStatus(201);
}// Solo verificas que el evento se disparó
public function test_order_creation()
{
Event::fake();
$this->post('/orders', $data)
->assertStatus(201);
Event::assertDispatched(OrderCreated::class);
}
// Cada listener tiene su propio test:
public function test_sends_email()
{
Mail::fake();
$order = Order::factory()->create();
(new SendConfirmationEmail)
->handle(new OrderCreated($order));
Mail::assertSent(ConfirmationMail::class);
}| Situación | Llamada directa | Eventos |
|---|---|---|
| Lógica simple, 1-2 pasos | ✅ Perfecto | ❌ Innecesario |
| Prototipo rápido | ✅ Más rápido | ❌ Overkill |
| Múltiples reacciones a una acción | ⚠ Se complica | ✅ Ideal |
| Necesitas asincronía / colas | ❌ Requiere refactor | ✅ ShouldQueue trivial |
| Código de equipo grande | ⚠ Difícil de mantener | ✅ Responsabilidades claras |
| Tests unitarios independientes | ⚠ Muchos mocks | ✅ Fácil de aislar |
| Añadir comportamiento sin riesgos | ❌ Tocas código existente | ✅ Solo nuevo listener |
Si tienes 1-2 cosas que hacer, una llamada directa es perfecta. No añadas complejidad innecesaria.