Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

【Guía Práctica Completa】Crear un Centro de Notificaciones con Laravel: Database Notifications, Gestión de Lectura, Integración con Correo, Notificaciones en Tiempo Real y UI Accesible

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

【Guía Práctica Completa】Crear un Centro de Notificaciones con Laravel: Database Notifications, Gestión de Lectura, Integración con Correo, Notificaciones en Tiempo Real y UI Accesible

Lo que aprenderás en este artículo

  • Diseño básico de notificaciones dentro de la app usando Laravel Notifications
  • Almacenamiento, listado y gestión de leídas/no leídas con Database Notifications
  • Cómo diferenciar notificaciones por correo, dentro de la app y en tiempo real
  • Diseño de tipos, prioridad, configuración, bajas y agrupación de notificaciones
  • UI accesible para centro de notificaciones, badges, toasts y estados vacíos
  • Patrones prácticos de colas, reintentos, idempotencia, logs de auditoría y pruebas

Público objetivo

  • Ingenieros Laravel de nivel inicial a intermedio que quieren crear no solo correos, sino también notificaciones internas y gestión de lectura
  • Desarrolladores de SaaS, paneles de administración y sistemas empresariales que quieren implementar un centro de notificaciones por usuario
  • Tech leads que necesitan ordenar el diseño y operación a medida que aumentan los tipos de notificación
  • Diseñadores, QA y responsables de accesibilidad que quieren mejorar badges, toasts y listas de notificaciones

Nivel de accesibilidad: ★★★★★
Las notificaciones son UI que llaman la atención del usuario. Por eso es importante no interrumpir la operación con visualizaciones excesivas o actualizaciones automáticas, no comunicar cantidades o estados solo mediante color, y usar adecuadamente role="status" y aria-live. En este artículo diseñamos las notificaciones no como “información que solo llega a quienes pueden verla”, sino como “cambios de estado comprensibles para todas las personas usuarias”.


1. Introducción: Las Notificaciones no Solo “Informan”, También Guían a la Acción

En una aplicación web se necesitan notificaciones en muchas situaciones: llegó un comentario, se aprobó una solicitud, se emitió una factura, terminó una exportación, cambió una configuración de seguridad o se acerca una fecha límite. Al entregar esta información a la persona usuaria, se facilita entender cuál debe ser la siguiente acción.

Sin embargo, demasiadas notificaciones producen el efecto contrario. Si todo se notifica, las personas dejan de mirar las notificaciones. Si las notificaciones importantes y las ligeras se ven igual, la información realmente necesaria queda enterrada. Además, una UI donde los toasts desaparecen rápido, el estado se indica solo con un badge rojo o el contenido no se entiende con lectores de pantalla también presenta problemas de accesibilidad.

Laravel cuenta con un potente mecanismo llamado Notifications. Permite trabajar con múltiples canales como correo, base de datos, Slack y Broadcast, y facilita crear una estructura ordenada. En este artículo veremos desde lo básico para crear un centro de notificaciones con Laravel hasta gestión de lectura, integración con correo, notificaciones en tiempo real y UI accesible, todo con enfoque práctico.


2. Organizar los Tipos de Notificación: No Tratar Todo Como la Misma Notificación

Antes de crear un centro de notificaciones, conviene organizar los tipos de notificación. Las notificaciones deben tratarse de forma distinta según su prioridad y propósito.

2.1 Notificaciones Informativas

Ejemplos:

  • llegó un nuevo comentario
  • terminó la generación de un reporte
  • se actualizó el perfil

Son notificaciones relativamente ligeras. Muchas veces basta con una notificación dentro de la aplicación.

2.2 Notificaciones que Requieren Acción

Ejemplos:

  • se requiere aprobar una solicitud
  • se debe actualizar el método de pago
  • se necesita responder a una invitación

Son notificaciones que piden una acción del usuario. La lista de notificaciones debe ofrecer una ruta clara hacia la pantalla de detalle u operación.

