Broadcasting
Broadcasting adalah mekanisme agar notifikasi dapat mencapai browser yang sedang terbuka. Job yang selesai sepuluh menit setelah request pemicunya sudah tidak memiliki response untuk membawa pesan kembali, sehingga Panel mendorong pesan tersebut melalui websocket — menghasilkan toast yang sama seperti flash message dan memperbarui bell yang sama seperti stored notification. Halaman ini membahas event, channel, toggle, dan subscription di client. Untuk menyiapkan broadcaster, lihat Setup Reverb dan Echo.
Contoh minimal yang berfungsi
<?php
use PandaPanel\Broadcasting\PanelNotification;
PanelNotification::dispatch($user, 'Your export is ready.', 'success');2
3
4
5
Semua Panel yang sedang dibuka oleh pengguna tersebut akan menampilkan toast hijau. Tidak ada data yang disimpan, sehingga pengguna yang sedang tidak membuka Panel tidak akan melihat apa pun.
Dua event
Keduanya menggunakan channel dan nama broadcast yang sama karena bagi frontend keduanya merupakan hal yang sama: sebuah pesan untuk ditampilkan dan, ketika notifikasinya persisten, sebuah bell yang perlu bertambah. Dua nama berbeda berarti dua subscription dan dua peluang untuk terjadi ketidaksinkronan.
| Event | Dipicu oleh | Membawa |
|---|---|---|
PandaPanel\Notifications\PanelNotificationSent | Notification::send() | seluruh payload notifikasi |
PandaPanel\Broadcasting\PanelNotification | Anda, secara langsung | message, type, dan link opsional |
PanelNotificationSent
namespace PandaPanel\Notifications;
final class PanelNotificationSent implements ShouldBroadcast
{
public function __construct(
public readonly Authenticatable $user,
public readonly array $payload,
) {}
public function broadcastOn(): array; // [new PrivateChannel(PanelNotification::channelFor($user))]
public function broadcastAs(): string; // 'panel.notification'
public function broadcastWith(): array;
}2
3
4
5
6
7
8
9
10
11
12
13
Biasanya Anda tidak perlu membuat event ini secara manual. Notification::send() melakukannya:
if ($this->broadcast) {
event(new PanelNotificationSent($user, $this->toArray()));
}2
3
broadcastWith() berisi payload notifikasi dengan tambahan message, serta persistent yang ditegaskan kembali sebagai strict boolean — toArray() sendiri sebenarnya sudah membawa persistent:
[
...$this->payload,
'message' => $this->payload['title'] ?? '', // the toast reads this
'persistent' => true, // the bell reads this
]2
3
4
5
message dikirim berdampingan dengan title agar tidak ada sisi yang perlu menebak, sedangkan persistent memberi tahu frontend apakah bell perlu mengambil row baru tanpa melakukan request tambahan untuk menanyakannya.
PanelNotification
namespace PandaPanel\Broadcasting;
final class PanelNotification implements ShouldBroadcast
{
public function __construct(
public readonly Authenticatable $user,
public readonly string $message,
public readonly string $type = 'info', // 'success'|'info'|'warning'|'error'
public readonly ?string $url = null,
public readonly ?string $urlLabel = null,
) {}
public function broadcastOn(): array;
public static function channelFor(Authenticatable $user): string;
public function broadcastAs(): string; // 'panel.notification'
public function broadcastWith(): array; // {type, message, url, urlLabel}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use PandaPanel\Broadcasting\PanelNotification;
// Dispatchable, so all four of these work.
PanelNotification::dispatch($user, 'Heads up');
event(new PanelNotification($user, 'Heads up'));
PanelNotification::dispatch($user, 'Import finished', 'warning');
PanelNotification::dispatch($user, 'Your export is ready.', 'success', $url, 'Download');2
3
4
5
6
7
Ini adalah satu-satunya event yang membawa url dan urlLabel, yang kemudian diubah client menjadi tombol pada toast. Export yang sudah selesai menghasilkan sebuah file; toast yang hanya mengatakan bahwa file siap tetapi tidak memberi link hanya membuat pengguna harus mencarinya sendiri.
Channel
public static function channelFor(Authenticatable $user): string
{
return 'App.Models.User.'.$user->getAuthIdentifier();
}2
3
4
Ini memakai nama channel default Laravel tanpa perubahan, sehingga notifikasi yang dibroadcast oleh bagian lain dari aplikasi dapat tiba pada channel yang sama. Nama channel dibangun di server, bukan ditulis ulang di Vue, agar kedua sisi tidak mungkin drift.
Kedua event membungkus channel tersebut dalam PrivateChannel, sehingga nama yang muncul pada wire adalah private-App.Models.User.7. Subscription membutuhkan callback authorization channel di routes/channels.php — lihat Authorization channel.
Mengaktifkan dan menonaktifkan
use PandaPanel\Core\Panel;
$panel->broadcasting(false);2
3
| Method | Signature | Default |
|---|---|---|
broadcasting | broadcasting(bool $broadcasting = true): self | true |
hasBroadcasting | hasBroadcasting(): bool | true |
getBroadcastChannel | getBroadcastChannel(?Authenticatable $user): ?string | null ketika nonaktif atau tidak ada user |
use PandaPanel\Core\Panel;
$panel = Panel::make('quiet')->broadcasting(false);
$panel->hasBroadcasting(); // false
$panel->getBroadcastChannel($user); // null2
3
4
5
6
Tidak ada koneksi yang dibuka sampai sebuah page benar-benar melakukan subscription, dan page hanya melakukan subscription ketika server mengirimkan channel. Artinya Panel yang mematikan broadcasting benar-benar tidak mengeluarkan biaya koneksi, bukan sekadar menyembunyikan koneksi yang sebenarnya sudah dibuka.
Dua pertanyaan yang keduanya harus menjawab ya
Panel yang menginginkan realtime notification tidak sama dengan aplikasi yang memiliki mekanisme untuk mengirimkannya. SharePanelData memeriksa kedua hal tersebut:
if ($panel === null || ! $panel->hasBroadcasting() || ! BroadcastSupport::isConfigured()) {
return ['enabled' => false, 'channel' => null];
}2
3
PandaPanel\Support\BroadcastSupport::isConfigured(): bool menjawab pertanyaan kedua. Tiga kondisi harus terpenuhi:
| Pemeriksaan | Gagal ketika |
|---|---|
| Default connection memiliki nama | broadcasting.default tidak ada atau kosong — Laravel baru tanpa config/broadcasting.php |
| Connection tersebut ada dan memiliki driver | broadcasting.connections.{default}.driver tidak ada — typo, bukan broadcaster |
| Driver dapat mencapai browser | drivernya null atau log — keduanya driver valid, tetapi tidak dapat disubscribe oleh Echo |
use PandaPanel\Support\BroadcastSupport;
BroadcastSupport::isConfigured(); // bool2
3
Credential sengaja tidak diperiksa karena hanya broadcaster yang dapat memastikan credential tersebut benar. Panel yang menolak membuka koneksi hanya karena format key terlihat mencurigakan justru akan menghasilkan failure mode yang lebih buruk.
Failure yang ingin dicegah di sini cukup penting untuk dijelaskan: server mengirim channel, client memanggil echo(), lalu @laravel/echo-vue melempar "Echo has not been configured" dari dalam onMounted. Mount yang terputus kemudian menghasilkan banyak warning Slot "default" invoked outside of the render function ketika Inertia mengganti layout yang belum selesai mount. Tidak ada satu pun pesan di urutan itu yang menyebut broadcaster sebagai akar masalah.
Shared prop
'broadcasting' => ['enabled' => true, 'channel' => 'App.Models.User.7'],| Field | Type | Nilai saat nonaktif |
|---|---|---|
enabled | bool | false |
channel | string|null | null |
Menggunakan null dan bukan string kosong memberi frontend kondisi yang tegas: tidak ada channel yang harus disubscribe, bukan channel kosong yang nantinya ditolak.
Nilai ini berada pada shared props, bukan Panel::toSharedArray(). Jawabannya bergantung pada siapa yang sedang melakukan request, sehingga tempat yang tepat adalah request context, bukan definisi Panel yang dapat di-cache client.
import { usePanel } from '@/panel/composables/usePanel';
const { broadcasting } = usePanel();
broadcasting.value.enabled; // boolean
broadcasting.value.channel; // string | null2
3
4
5
6
Subscription di client
resources/js/panel/composables/usePanelBroadcasting.ts, dipanggil satu kali di PanelLayout.vue:
import { usePanelBroadcasting } from '@/panel/composables/usePanelBroadcasting';
usePanelBroadcasting();2
3
const EVENT = '.panel.notification'; // the dot marks a custom broadcastAs name
client.private(channel).listen(EVENT, (payload: unknown) => { /* … */ });2
3
Yang dilakukan, secara berurutan:
- Langsung berhenti ketika
broadcasting.channelbernilainull. Inilah yang membuat Panel dengan broadcasting nonaktif benar-benar tidak mengeluarkan biaya subscription. - Melakukan narrowing terhadap payload. Payload websocket telah melewati boundary yang sama seperti HTTP response dan harus divalidasi dengan prinsip yang sama:
messagedantypeharus berupa string, dantypeharus salah satu darisuccess,info,warning, atauerror. Nilai lain diabaikan. - Memicu event
windowbernamapanel:notificationketikapayload.persistent === true— dilakukan sebelum toast agar counter pada bell sudah benar walaupun toast langsung ditutup. - Menampilkan toast melalui
vue-sonner, termasuk tombol action jika payload membawaurl. - Meninggalkan channel saat unmount, tetapi hanya jika subscription memang sempat dibuat.
Jika configureEcho() belum pernah dipanggil di browser, echo() akan melempar exception. Composable menangkapnya, memberi warning satu kali di development, lalu Panel tetap berjalan tanpa realtime notification. Fiturnya gagal; screennya tidak ikut gagal.
Queueing
Kedua event mengimplementasikan ShouldBroadcast, bukan ShouldBroadcastNow. Laravel mendorong job BroadcastEvent ke default queue connection, sehingga worker harus berjalan agar toast dapat sampai. Dengan QUEUE_CONNECTION=sync, proses berlangsung inline; itulah mengapa fitur ini tampak bekerja di local development tanpa worker. Lihat Queued notifications.
Hal yang perlu diperhatikan
dispatch()pada eventShouldBroadcastbukan berarti pesan sudah terkirim. Itu berarti sebuah queued job dibuat. Worker yang tidak berjalan dari sisi browser terlihat hampir sama dengan broadcaster yang belum dikonfigurasi.- Nama event membutuhkan titik di depan.
broadcastAs()mengembalikanpanel.notification; Echo membutuhkan.panel.notificationagar namespace prefixing dilewati. Composable bawaan sudah melakukannya — ini hanya perlu diperhatikan jika Anda membuat subscription sendiri. - Guest tidak mendapatkan channel.
getBroadcastChannel(null)menghasilkannull, danSharePanelDatabahkan tidak akan mencoba melewati tahap tersebut. - Channel adalah milik user, bukan milik Panel. Dua Panel yang dibuka pada dua tab akan subscribe ke channel yang sama dan keduanya menampilkan toast. Ini disengaja: notifikasi ditujukan kepada orangnya, bukan screennya.
channelFor()memakaigetAuthIdentifier(). Custom user model dengan key non-integer tetap bekerja selama callback diroutes/channels.phpmembandingkan identifier dengan cara yang sama.
Lihat juga
- Setup Reverb dan Echo — membuat broadcaster tersedia
- Authorization channel — rule yang membuat private channel aman
- Toast notifications — hasil akhir sebuah broadcast
- Notification center — bell yang di-refresh oleh
panel:notification - Queued notifications
- Metadata server ke Vue
- Testing notifications