ArtisanPack UI
DocsCMS FrameworkNotifications — Managing Notifications

Notifications — Managing Notifications

CMS Framework v2.8.0

This guide covers retrieving, reading, and dismissing notifications for users. Each user has their own state for every notification they receive, tracked...

This guide covers retrieving, reading, and dismissing notifications for users.

Notification States

Each user has their own state for every notification they receive, tracked in the notification_user pivot table:

  • Unreadis_read = false, is_dismissed = false
  • Readis_read = true, is_dismissed = false
  • Dismissedis_dismissed = true (hidden from user's list)

Retrieving Notifications

Using User Model Methods

The HasNotifications trait provides convenient relationship methods:

$user = auth()->user();

// All notifications for this user
$all = $user->systemNotifications;

// Only unread notifications
$unread = $user->unreadSystemNotifications;

// Count unread
$count = $user->unreadSystemNotificationsCount();

Using Helper Functions

use function apGetNotifications;
use function apGetUnreadNotificationCount;

// Get notifications for user ID 1
$notifications = apGetNotifications($userId, $limit = 10, $unreadOnly = false);

// Get only unread
$unread = apGetNotifications($userId, $limit = 10, $unreadOnly = true);

// Get unread count
$count = apGetUnreadNotificationCount($userId);

Accessing Notification Data

$user = auth()->user();

foreach ($user->systemNotifications as $notification) {
    echo $notification->id;           // Notification ID
    echo $notification->title;        // Title
    echo $notification->content;      // Content
    echo $notification->type->label(); // "Success", "Error", etc.
    echo $notification->metadata;     // Array of custom data
    echo $notification->created_at;   // Carbon timestamp

    // Access pivot data (user-specific state)
    echo $notification->pivot->is_read;
    echo $notification->pivot->read_at;
    echo $notification->pivot->is_dismissed;
    echo $notification->pivot->dismissed_at;
}

Marking as Read

Mark Single Notification

Using the User model:

$user = auth()->user();
$notificationId = 123;

$success = $user->markNotificationAsRead($notificationId);

if ($success) {
    // Notification marked as read
} else {
    // Failed (notification not found or doesn't belong to user)
}

Using helper functions:

use function apMarkNotificationAsRead;

apMarkNotificationAsRead($notificationId, $userId);

Mark All as Read

Using the User model:

$user = auth()->user();
$count = $user->markAllNotificationsAsRead();

echo "Marked {$count} notifications as read";

Using helper functions:

use function apMarkAllNotificationsAsRead;

$count = apMarkAllNotificationsAsRead($userId);

Read State

When marked as read:

  • is_read is set to true
  • read_at is set to current timestamp
  • The notification remains visible in the user's list
  • The ap.notifications.readNotification action fires

Dismissing Notifications

Dismissing removes a notification from the user's visible list:

Dismiss Single Notification

Using the User model:

$user = auth()->user();
$notificationId = 123;

$success = $user->dismissNotification($notificationId);

if ($success) {
    // Notification dismissed
}

Using helper functions:

use function apDismissNotification;

apDismissNotification($notificationId, $userId);

Dismiss All Notifications

Using the User model:

$user = auth()->user();
$count = $user->dismissAllNotifications();

echo "Dismissed {$count} notifications";

Using helper functions:

use function apDismissAllNotifications;

$count = apDismissAllNotifications($userId);

Dismissed State

When dismissed:

  • is_dismissed is set to true
  • dismissed_at is set to current timestamp
  • The notification is hidden from queries by default
  • The ap.notifications.dismissNotification action fires

Query Scopes

The Notification model includes helpful query scopes:

Unread for User

use ArtisanPackUI\CMSFramework\Modules\Notifications\Models\Notification;

$unread = Notification::unreadForUser($userId)->get();

Read for User

$read = Notification::readForUser($userId)->get();

Not Dismissed for User

$active = Notification::notDismissedForUser($userId)->get();

By Type

use ArtisanPackUI\CMSFramework\Modules\Notifications\Enums\NotificationType;

$errors = Notification::ofType(NotificationType::Error)
    ->notDismissedForUser($userId)
    ->get();

Combining Scopes

// Get unread error notifications
$unreadErrors = Notification::unreadForUser($userId)
    ->ofType(NotificationType::Error)
    ->orderByDesc('created_at')
    ->limit(5)
    ->get();

Practical Examples

Display Notification Bell Icon

@php
    $unreadCount = auth()->user()->unreadSystemNotificationsCount();
@endphp

<div class="notification-bell">
    <i class="fas fa-bell"></i>
    @if($unreadCount > 0)
        <span class="badge">{{ $unreadCount }}</span>
    @endif
</div>

Notification Dropdown

@php
    $notifications = auth()->user()->systemNotifications()->limit(5)->get();
@endphp

<div class="notifications-dropdown">
    @forelse($notifications as $notification)
        <div class="notification-item {{ $notification->pivot->is_read ? 'read' : 'unread' }}">
            <div class="icon {{ $notification->type->colorClass() }}">
                <i class="{{ $notification->type->icon() }}"></i>
            </div>
            <div class="content">
                <h4>{{ $notification->title }}</h4>
                <p>{{ $notification->content }}</p>
                <small>{{ $notification->created_at->diffForHumans() }}</small>
            </div>
            <div class="actions">
                @unless($notification->pivot->is_read)
                    <button wire:click="markAsRead({{ $notification->id }})">
                        Mark as read
                    </button>
                @endunless
                <button wire:click="dismiss({{ $notification->id }})">
                    Dismiss
                </button>
            </div>
        </div>
    @empty
        <div class="no-notifications">
            No notifications
        </div>
    @endforelse

    @if($notifications->count() > 0)
        <div class="notification-actions">
            <button wire:click="markAllAsRead">Mark all as read</button>
            <button wire:click="dismissAll">Clear all</button>
        </div>
    @endif
</div>

Livewire Component

use Livewire\Component;
use ArtisanPackUI\CMSFramework\Modules\Notifications\Models\Notification;

class NotificationDropdown extends Component
{
    public function markAsRead($notificationId)
    {
        auth()->user()->markNotificationAsRead($notificationId);
        $this->dispatch('notification-updated');
    }

    public function dismiss($notificationId)
    {
        auth()->user()->dismissNotification($notificationId);
        $this->dispatch('notification-updated');
    }

    public function markAllAsRead()
    {
        auth()->user()->markAllNotificationsAsRead();
        $this->dispatch('notification-updated');
    }

    public function dismissAll()
    {
        auth()->user()->dismissAllNotifications();
        $this->dispatch('notification-updated');
    }

    public function render()
    {
        return view('livewire.notification-dropdown', [
            'notifications' => auth()->user()->systemNotifications()->limit(10)->get(),
            'unreadCount' => auth()->user()->unreadSystemNotificationsCount()
        ]);
    }
}

API Controller Actions

use Illuminate\Http\Request;

class UserNotificationController extends Controller
{
    public function markAsRead(Request $request, int $id)
    {
        $success = $request->user()->markNotificationAsRead($id);

        return response()->json([
            'success' => $success,
            'unread_count' => $request->user()->unreadSystemNotificationsCount()
        ]);
    }

    public function dismiss(Request $request, int $id)
    {
        $success = $request->user()->dismissNotification($id);

        return response()->json(['success' => $success]);
    }

    public function markAllAsRead(Request $request)
    {
        $count = $request->user()->markAllNotificationsAsRead();

        return response()->json([
            'count' => $count,
            'unread_count' => 0
        ]);
    }
}

Pagination

For large notification lists, use pagination:

$notifications = Notification::notDismissedForUser($userId)
    ->orderByDesc('created_at')
    ->paginate(20);

return view('notifications.index', compact('notifications'));

Filtering by Type

Allow users to filter by notification type:

use ArtisanPackUI\CMSFramework\Modules\Notifications\Enums\NotificationType;

$type = request('type');

$query = Notification::notDismissedForUser($userId);

if ($type) {
    $query->ofType(NotificationType::from($type));
}

$notifications = $query->orderByDesc('created_at')->paginate(20);

Soft Delete vs Dismiss

The module uses dismissal rather than deletion:

  • Dismissed notifications remain in the database
  • Useful for audit trails and analytics
  • Users can potentially "undo" dismissals (with custom implementation)

If you need to permanently delete:

// Delete dismissed notifications older than 90 days
Notification::whereHas('users', function ($q) {
    $q->where('is_dismissed', true)
        ->where('dismissed_at', '<', now()->subDays(90));
})->delete();

Real-Time Updates

For real-time notification updates, combine with Laravel Echo and broadcasting:

// In your event
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class NotificationSent implements ShouldBroadcast
{
    public function __construct(public Notification $notification, public array $userIds)
    {
    }

    public function broadcastOn(): array
    {
        return array_map(
            fn($userId) => new PrivateChannel("users.{$userId}"),
            $this->userIds
        );
    }
}
// Frontend
Echo.private(`users.${userId}`)
    .listen('NotificationSent', (e) => {
        // Update notification count
        updateNotificationBadge();
        // Show toast
        showNotificationToast(e.notification);
    });

Best Practices

Auto-Mark as Read

Consider automatically marking notifications as read when the user views them:

public function show(Notification $notification)
{
    $user = auth()->user();

    if (!$notification->pivot->is_read) {
        $user->markNotificationAsRead($notification->id);
    }

    return view('notifications.show', compact('notification'));
}

Cleanup Old Notifications

Schedule a command to clean up old dismissed notifications:

// In App\Console\Kernel
protected function schedule(Schedule $schedule)
{
    $schedule->command('notifications:cleanup')->monthly();
}
// Command
public function handle()
{
    $deleted = Notification::whereDoesntHave('users', function ($q) {
        $q->where('is_dismissed', false);
    })->where('created_at', '<', now()->subMonths(6))->delete();

    $this->info("Deleted {$deleted} old notifications");
}

Cache Unread Count

Cache the unread count to reduce database queries:

public function unreadCount()
{
    return Cache::remember(
        "user.{$userId}.unread_notifications",
        300, // 5 minutes
        fn() => apGetUnreadNotificationCount($userId)
    );
}

// Invalidate cache when marking as read
public function markAsRead($notificationId)
{
    $success = auth()->user()->markNotificationAsRead($notificationId);

    if ($success) {
        Cache::forget("user.{$userId}.unread_notifications");
    }

    return $success;
}

Next Steps