2.3 Notificaciones Importantes o de Seguridad

Ejemplos:

  • se cambió la contraseña
  • se desactivó la autenticación de dos factores
  • se detectó un inicio de sesión sospechoso

Conviene combinarlas con correo. Si solo aparecen dentro de la aplicación, pueden pasar desapercibidas.

2.4 Notificaciones Periódicas o Agrupadas

Ejemplos:

  • reporte semanal
  • resumen de tareas pendientes
  • digest de comentarios

Cuando hay demasiadas notificaciones, suele ser más amable agruparlas en lugar de enviarlas individualmente.

Al diseñar notificaciones, conviene decidir: “¿debe avisarse de inmediato?”, “¿también necesita correo?”, “¿basta con que aparezca en la lista?”, “¿requiere acción?”. Esto ayuda a evitar la fatiga de notificaciones.


3. Conceptos Básicos de Laravel Notifications

En Laravel se crea una clase de notificación y se envía a un modelo Notifiable, como un usuario. En muchos casos, el modelo User ya incluye el trait Notifiable.

Crear una clase de notificación:

php artisan make:notification ReportGeneratedNotification
namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class ReportGeneratedNotification extends Notification
{
    use Queueable;

    public function __construct(
        public int $reportId,
        public string $title
    ) {}

    public function via(object $notifiable): array
    {
        return ['database'];
    }

    public function toArray(object $notifiable): array
    {
        return [
            'type' => 'report.generated',
            'report_id' => $this->reportId,
            'title' => $this->title,
            'message' => 'La generación del reporte ha finalizado.',
            'url' => route('reports.show', $this->reportId),
        ];
    }
}

Envío:

$user->notify(new ReportGeneratedNotification(
    reportId: $report->id,
    title: $report->title
));

En este ejemplo, la notificación se guarda en el canal database. Es decir, puede mostrarse en la lista del centro de notificaciones.


4. Preparar Database Notifications

Para guardar notificaciones dentro de la aplicación, crea la tabla notifications.

php artisan notifications:table
php artisan migrate

Esto crea la tabla notifications. Los datos se guardan como JSON y el estado de lectura se gestiona con read_at.

La lista de notificaciones del usuario puede obtenerse así:

$notifications = auth()->user()
    ->notifications()
    ->latest()
    ->paginate(20);

Solo notificaciones no leídas:

$unreadNotifications = auth()->user()
    ->unreadNotifications()
    ->latest()
    ->get();

Marcar como leída:

$notification = auth()->user()
    ->notifications()
    ->whereKey($id)
    ->firstOrFail();

$notification->markAsRead();

Marcar todas como leídas:

auth()->user()
    ->unreadNotifications
    ->markAsRead();

Database Notifications es una base muy manejable para construir un centro de notificaciones.


5. Diseño de Datos de Notificación: Hacerlos Fáciles de Mostrar Después

En data puedes guardar información libremente. Sin embargo, si guardas cualquier cosa, la capa de presentación se vuelve confusa. En la práctica, conviene estandarizar una estructura como esta:

[
    'type' => 'report.generated',
    'title' => 'Reporte generado',
    'message' => 'Ya puedes revisar el reporte de ventas de abril.',
    'url' => route('reports.show', $report->id),
    'severity' => 'info',
    'action_label' => 'Ver reporte',
]

Campos recomendados:

  • type: tipo de notificación
  • title: título breve
  • message: cuerpo
  • url: enlace a detalle u operación
  • severity: info / success / warning / danger, etc.
  • action_label: texto del enlace o botón
  • meta: información adicional si es necesaria

Con esta estructura, es más fácil reutilizar la información en listas, toasts y correos.

Como precaución, evita incluir demasiada información confidencial en los datos de notificación. La tabla de notificaciones puede mostrarse más adelante o aparecer en logs y pantallas administrativas. Si incluyes datos personales o secretos, que sea lo mínimo necesario.


