Database Notifications
Persistent notification adalah row yang dapat ditemukan kembali oleh pengguna di kemudian hari. Gunakan ketika pesan memiliki umur lebih panjang daripada request yang membuatnya — misalnya queued export yang selesai, import yang sebagian gagal, atau informasi lain yang tetap perlu dilihat pengguna yang sedang tidak membuka Panel. Panel menyimpannya menggunakan table notifications bawaan Laravel, bukan table khusus miliknya sendiri, sehingga unreadNotifications, markAsRead(), dan API Laravel lainnya langsung bekerja tanpa tambahan kode.
Contoh minimal yang berfungsi
<?php
use PandaPanel\Notifications\Notification;
Notification::make('export-ready')
->title('Your export is ready')
->body('1,204 records')
->success()
->persistent()
->send($user);2
3
4
5
6
7
8
9
10
Hasilnya adalah satu row di notifications, satu toast pada setiap Panel yang sedang dibuka user tersebut, dan badge bell yang sekarang bernilai 1.
Apa yang dilakukan persistent()
public function persistent(bool $persistent = true): selfNonaktif secara default. Di dalam send() hanya ada satu branch:
if ($this->persistent && method_exists($user, 'notify')) {
$user->notify(new PanelDatabaseNotification($this->toArray()));
}2
3
Digunakan method_exists() dan bukan type check karena yang penting adalah apakah notifiable ini benar-benar dapat menerima notifikasi. User model tanpa trait tersebut tidak memiliki tempat untuk menyimpan row — tetapi toast tetap dapat dikirim.
Urutan di dalam send() adalah persist terlebih dahulu, lalu broadcast. Jadi ketika user mengklik action pada toast dan berpindah ke notification center, row notifikasinya sudah tersedia.
PanelDatabaseNotification
PandaPanel\Notifications\PanelDatabaseNotification meng-extend Illuminate\Notifications\Notification dan melakukan dua hal:
public function via(object $notifiable): array // ['database']
public function toArray(object $notifiable): array // the payload, unchanged2
Broadcasting sengaja tidak dimasukkan ke via(). Panel mengirim event-nya sendiri dengan payload yang sama, lalu bell melakukan refetch ketika event tersebut mengatakan bahwa sebuah row baru telah ditulis — satu subscription dan tidak ada event kedua yang bisa berbeda isi dari yang pertama.
Karena itu kolom data menyimpan hasil Notification::toArray() secara persis:
[
'name' => 'export-ready',
'title' => 'Your export is ready',
'body' => '1,204 records',
'color' => 'success',
'icon' => 'download',
'actions' => [
['name' => 'download', 'label' => 'Download', 'url' => '/admin/exports/users.csv',
'variant' => 'outline', 'markAsRead' => true, 'newTab' => false],
],
'type' => 'success',
'persistent' => true,
]2
3
4
5
6
7
8
9
10
11
12
13
User model
Notification center membutuhkan user model yang bersifat Notifiable. PandaPanel\Contracts\PanelNotifiable mendokumentasikan requirement tersebut:
namespace PandaPanel\Contracts;
interface PanelNotifiable
{
public function notifications(); // MorphMany<DatabaseNotification, Model>
public function unreadNotifications(); // MorphMany<DatabaseNotification, Model>
public function notify($instance); // void
}2
3
4
5
6
7
8
Tidak ada kewajiban untuk mengimplementasikan interface tersebut. Trait bawaan Laravel sudah menyediakan semua method, dan controller menerima model yang hanya menggunakan trait:
use Illuminate\Notifications\Notifiable;
final class User extends Authenticatable
{
use Notifiable;
}2
3
4
5
6
Interface ada agar requirement "user model harus Notifiable" tertulis secara eksplisit dan dapat dipahami static analysis, bukan baru diketahui ketika bell melempar error. Implementasikan jika Anda ingin PHPStan memeriksanya; runtime tidak bergantung pada interface itu.
Table
Package menyediakan database/migrations/2026_08_14_130919_create_notifications_table.php, menggunakan schema notifikasi standar Laravel:
| Column | Type |
|---|---|
id | uuid, primary |
type | string |
notifiable_type, notifiable_id | morphs |
data | text |
read_at | timestamp, nullable |
created_at, updated_at | timestamps |
dengan index(['notifiable_type', 'notifiable_id', 'read_at']) — dua pertanyaan utama terhadap table ini adalah "notifikasi apa saja yang dimiliki user ini" dan "berapa banyak yang belum dibaca".
php artisan migrateup() langsung berhenti jika table sudah ada karena banyak aplikasi Laravel sudah memilikinya. Migration package yang menganggap table tersebut selalu belum ada akan gagal pada migrate pertama setelah instalasi.
down() sengaja tidak simetris. Method tersebut memanggil PandaPanel\Support\PackageSchema::dropIfOwned(), yang hanya menghapus table ketika tidak ada migration lain bernama *_create_notifications_table yang pernah dijalankan dan daftar kolomnya persis sama dengan yang dibuat up(). Jika ownership tidak dapat dibuktikan dengan jelas, table dibiarkan tetap ada: table kosong yang tidak dihapus hampir tidak merugikan, sedangkan salah menghapus table dapat menghancurkan notifikasi aplikasi.
Membaca kembali notifikasi
Gunakan Eloquent biasa karena ini memang notifikasi Laravel biasa:
$user->notifications; // newest first, all of them
$user->unreadNotifications; // read_at is null
$user->unreadNotifications()->count();
$user->notifications()->first()?->markAsRead();
$user->unreadNotifications->markAsRead(); // the collection method
$user->notifications()->first()?->data['title'];2
3
4
5
6
7
8
Panel sendiri membacanya melalui dua jalur: endpoint index pada notification center (30 terbaru) dan unread count yang ditambahkan SharePanelData pada setiap request Panel. Lihat Notification center.
Notifikasi yang dikirim bagian lain dari aplikasi
Karena menggunakan table Laravel dan bukan table khusus Panel, ada satu konsekuensi yang perlu direncanakan: semua notification aplikasi yang dikirim melalui channel database akan ikut muncul pada bell Panel.
use Illuminate\Notifications\Notification as LaravelNotification;
final class InvoiceOverdue extends LaravelNotification
{
public function via(object $notifiable): array
{
return ['database'];
}
/**
* The keys the bell reads. Anything else in here is stored and ignored.
*/
public function toArray(object $notifiable): array
{
return [
'title' => 'Invoice overdue',
'body' => 'INV-2043 was due on Tuesday.',
'color' => 'warning',
'icon' => 'triangle-alert',
'actions' => [],
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Controller melakukan narrowing terhadap stored row menjadi id, title, body, color, icon, actions, read, dan createdAt. Row tanpa title dirender dengan title kosong; color di luar empat case akan fallback ke info; action yang gagal diparse dibuang. Tidak ada exception yang dilempar.
Hal yang perlu diperhatikan
- Persistent notification tetap dibroadcast.
->persistent()menambahkan row, bukan mematikan toast. Gunakan->persistent()->broadcast(false)ketika response yang sedang berjalan sudah membawa pesan yang sama;ExportActionmenggunakan pola ini. send()tidak menulis row untuk model yang tidak memilikinotify(). Tidak ada exception dan toast tetap dikirim. Jika persistent notification tidak pernah muncul di bell, periksa trait pada user model terlebih dahulu.- Migration belum dijalankan tidak menyebabkan seluruh Panel crash.
SharePanelData::unreadCount()menangkapQueryExceptiondan mengembalikan 0, sehingga Panel yang dipasang sebelummigratemenampilkan bell kosong daripada 500 di setiap page. - Bell hanya menampilkan 30.
PanelNotificationController::LIMITadalah private constant, bukan configuration. Row yang lebih lama tetap ada di table dan dapat diakses dengan Eloquent, hanya tidak ditampilkan di center. - Tidak ada pruning otomatis. Notifikasi hidup sampai user membersihkannya atau aplikasi menghapusnya. Scheduled command seperti
$user->notifications()->where('created_at', '<', now()->subMonths(3))->delete()merupakan pola umum.
Lihat juga
- Notification center — bell, endpoint, dan limit-nya
- Notification actions — tombol yang disimpan bersama row
- Toast notifications — sisi transient
- Queued notifications — sumber sebagian besar persistent notification
- Konfigurasi migrations
- Testing notifications