Laravel & Vue: Notificaciones en Tiempo Real usando Reverb, Redis y TDD - 03
José Rafael Gutierrez
hace 2 semanas
Capítulo 3: Configuración de Redis para Almacenar y Administrar Notificaciones
Objetivo
En este capítulo, configuraremos Redis para gestionar notificaciones en tiempo real, sincronizándolas con la base de datos para obtener persistencia a largo plazo. Utilizaremos el ciclo TDD (rojo → verde → refactor) para desarrollar un sistema eficiente de notificaciones en tiempo real con Redis y una persistencia robusta en la base de datos.
1. Generar notificaciones automáticas al cambiar el estado de una orden (Order
).
2. Almacenar notificaciones en Redis con tiempos de expiración para mejorar el rendimiento.
3. Notificar tanto al usuario como a los administradores sobre cambios en el estado de una orden.
4. Implementar un sistema para marcar notificaciones como "leídas" o "no leídas" utilizando Redis.
Introducción
En aplicaciones modernas, Redis es una base de datos en memoria que ofrece una velocidad excepcional para operaciones de lectura y escritura. Esto lo convierte en una solución ideal para manejar notificaciones en tiempo real, donde la latencia debe ser mínima. Al combinar Redis con una base de datos relacional como MySQL o PostgreSQL, podemos obtener lo mejor de ambos mundos: rapidez en la entrega de notificaciones y persistencia para mantener un historial permanente.
Vamos a trabajar con una nueva entidad llamada Order
, que representa un pedido con un estado de entrega (delivery_status
). Cada vez que el estado de una orden cambie (por ejemplo, de "en preparación" a "en tránsito"), un Observer se encargará de generar automáticamente notificaciones tanto para el usuario como para los administradores del sistema.
Ciclo TDD: Generación de Notificaciones
1. Escribir la Prueba Inicial
Empezaremos escribiendo una prueba para validar que se generan notificaciones cuando el estado de una orden cambia y que estas notificaciones se envían tanto al usuario como a los administradores.
Ejecuta el siguiente comando para crear un archivo de prueba:
sail artisan make:test OrderStatusObserverTest
Modifica tests/Feature/OrderStatusObserverTest.php
con el siguiente contenido:
namespace Tests\Feature;
use App\Models\Notification;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class OrderStatusObserverTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_notifications_for_user_and_admins_on_order_status_change()
{
// Given
$user = User::factory()->create();
$admin = User::factory()->state(['is_admin' => true])->create();
// When
$order = Order::create([
'user_id' => $user->id,
'delivery_status' => 'in_preparation',
]);
$order->update(['delivery_status' => 'in_transit']);
// Then
$this->assertDatabaseHas('notifications', [
'user_id' => $user->id,
'message' => 'El estado del pedido ha cambiado a: En tránsito.',
]);
$this->assertDatabaseHas('notifications', [
'user_id' => $admin->id,
'message' => 'El pedido #1 ha cambiado de estado a: En tránsito.',
]);
$userNotification = json_decode(Redis::lpop("notifications:{$user->id}"), true);
$adminNotification = json_decode(Redis::lpop("notifications:{$admin->id}"), true);
$this->assertEquals('El estado del pedido ha cambiado a: En tránsito.', $userNotification['message']);
$this->assertEquals('El pedido #1 ha cambiado de estado a: En tránsito.', $adminNotification['message']);
}
}
2. Ejecutar la Prueba Inicial (Rojo)
Ejecuta el test para confirmar que falle debido a que no existen ni el modelo Order
ni su observer:
sail artisan test --filter=OrderStatusObserverTest
Errores esperados:
-
Clase
Order
no encontrada. -
Migración para
orders
no definida.
Ahora que hemos identificado los errores esperados, vamos a crear el modelo Order
y su respectiva migración para solucionar el primer fallo.
Creación de Entidades y Migraciones
3. Crear el Modelo y la Migración para Order
Crea el modelo Order
y su migración con el siguiente comando:
sail artisan make:model Order -m
Modifica la migración generada en database/migrations/xxxx_xx_xx_create_orders_table.php
para incluir la estructura de la tabla:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('delivery_status');
$table->timestamps();
});
En el modelo app/Models/Order.php
, añade el atributo fillable
:
protected $fillable = ['user_id', 'delivery_status'];
Ejecuta la migración:
sail artisan migrate
4. Crear una Nueva Migración para Agregar el Campo message
a notifications
Para agregar el campo message
a la tabla notifications
, ejecuta:
sail artisan make:migration add_message_to_notifications_table --table=notifications
Modifica la migración generada en database/migrations/xxxx_xx_xx_add_message_to_notifications_table.php
:
Schema::table('notifications', function (Blueprint $table) {
$table->text('message');
});
Ejecuta la migración:
sail artisan migrate
Modifica el modelo Notification
para incluir message
en $fillable
:
protected $fillable = ['user_id', 'status', 'message'];
Gestión del Estado de Lectura de Notificaciones
Para mejorar la experiencia del usuario, implementaremos una funcionalidad que permita marcar notificaciones como "leídas" o "no leídas" utilizando Redis.
Explicación del Proceso:
-
Cuando se crea una notificación, se añade automáticamente al conjunto de notificaciones no leídas en Redis.
-
Cuando el usuario revisa una notificación, se elimina del conjunto de no leídas.
Código para Manejar el Estado de Lectura
-
Añadir una Notificación como No Leída:
Redis::sadd("notifications:unread:{$user->id}", $notification->id);
-
Marcar una Notificación como Leída:
Redis::srem("notifications:unread:{$user->id}", $notificationId);
5. Implementar el Observer para Generar Notificaciones (Verde)
Crea un Observer para el modelo Order
:
sail artisan make:observer OrderObserver --model=Order
Modifica app/Observers/OrderObserver.php
con la lógica para generar las notificaciones:
namespace App\Observers;
use App\Models\Notification;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Redis;
class OrderObserver
{
public function updating(Order $order)
{
if ($order->isDirty('delivery_status')) {
$status = $order->delivery_status;
// Mensaje para el usuario
$userMessage = "El estado del pedido ha cambiado a: {$this->getStatusLabel($status)}.";
Notification::create([
'user_id' => $order->user_id,
'status' => $status,
'message' => $userMessage,
]);
Redis::lpush("notifications:{$order->user_id}", json_encode([
'message' => $userMessage,
'timestamp' => now()->timestamp,
]));
// Mensaje para los administradores
$adminMessage = "El pedido #{$order->id} ha cambiado de estado a: {$this->getStatusLabel($status)}.";
$admins = User::where('is_admin', true)->get();
foreach ($admins as $admin) {
Notification::create([
'user_id' => $admin->id,
'status' => $status,
'message' => $adminMessage,
]);
Redis::lpush("notifications:{$admin->id}", json_encode([
'message' => $adminMessage,
'timestamp' => now()->timestamp,
]));
}
}
}
private function getStatusLabel(string $status): string
{
return [
'in_preparation' => 'En preparación',
'in_transit' => 'En tránsito',
'delivered' => 'Entregado',
][$status] ?? $status;
}
}
Registra el observer en AppServiceProvider
:
use App\Models\Order;
use App\Observers\OrderObserver;
public function boot()
{
Order::observe(OrderObserver::class);
}
Ahora, ejecuta nuevamente el test:
sail artisan test --filter=OrderStatusObserverTest
PASS Tests\Feature\OrderStatusObserverTest
✓ it creates notifications for user and admins on order status change
Actualización del Test NotificationTest
¿Por qué Actualizamos el Test?
Después de agregar el campo message
a la tabla notifications
, nuestro test original comenzó a fallar porque no contemplaba este nuevo campo obligatorio. Vamos a actualizar el test para reflejar esta nueva estructura de datos y cumplir con el ciclo TDD.
1. Ejecución del Test (Rojo)
Si ejecutamos el siguiente comando para correr todos los tests:
sail artisan test
Obtenemos el siguiente error:
FAIL Tests\Feature\NotificationTest
⨯ it creates a notification
Failed asserting that a row in the table [notifications] matches the attributes {
"user_id": 1,
"status": "Enviado"
}.
Found similar results: [
{
"user_id": 1,
"status": "Enviado",
"message": null
}
].
Análisis del Error:
El test falla porque la creación de una notificación ahora requiere un message
no nulo. La migración que añadimos establece el campo message
como un text
, por lo que debemos incluirlo al crear una notificación en el test.
2. Modificar el Test (Verde)
Para solucionar el error, actualizaremos el test NotificationTest
para incluir el campo message
al crear una notificación.
Modifica el archivo tests/Feature/NotificationTest.php
de la siguiente manera:
<?php
namespace Tests\Feature;
use App\Models\Notification;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class NotificationTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_a_notification()
{
// Given
$user = User::factory()->create();
$data = [
'user_id' => $user->id,
'status' => 'Enviado',
'message' => 'El pedido ha sido enviado',
];
// When
Notification::create($data);
// Then
$this->assertDatabaseHas('notifications', $data);
}
}
3. Ejecutar el Test Corregido
Ahora, ejecuta nuevamente el test:
sail artisan test --filter=NotificationTest
El test debería pasar correctamente:
PASS Tests\Feature\NotificationTest
✓ it creates a notification
4. Ahora ejecutamos todos los tests
sail artisan test
Explicación de los Cambios
-
Adición del Campo
message
: Se agregó el campomessage
en el array$data
para cumplir con el nuevo requisito del modeloNotification
. -
Consistencia con la Migración: El campo
message
es obligatorio debido a la migración que añadimos. Esta actualización del test asegura que estamos creando notificaciones válidas que cumplen con los requisitos actuales del modelo y la base de datos. -
Aplicación del Ciclo TDD:
-
Rojo: El test falló después de añadir el campo
message
. -
Verde: Actualizamos el test para incluir el campo
message
, y el test pasó. - Refactor: No se requiere refactorización adicional en este caso, ya que el código es claro y conciso.
-
Rojo: El test falló después de añadir el campo
Conclusión
En este capítulo, implementamos un sistema completo de notificaciones usando Redis y Laravel, siguiendo el ciclo TDD. Hemos logrado:
- Generar notificaciones automáticas al cambiar el estado de una orden.
- Almacenar notificaciones en Redis para mejorar el rendimiento.
- Sincronizar con la base de datos para persistencia a largo plazo.
- Gestionar el estado "leído" o "no leído" con Redis.
En el próximo capítulo, conectaremos Laravel con el frontend para recibir notificaciones en tiempo real usando Laravel Echo. 🚀