6. Pantalla del Centro de Notificaciones: Lista, Estado de Lectura y Estado Vacío

Un centro de notificaciones no debe limitarse a listar notificaciones. También debe mostrar claramente su estado.

<section aria-labelledby="notifications-title">
    <h1 id="notifications-title" class="text-2xl font-semibold">
        Notificaciones
    </h1>

    <p role="status" aria-live="polite" class="mt-2 text-sm">
        Tienes {{ auth()->user()->unreadNotifications()->count() }} notificaciones sin leer.
    </p>

    @if($notifications->isEmpty())
        <div class="border rounded p-4 mt-6">
            <h2 class="text-lg font-semibold">No hay notificaciones</h2>
            <p class="mt-2">
                Cuando recibas nuevos avisos, aparecerán en esta pantalla.
            </p>
        </div>
    @else
        <ul class="mt-6 divide-y">
            @foreach($notifications as $notification)
                @php
                    $data = $notification->data;
                    $isUnread = is_null($notification->read_at);
                @endphp

                <li class="py-4">
                    <article aria-labelledby="notification-{{ $notification->id }}-title">
                        <div class="flex items-start justify-between gap-4">
                            <div>
                                <h2 id="notification-{{ $notification->id }}-title" class="font-semibold">
                                    {{ $data['title'] ?? 'Notificación' }}

                                    @if($isUnread)
                                        <span class="ml-2 rounded bg-blue-100 px-2 py-1 text-sm text-blue-900">
                                            Sin leer
                                        </span>
                                    @endif
                                </h2>

                                <p class="mt-1">
                                    {{ $data['message'] ?? '' }}
                                </p>

                                <p class="mt-1 text-sm text-gray-600">
                                    {{ $notification->created_at->diffForHumans() }}
                                </p>
                            </div>

                            @if(!empty($data['url']))
                                <a href="{{ $data['url'] }}" class="underline">
                                    {{ $data['action_label'] ?? 'Ver detalles' }}
                                </a>
                            @endif
                        </div>
                    </article>
                </li>
            @endforeach
        </ul>

        <div class="mt-6">
            {{ $notifications->links() }}
        </div>
    @endif
</section>

En este ejemplo, el estado no leído se muestra no solo con azul, sino también con el texto “Sin leer”. Es importante no depender solo del color.
Además, en el estado vacío no se muestra solo “No hay notificaciones”, sino también una explicación de dónde aparecerán en el futuro.


7. Gestión de Lectura: Separar Lectura Automática y Manual

Hay dos enfoques principales para gestionar notificaciones leídas.

7.1 Marcar Como Leída al Mostrar

Se marca como leída cuando se abre el detalle o se hace clic en el enlace de la notificación.
Es natural, pero marcar como leída solo por mostrar la lista requiere cuidado, porque la persona quizá no la leyó realmente.

7.2 Marcar Como Leída Manualmente

Se ofrece un botón “Marcar como leída” o una acción “Marcar todas como leídas”.
Permite al usuario gestionar el estado con más control, aunque añade una operación.

En la práctica, suele ser manejable marcar como leída al abrir el enlace de la notificación y ofrecer también “Marcar todas como leídas” cuando sea necesario.

Route::patch('/notifications/{id}/read', function (string $id) {
    $notification = auth()->user()
        ->notifications()
        ->whereKey($id)
        ->firstOrFail();

    $notification->markAsRead();

    return back()->with('status', 'La notificación se marcó como leída.');
})->name('notifications.read');

UI:

<form method="POST" action="{{ route('notifications.read', $notification->id) }}">
    @csrf
    @method('PATCH')

    <button type="submit" class="underline">
        Marcar como leída
    </button>
</form>

Después de marcarla como leída, es amable informar el resultado con role="status".


8. Badge de Notificaciones: Comunicar Estado con Número y Etiqueta

Si muestras un badge en el encabezado o barra lateral, un círculo rojo por sí solo no comunica suficiente significado. Incluye número y texto.

