0

Creating notifications — mail, database, Slack, custom channels

Intermediate5 min read·lv-21-002

Concept

Database notifications store notifications in the database so users can view their notification history (inbox), mark them as read, and retrieve unread counts. This powers "notification bell" UI features.

Setup: php artisan notifications:table && php artisan migrate creates the notifications table. Add Notifiable trait to the User model (already there by default).

Storing: Add 'database' to the via() array in the notification class. Implement toDatabase(object $notifiable): array returning the data to store.

notifications table columns: id (UUID), type (notification class name), notifiable_type, notifiable_id, data (JSON), read_at (nullable timestamp), created_at, updated_at.

Accessing notifications on a user:

  • $user->notifications: All notifications (newest first).
  • $user->unreadNotifications: Where read_at IS NULL.
  • $user->readNotifications: Where read_at IS NOT NULL.

Marking as read:

  • $notification->markAsRead(): Sets read_at = now().
  • $user->unreadNotifications->markAsRead(): Marks all.
  • $user->notifications()->update(['read_at' => now()]): Mass update.

notificationSent event: Fired after each notification. Listen for logging or tracking.

Code Example

php
<?php
// Notification with database channel
class CommentPosted extends \Illuminate\Notifications\Notification
{
    public function __construct(
        private readonly \App\Models\Comment $comment,
        private readonly \App\Models\Post $post,
    ) {}

    public function via(object $notifiable): array
    {
        return ['database', 'mail']; // store in DB + send email
    }

    public function toDatabase(object $notifiable): array
    {
        return [
            'message'    => "{$this->comment->user->name} commented on your post",
            'post_id'    => $this->post->id,
            'post_title' => $this->post->title,
            'comment_id' => $this->comment->id,
            'excerpt'    => \Illuminate\Support\Str::limit($this->comment->body, 100),
            'url'        => route('posts.show', $this->post) . '#comment-' . $this->comment->id,
        ];
    }

    public function toMail(object $notifiable): \Illuminate\Notifications\Messages\MailMessage
    {
        return (new \Illuminate\Notifications\Messages\MailMessage)
            ->subject("New comment on '{$this->post->title}'")
            ->line("{$this->comment->user->name} commented: {$this->comment->body}")
            ->action('View Comment', route('posts.show', $this->post));
    }
}

// Accessing notifications in a controller
class NotificationController extends Controller
{
    public function index(Request $request)
    {
        return response()->json([
            'unread_count'  => $request->user()->unreadNotifications()->count(),
            'notifications' => $request->user()->notifications()->paginate(20),
        ]);
    }

    public function markAsRead(Request $request, string $id)
    {
        $notification = $request->user()->notifications()->findOrFail($id);
        $notification->markAsRead();
        return response()->noContent();
    }

    public function markAllRead(Request $request)
    {
        $request->user()->unreadNotifications()->update(['read_at' => now()]);
        return response()->noContent();
    }
}

// Blade notification bell — unread count
// {{ auth()->user()->unreadNotifications()->count() }}
// @foreach (auth()->user()->notifications()->limit(10)->get() as $notification)
//     <div class="{{ $notification->read_at ? 'read' : 'unread' }}">
//         {{ $notification->data['message'] }}
//     </div>
// @endforeach