Queued Notifications
Queued job tidak memiliki response untuk membawa pesan kembali. Request yang memulainya sudah selesai beberapa menit lalu, dan user mungkin sedang berada di page lain, Panel lain, atau bahkan tidak membuka aplikasi. Inilah kasus utama panel notification: persist pesan agar bisa ditemukan nanti, broadcast agar Panel yang sedang terbuka dapat menampilkannya sekarang, dan beri tahu user ketika job gagal daripada membiarkannya menunggu bell yang tidak akan pernah berbunyi.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Auth;
use PandaPanel\Notifications\Notification;
use Throwable;
final class RebuildSearchIndex implements ShouldQueue
{
use Queueable;
public function __construct(private readonly int|string $owner) {}
public function handle(): void
{
// … the work …
$user = Auth::getProvider()->retrieveById($this->owner);
if ($user === null) {
return;
}
Notification::make('index-rebuilt')
->title('Search index rebuilt')
->body('4,102 records indexed.')
->success()
->persistent()
->send($user);
}
public function failed(?Throwable $exception): void
{
$user = Auth::getProvider()->retrieveById($this->owner);
if ($user === null) {
return;
}
Notification::make('index-failed')
->title('Search index rebuild failed')
->body($exception?->getMessage() ?? 'The index could not be rebuilt.')
->danger()
->persistent()
->send($user);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
php artisan queue:workBawa key, resolve user
Kedua job bawaan package menerima int|string $owner dan me-resolve user di sisi worker:
use Illuminate\Support\Facades\Auth;
$user = Auth::getProvider()->retrieveById($this->owner);
if ($user === null) {
return;
}2
3
4
5
6
7
Yang dibawa adalah scalar, bukan model, untuk alasan yang sama dengan RunPanelExport membawa deskripsi query alih-alih builder: queue payload adalah data, dan bentuk paling kecil sekaligus jujur yang perlu dibawa adalah key. Resolve melalui auth provider juga mendukung aplikasi dengan custom provider, serta menghasilkan null jika user dihapus antara dispatch dan eksekusi — kondisi yang seharusnya membuat job berhenti secara aman, bukan crash.
Notification::send() menerima Authenticatable, tepat seperti object yang dikembalikan provider.
Dua queue hop, bukan satu
Ada dua hop terpisah dan keduanya membutuhkan worker:
| Hop | Yang di-queue | Dikonfigurasi oleh |
|---|---|---|
| Job Anda | class ShouldQueue Anda | queue.default, $connection/$queue milik job |
| Broadcast | BroadcastEvent milik Laravel yang membungkus PanelNotificationSent | queue.default, kecuali event menentukan queue lain |
PanelNotificationSent dan PanelNotification mengimplementasikan ShouldBroadcast, bukan ShouldBroadcastNow, sehingga dispatch membuat job alih-alih berbicara langsung dengan broadcaster. Dengan QUEUE_CONNECTION=sync — konfigurasi local yang umum walaupun default Laravel sendiri adalah database — kedua hop berjalan langsung. Itu sebabnya realtime notification dapat terlihat berfungsi lokal tanpa worker tetapi berhenti ketika queue nyata digunakan.
Sisi database tidak di-queue: PanelDatabaseNotification tidak mengimplementasikan ShouldQueue, sehingga ->persistent() menulis row di dalam job yang memanggil send(). Artinya notifikasi tetap tersimpan walaupun tidak ada worker yang pernah memproses broadcast.
Panel context di dalam job
Scope Resource, table, dan URL-nya dibaca melalui current Panel, sedangkan queued job berjalan di luar request yang sebelumnya me-resolve Panel. Kedua job bawaan package membawa Panel id dan mengaktifkannya sebelum melakukan pekerjaan:
use PandaPanel\Core\PanelManager;
public function handle(PanelManager $manager): void
{
$panel = $manager->get($this->panelId);
$manager->setCurrentPanel($panel);
// … now Resource::query() and $panel->routeName() answer correctly …
}2
3
4
5
6
7
8
9
10
Gunakan pola yang sama untuk job apa pun yang membangun URL Panel bagi notification action:
use PandaPanel\Notifications\NotificationAction;
NotificationAction::make('view')
->label('View import')
->url(route($panel->routeName('import-file'), [
'file' => $result['report'],
'importer' => $importer,
], absolute: false));2
3
4
5
6
7
8
absolute: false bahkan lebih penting di worker daripada di request: APP_URL pada worker tidak selalu sama dengan hostname yang sedang dibuka user. URL relatif tidak dapat salah mengenai host tersebut.
Job yang disediakan package
Ada dua, dan keduanya sengaja memiliki strategi yang berlawanan.
PandaPanel\Jobs\RunPanelExport
| Property | Value | Alasan |
|---|---|---|
$tries | 3 | export hanya membaca row dan menulis file; file setengah jadi dapat digantikan pada retry berikutnya |
backoff() | [10, 60] | failure yang layak di-retry perlu waktu, bukan tiga percobaan cepat terhadap outage yang sama |
Saat sukses, job mengirim persistent notification dengan action Download. Setelah attempt terakhir gagal, failed() mengirim notifikasi danger berisi pesan exception. Keduanya persistent karena user meminta file; toast yang muncul saat user tidak melihat layar tidak akan cukup.
PandaPanel\Jobs\RunPanelImport
| Property | Value | Alasan |
|---|---|---|
$tries | 1 | import menulis row; job yang gagal di tengah mungkin sudah menulis sebagian data dan tidak ada cara umum untuk mengetahui mana yang sudah masuk |
Saat sukses, job menghapus uploaded file lalu mengirim notifikasi — success jika tidak ada row gagal, warning jika ada, dengan action "Download failed rows" bila failure report tersedia. failed() juga menghapus file lalu mengirim pesan exception agar error seperti "unsupported file format" atau "column count mismatch on row 12" memberi petunjuk perbaikan yang nyata.
Kapan pekerjaan di-queue
Kedua action tidak selalu masuk queue. Masing-masing bertanya kepada exporter atau importer:
if ($exporter::queueAfter() >= 0 && $count > $exporter::queueAfter()) {
RunPanelExport::dispatch(/* … */);
return;
}2
3
4
5
| Class | Method | Default |
|---|---|---|
PandaPanel\Actions\Exports\Exporter | static queueAfter(): int | 2000 records |
PandaPanel\Actions\Imports\Importer | static queueAfter(): int | 500 rows |
final class UserExporter extends Exporter
{
public static function queueAfter(): int
{
return 0; // always queue
}
}
final class SmallImporter extends Importer
{
public static function queueAfter(): int
{
return -1; // never queue: the >= 0 guard skips the branch entirely
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Digunakan angka, bukan boolean, karena kedua ekstrem sama-sama buruk: export kecil yang dipindahkan ke background memberi UX lebih buruk daripada menunggu sebentar, sedangkan export besar yang dipaksa selesai dalam request berisiko timeout.
Branch synchronous memberi notifikasi dengan cara berbeda — persisten menggunakan ->broadcast(false) lalu mem-flash toast melalui response yang memang sedang dikembalikan, karena mengirim pesan yang sama lewat websocket akan menampilkannya dua kali. Lihat Flash toast bridge.
Email dari queue
Satu notifikasi package menuju inbox, bukan Panel: PandaPanel\Notifications\TwoFactorCode, dikirim ke account yang mengaktifkan kode sign-in melalui email.
final class TwoFactorCode extends LaravelNotification implements ShouldQueue
{
use Queueable;
public function via(object $notifiable): array // ['mail']
public function toMail(object $notifiable): MailMessage
}2
3
4
5
6
7
Notifikasi ini ShouldQueue karena pengiriman email adalah bagian paling lambat dari sign-in dan user tidak seharusnya menunggu SMTP handshake. Kode sudah berada di cache ketika job di-dispatch, sehingga email yang terlambat hanya memperlambat login, bukan merusaknya. Ini sengaja bukan panel notification: credential yang dikirim ke satu alamat email tidak seharusnya disimpan pada bell yang bisa dibuka pada tempat lain.
Hal yang perlu diperhatikan
- Tidak ada worker berarti tidak ada toast. Row database tetap ditulis dan badge benar pada navigasi berikutnya, tetapi websocket diam karena job
BroadcastEventmasih menunggu di queue. Ini adalah penyebab paling umum laporan "broadcasting rusak". failed()dipanggil setelah attempt terakhir, bukan setelah setiap attempt. PadaRunPanelExportberarti user baru diberi tahu setelah tiga attempt, sesuai trade-off yang disengaja.failed()tidak dipanggil untuk job yang tidak pernah di-dispatch.dispatchSync()yang melempar exception akan meneruskannya ke caller; tangani sendiri bila user harus diberi tahu.- User yang sudah dihapus membuat job berhenti tanpa error.
retrieveById()menghasilkannulldan kedua job package langsung return. Pertahankan guard ini karenasend()tidak dapat menerimanull. - Jangan meng-queue object Panel
Notificationitu sendiri. Ini bukan Laravel notification dan tidak memiliki perilakuShouldQueue; object tersebut adalah builder yang menulis row dan mendispatch event. Queue pekerjaan yang membungkusnya. ->persistent()adalah bagian yang tetap bertahan ketika worker tidak tersedia. Broadcast-only notification dari job hilang jika browser tertutup atau queue tertunda. Persist semua hal yang tidak boleh terlewat user.
Lihat juga
- Database notifications — row yang ditinggalkan queued job
- Broadcasting — queue hop kedua
- Notification actions — link yang dipasang job
- Queued exports dan queued imports
- Failure reports
- Queue di production
- Testing notifications