<a href="{{ route('notifications.index') }}" class="relative inline-flex items-center">
    <span>Notificaciones</span>

    @if($unreadCount > 0)
        <span class="ml-2 rounded-full bg-red-600 px-2 py-1 text-sm text-white">
            {{ $unreadCount }}
        </span>

        <span class="sr-only">
            Tienes {{ $unreadCount }} notificaciones sin leer
        </span>
    @endif
</a>

Las personas que ven la pantalla reciben el número, y quienes usan lectores de pantalla reciben la explicación con sr-only.
Lo importante no es “se entiende porque hay un círculo rojo”, sino comunicar por texto cuántas notificaciones no leídas existen.


9. Toasts: Úsalos con Moderación y Cuida Cómo Desaparecen

Para avisos ligeros como “guardado completado”, a veces se usa una UI de toast. Es práctica, pero requiere cuidado de accesibilidad.

  • No debe desaparecer demasiado rápido
  • Debe poder cerrarse con teclado, no solo con mouse
  • No uses solo toast para notificaciones importantes
  • No debe robar el foco automáticamente
  • Usa correctamente role="status" o role="alert"

Ejemplo de notificación de éxito:

@if(session('status'))
    <div role="status" aria-live="polite" class="border rounded p-3 mb-4">
        {{ session('status') }}
    </div>
@endif

Para errores graves:

@if(session('error'))
    <div role="alert" class="border rounded p-3 mb-4">
        {{ session('error') }}
    </div>
@endif

Como regla general, las notificaciones de finalización de baja prioridad pueden usar role="status", y los fallos urgentes role="alert".
Pero role="alert" puede interrumpir fuertemente la lectura, así que no conviene abusar.


10. Diferenciar con Notificaciones por Correo: Decidir Según Importancia y Necesidad de Acción

Algunas notificaciones pueden quedarse solo dentro de la app; otras deben enviarse también por correo.

Ejemplos donde basta la notificación dentro de la app

  • se añadió un comentario
  • terminó la generación de un reporte
  • se completó un cambio menor de configuración

Ejemplos donde también conviene enviar correo

  • se cambió la contraseña
  • se emitió una factura
  • se modificó una configuración de seguridad
  • se envió una invitación
  • hay una solicitud de aprobación importante

En Laravel Notifications, puedes cambiar canales con via().

public function via(object $notifiable): array
{
    return ['database', 'mail'];
}

También en el correo conviene usar un asunto concreto, cuerpo breve y texto de enlace claro, igual que en el centro de notificaciones.
Por ejemplo, en lugar de “aquí”, usa algo como “Ver factura”.


11. Configuración de Notificaciones: Dar Margen de Elección al Usuario

A medida que aumentan las notificaciones, se necesita permitir que cada usuario elija cómo recibirlas. Especialmente en correo, demasiadas notificaciones innecesarias pueden causar bajas o insatisfacción.

Ejemplos de configuración:

  • Notificaciones dentro de la app: recibir siempre
  • Notificaciones por correo: solo importantes
  • Digest: diario o semanal
  • Notificaciones de seguridad: no desactivables
  • Notificaciones de marketing: desactivables voluntariamente

Ejemplo de diseño DB:

notification_preferences
- user_id
- type
- channel
- enabled

Antes de enviar, se comprueba la configuración.

if ($user->wantsNotification('report.generated', 'mail')) {
    $user->notify(new ReportGeneratedNotification($report->id, $report->title));
}

No hace falta permitir que el usuario elija todo, pero cuanto más crezcan las notificaciones, más importante será una pantalla de configuración.


12. Agrupación de Notificaciones: Evitar la Fatiga

Si llegan muchas notificaciones del mismo tipo en poco tiempo, el usuario se cansa.
Por ejemplo, si se añaden 10 comentarios, puede ser más amable enviar “Tienes 10 comentarios nuevos” que 10 notificaciones individuales.

Ideas de agrupación:

  • agrupar notificaciones similares en un corto periodo
  • enviar digest periódicos
  • mostrar solo la cantidad no leída en el badge
  • permitir ver detalles en la lista

En la práctica, no hace falta crear agrupaciones complejas desde el inicio. Cuando las notificaciones empiecen a crecer, conviene mirar logs y uso real, y agrupar primero las notificaciones más frecuentes.


13. Colas: El Envío de Notificaciones Debe Ser Asíncrono por Defecto

Si correos o notificaciones externas se envían de forma síncrona, la respuesta de pantalla se vuelve lenta. Usa Queueable en la clase de notificación e implementa ShouldQueue cuando sea necesario.

use Illuminate\Contracts\Queue\ShouldQueue;

class ReportGeneratedNotification extends Notification implements ShouldQueue
{
    use Queueable;

    // ...
}

Así puedes procesar el envío mediante colas.
En operación, también debes monitorizar retrasos y fallos de la cola de notificaciones. Si una notificación tarda demasiado, para el usuario puede parecer que el proceso no terminó.


14. Idempotencia: Diseñar para no Enviar Dos Veces la Misma Notificación

Cuando usas colas o eventos, existe la posibilidad de enviar la misma notificación varias veces. En notificaciones importantes, hay que prevenir duplicados.

Ejemplos de métodos:

  • guardar un flag de enviado por objeto notificado
  • no crear una notificación si ya existe una no leída con el mismo type e ID objetivo
  • prevenir duplicados de corto plazo con cache lock

Ejemplo:

$exists = $user->notifications()
    ->where('type', ReportGeneratedNotification::class)
    ->where('data->report_id', $report->id)
    ->exists();

if (! $exists) {
    $user->notify(new ReportGeneratedNotification($report->id, $report->title));
}

Aunque no sea necesario cuando hay pocas notificaciones, es especialmente importante para facturación y seguridad.


15. Notificaciones en Tiempo Real: Útiles, Pero sin Interrumpir la Operación

Con Broadcasting puedes entregar notificaciones en tiempo real a la pantalla.
Sin embargo, requieren cuidado. Si la pantalla se actualiza sola, puede interrumpir la operación del usuario.

Bases de una notificación en tiempo real accesible:

  • no mover el foco automáticamente
  • actualizar la cantidad de notificaciones con texto
  • usar aria-live="polite"
  • considerar role="alert" solo para avisos urgentes
  • no completar información importante solo con toast

Ejemplo:

<div id="notification-live-region" role="status" aria-live="polite" class="sr-only"></div>

Cuando llegue una notificación en tiempo real, inserta una frase breve en esa región.

Tienes 1 notificación nueva.

Así el cambio de estado se comunica no solo mediante el badge visual, sino también por lectura.


16. Pruebas del Centro de Notificaciones: Proteger Guardado, Lectura y Visualización

La función de notificaciones tiene muchos estados, por lo que vale la pena cubrirla con pruebas.

16.1 Prueba de Guardado de Notificación

public function test_report_generated_notification_is_stored()
{
    $user = User::factory()->create();

    $user->notify(new ReportGeneratedNotification(
        reportId: 1,
        title: 'Reporte mensual'
    ));

    $this->assertDatabaseHas('notifications', [
        'notifiable_id' => $user->id,
        'notifiable_type' => User::class,
    ]);
}

16.2 Prueba de Marcar Como Leída

public function test_user_can_mark_notification_as_read()
{
    $user = User::factory()->create();

    $user->notify(new ReportGeneratedNotification(
        reportId: 1,
        title: 'Reporte mensual'
    ));

    $notification = $user->notifications()->first();

    $this->actingAs($user)
        ->patch(route('notifications.read', $notification->id))
        ->assertRedirect();

    $this->assertNotNull($notification->fresh()->read_at);
}

16.3 Prueba de que no se Puede Marcar Como Leída una Notificación Ajena

public function test_user_cannot_mark_others_notification_as_read()
{
    $user = User::factory()->create();
    $other = User::factory()->create();

    $other->notify(new ReportGeneratedNotification(
        reportId: 1,
        title: 'Reporte mensual'
    ));

    $notification = $other->notifications()->first();

    $this->actingAs($user)
        ->patch(route('notifications.read', $notification->id))
        ->assertNotFound();
}

Las notificaciones están vinculadas a información personal, por lo que es indispensable confirmar que nadie pueda operar notificaciones de otra persona.


17. Errores Frecuentes y Cómo Evitarlos

17.1 Demasiadas Notificaciones

Si todo se notifica, las notificaciones importantes quedan enterradas.
Organiza tipos y prioridades; si hace falta, agrupa.

17.2 Badge de No Leído Solo con Color

Un círculo rojo por sí solo no comunica significado.
Muestra cantidad no leída con número y texto.

17.3 Toast que Desaparece Demasiado Rápido

Una notificación que desaparece antes de poder leerse no es amable.
La información importante también debe quedar en el centro de notificaciones.

17.4 Diferencias Excesivas entre Correo y Notificación Interna

Puede confundir al usuario.
Unifica los campos básicos de datos de notificación.

17.5 Permitir Operar Notificaciones de Otra Persona

Es un fallo de autorización.
Al obtener la notificación, busca siempre desde las notificaciones del usuario autenticado.

17.6 Notificaciones en Tiempo Real que Interrumpen la Operación

No robes el foco y comunica de forma discreta con aria-live="polite".


18. Checklist para Compartir

Diseño

  • [ ] Los tipos y prioridades de notificación están organizados
  • [ ] Existe una diferenciación entre notificaciones internas, por correo y en tiempo real
  • [ ] Hay campos comunes para datos de notificación
  • [ ] Existe una política de preferencias y bajas

Database Notifications

  • [ ] La tabla notifications está creada
  • [ ] Existe una lista de notificaciones
  • [ ] Hay gestión de leídas/no leídas
  • [ ] No se pueden operar notificaciones ajenas

UI / Accesibilidad

  • [ ] La cantidad de notificaciones se comunica con texto
  • [ ] El estado no leído no se comunica solo con color
  • [ ] Existe explicación para el estado vacío
  • [ ] Las notificaciones de éxito usan role="status"
  • [ ] Los fallos importantes usan role="alert"
  • [ ] Las actualizaciones en tiempo real no roban el foco

Operación

  • [ ] El envío de notificaciones usa cola
  • [ ] Se previene el doble envío de notificaciones importantes
  • [ ] Existe una política de agrupación cuando aumentan las notificaciones
  • [ ] Se monitorizan fallos de notificación

Pruebas

  • [ ] Hay pruebas de guardado de notificaciones
  • [ ] Hay pruebas de marcado como leída
  • [ ] Hay pruebas para impedir operar notificaciones ajenas
  • [ ] Hay pruebas de visualización de la lista

19. Conclusión

La función de notificaciones de Laravel no sirve solo para enviar correos. Es un mecanismo potente que puede ampliarse a notificaciones dentro de la app, centro de notificaciones, gestión de lectura y actualizaciones en tiempo real. Lo importante no es aumentar notificaciones, sino entregar la información necesaria, en el momento necesario y de forma comprensible.

Primero, guarda notificaciones con Database Notifications y prepara una lista, badge de no leídas y marcado como leído. Después, combina las notificaciones importantes con correo y considera preferencias y agrupación. Las notificaciones en tiempo real son útiles, pero deben usarse con moderación para no interrumpir la operación.

Las notificaciones son una guía para ayudar la acción del usuario. Al no depender solo del color, comunicar estados con texto, considerar lectores de pantalla y evitar que la información importante quede enterrada, puedes crear una experiencia de notificación confiable.


Enlaces de Referencia

Salir de la versión móvil