Notifikasi adalah satu objek pesan yang menentukan sendiri kanal-kanal pengirimannya, mulai dari penyimpanan di database sampai surel. Perbedaannya dengan mailable terletak pada cakupan pesan. Mailable membungkus satu pesan surel, sedangkan satu kelas notifikasi dapat memuat versi pesan untuk beberapa kanal sekaligus, sehingga logika tentang apa yang diberitahukan cukup ditulis satu kali. Topik ini membahas pembuatan kelas notifikasi pada aplikasi catatan, pengirimannya ke kanal database dan mail, penandaan notifikasi sebagai sudah dibaca, kanal Slack yang disediakan paket terpisah, serta pengujian pengirimannya dengan Notification::fake().
Notifikasi dan kanal pengiriman#
Kanal yang digunakan sebuah notifikasi ditentukan oleh metode via(), dan setiap kanal membaca isinya dari metode khusus pada kelas yang sama, misalnya toMail() untuk surel dan toArray() untuk kanal database. Dengan pembagian ini, perubahan isi pesan cukup dilakukan di satu tempat, sedangkan kanal yang dituju dapat ditambah atau dikurangi tanpa menyentuh kode pengirimannya. Tabel berikut merangkum kanal bawaan beserta perilakunya.
| Kanal | Perilaku |
|---|---|
database | Menyimpan pesan pada tabel notifications untuk ditampilkan di dalam aplikasi |
mail | Mengirim surel melalui mailer yang diatur pada config/mail.php |
broadcast | Menyiarkan pesan ke frontend melalui layanan broadcasting |
slack | Mengirim pesan ke Slack melalui paket laravel/slack-notification-channel |
Penerima notifikasi adalah model apa pun yang menggunakan trait Notifiable, yaitu trait PHP yang menyediakan metode notify() beserta kumpulan metode pembaca notifikasi. Model User bawaan Laravel sudah menggunakan trait ini, sehingga notifikasi dapat dikirim langsung ke pengguna. Kanal database dan mail digunakan pada topik ini karena keduanya tersedia tanpa pemasangan apa pun, sedangkan kanal Slack dibahas terpisah pada bagian akhir karena disediakan paket lain.
Membuat kelas notifikasi#
Kelas notifikasi dibuat dengan perintah make:notification, dan kanal database memerlukan satu langkah persiapan tambahan karena tabel notifications tidak termasuk migration bawaan proyek. Tabel tersebut dibuat dengan perintah make:notifications-table, kemudian migration-nya dijalankan.
# tidak-dijalankan
# membuat migration tabel notifications lalu menjalankannya
cd ~/aplikasi-catatan
php artisan make:notifications-table
php artisan migrate --forcePerintah di atas tidak dieksekusi ulang pada contoh ini karena migration-nya sudah dibuat dan dijalankan. Keluaran asli kedua perintah itu adalah sebagai berikut.
INFO Migration created successfully.
INFO Running migrations.
2026_11_03_085521_create_notifications_table ................... 1.76ms DONEDefinisi tabel tersebut memuat kolom id bertipe UUID, kolom type, pasangan kolom notifiable_type dan notifiable_id untuk menghubungkan setiap notifikasi dengan pemiliknya, kolom data bertipe teks, serta kolom read_at yang bernilai NULL selama notifikasi belum dibaca. Selanjutnya kelas notifikasinya dibuat dengan make:notification.
# tidak-dijalankan
# membuat kelas notifikasi
cd ~/aplikasi-catatan
php artisan make:notification PengingatCatatanKeluaran asli perintah tersebut menunjukkan berkas yang dihasilkannya.
INFO Notification [app/Notifications/PengingatCatatan.php] created successfully.Kelas hasil perintah tersebut disunting agar menerima satu model Note, memilih dua kanal pada via(), dan menyediakan satu metode isi untuk masing-masing kanal.
// tidak-dijalankan
<?php
namespace App\Notifications;
use App\Models\Note;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class PengingatCatatan extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(public Note $catatan)
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Pengingat arsip catatan')
->greeting('Halo!')
->line("Catatan \"{$this->catatan->judul}\" masuk daftar pengingat arsip.")
->action('Buka catatan', url('/catatan/'.$this->catatan->id))
->line('Terima kasih telah menggunakan aplikasi catatan.');
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'judul' => $this->catatan->judul,
'pesan' => 'Catatan ini masuk daftar pengingat arsip.',
];
}
}Konstruktor menerima model Note yang diberitahukan. Metode via() mengembalikan ['mail', 'database'], sehingga satu pemanggilan notify() menghasilkan satu surel dan satu baris tabel sekaligus. Metode toMail() menyusun pesan dengan MailMessage, yaitu builder yang merangkai subjek, sapaan, baris-baris isi, dan tombol tautan tanpa templat Blade, lalu pesan yang sama dirender menjadi versi teks polos sebagaimana templat markdown pada mailable. Metode toArray() digunakan oleh kanal database, dan apabila metode toDatabase() tersedia, metode itulah yang diutamakan dengan toArray() sebagai cadangannya.
Mengirim ke kanal database dan mail#
Notifikasi dikirim dengan memanggil notify() pada model penerima. Contoh berikut menyiapkan datanya sendiri agar dapat diulang kapan pun, yaitu pengguna Ali beserta satu catatan pada kategori Kerja, kemudian mengirim PengingatCatatan kepadanya. Log mailer dikosongkan lebih dahulu agar baris yang diperiksa pasti milik pengiriman ini.
# menyiapkan data uji lalu mengirim notifikasi ke dua kanal sekaligus
cd ~/aplikasi-catatan
php artisan migrate --force
: > storage/logs/laravel.log
php artisan tinker --execute='
use App\Models\Category;
use App\Models\Note;
use App\Models\User;
use App\Notifications\PengingatCatatan;
$pengguna = User::firstOrCreate(
["email" => "[email protected]"],
["name" => "Ali", "password" => "rahasia-ali"]
);
$kerja = Category::firstOrCreate(["nama" => "Kerja"]);
$catatan = Note::firstOrCreate(
["judul" => "Pengingat arsip bulanan"],
["isi" => "Menandai catatan yang perlu diarsipkan pada akhir bulan.", "category_id" => $kerja->id]
);
$pengguna->notifications()->delete();
$pengguna->notify(new PengingatCatatan($catatan));
$baru = $pengguna->notifications()->first();
echo "Notifikasi tersimpan: " . $pengguna->notifications()->count() . " baris." . PHP_EOL;
echo "Tipe: " . $baru->type . PHP_EOL;
echo "Data: " . json_encode($baru->data, JSON_UNESCAPED_UNICODE) . PHP_EOL;
echo "Belum dibaca: " . $pengguna->unreadNotifications()->count() . " notifikasi." . PHP_EOL;
'
grep -oE "(From|To|Subject): .*" storage/logs/laravel.log | head -3
grep -E "^Catatan " storage/logs/laravel.log | head -1 INFO Nothing to migrate.
Notifikasi tersimpan: 1 baris.
Tipe: App\Notifications\PengingatCatatan
Data: {"judul":"Pengingat arsip bulanan","pesan":"Catatan ini masuk daftar pengingat arsip."}
Belum dibaca: 1 notifikasi.
From: Laravel <[email protected]>
To: [email protected]
Subject: Pengingat arsip catatan
Catatan "Pengingat arsip bulanan" masuk daftar pengingat arsip.Baris Nothing to migrate. menunjukkan migration sudah dijalankan sebelumnya, sehingga perintah itu berhenti tanpa perubahan. Notifikasi tersimpan sebagai satu baris pada tabel notifications, dengan kolom type berisi nama kelas lengkapnya dan kolom data berisi hasil toArray() yang dikodekan menjadi JSON. Kolom read_at masih NULL, sehingga unreadNotifications() menghitung satu notifikasi. Tiga baris header berasal dari kanal mail yang menulis pesan ke log mailer, sedangkan baris terakhir memperlihatkan isi line() pada versi teks polos surelnya.
Ketika penerima sudah membaca notifikasinya, metode markAsRead() mengisi kolom read_at dengan waktu saat itu. Bentuk tanpa kurung seperti unreadNotifications mengembalikan koleksi notifikasi, sedangkan bentuk dengan kurung seperti unreadNotifications() mengembalikan relasi yang dapat dihitung, dan keduanya digunakan bergantian pada contoh berikut.
# menandai seluruh notifikasi belum dibaca sebagai sudah dibaca
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\User;
$pengguna = User::where("email", "[email protected]")->firstOrFail();
$pengguna->unreadNotifications->markAsRead();
echo "Belum dibaca: " . $pengguna->unreadNotifications()->count() . " notifikasi." . PHP_EOL;
echo "Sudah dibaca: " . $pengguna->readNotifications()->count() . " notifikasi." . PHP_EOL;
'Belum dibaca: 0 notifikasi.
Sudah dibaca: 1 notifikasi.Kanal Slack#
Kanal Slack tidak termasuk bagian kerangka kerja, melainkan disediakan paket laravel/slack-notification-channel yang diinstal terpisah. Pengirimannya memerlukan webhook Slack, yaitu URL unik milik ruang kerja yang menerima pesan melalui HTTP. URL tersebut disimpan pada berkas .env, kemudian kelas notifikasi menyediakan metode toSlack() yang mengembalikan objek SlackMessage.
# tidak-dijalankan
# menginstal paket kanal Slack, lalu menyetel URL webhook pada berkas .env
cd ~/aplikasi-catatan
composer require laravel/slack-notification-channel
# pada berkas .env: SLACK_NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/xxx// tidak-dijalankan
use Illuminate\Notifications\Messages\SlackMessage;
public function toSlack(object $notifiable): SlackMessage
{
return (new SlackMessage)
->text("Pengingat arsip: {$this->catatan->judul}");
}Tujuan pengiriman kanal slack dicari melalui metode routeNotificationFor('slack') pada model penerima, dan untuk penerima tanpa model, notifikasi dapat dikirim ke alamat tertentu dengan Notification::route('slack', $url). Kedua bentuk tersebut mengirim pesan langsung ke layanan Slack melalui jaringan, sehingga keduanya tidak dieksekusi pada contoh ini.
Pengujian dengan Notification::fake#
Pengujian tidak mengirim surel maupun menulis baris tabel, melainkan memalsukan pengirimnya dengan Notification::fake(). Setelah pemanggilan ini, seluruh notifikasi ditangkap di memori, lalu asersi memeriksa notifikasi mana yang terkirim, kepada siapa, dan melalui kanal apa.
// tidak-dijalankan
<?php
namespace Tests\Feature;
use App\Models\Note;
use App\Models\User;
use App\Notifications\PengingatCatatan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class PengingatCatatanTest extends TestCase
{
use RefreshDatabase;
public function test_notifikasi_pengingat_terkirim_ke_dua_kanal(): void
{
Notification::fake();
$pengguna = User::factory()->create();
$catatan = Note::factory()->create();
$pengguna->notify(new PengingatCatatan($catatan));
Notification::assertSentTo($pengguna, PengingatCatatan::class, function (PengingatCatatan $notifikasi, array $kanal) {
return in_array('database', $kanal) && in_array('mail', $kanal);
});
}
}Fungsi pemeriksa pada assertSentTo() menerima dua argumen, yaitu notifikasi yang tertangkap dan larik kanal hasil via(), sehingga pengujian dapat memastikan kedua kanal disertakan. Asersi berlawanannya juga tersedia, yaitu assertNotSentTo() untuk notifikasi tertentu yang tidak boleh terkirim dan assertNothingSent() untuk memastikan tidak ada notifikasi sama sekali.
# menjalankan pengujian fitur notifikasi
cd ~/aplikasi-catatan
php artisan test --filter=PengingatCatatanTest | grep -E "PASS|Tests:" PASS Tests\Feature\PengingatCatatanTest
Tests: 1 passed (1 assertions)Keluaran tersebut memperlihatkan satu pengujian lulus, yaitu notifikasi pengingat terkirim kepada pengguna melalui kanal database dan mail sekaligus. Karena pengirimnya dipalsukan, pengujian berjalan cepat tanpa jaringan dan tidak meninggalkan baris baru pada tabel notifications.
Kesalahan umum#
Kesalahan pertama adalah mengirim notifikasi dengan kanal database ketika tabelnya belum ada. Pesan errornya memuat no such table: notifications, dan perbaikannya adalah menjalankan php artisan make:notifications-table kemudian php artisan migrate sebelum notifikasi pertama dikirim. Tabel ini juga wajib ada pada lingkungan pengujian yang menggunakan RefreshDatabase, karena migration-nya ikut dijalankan di sana.
Kesalahan kedua adalah memanggil notify() pada model yang tidak menggunakan trait Notifiable. Pesan errornya berupa Call to undefined method, dan perbaikannya adalah menambahkan use Illuminate\Notifications\Notifiable; beserta use Notifiable; pada kelas modelnya. Model User bawaan sudah memuat trait ini, sedangkan model lain pada aplikasi, misalnya Category, tidak memilikinya secara otomatis.
Kesalahan ketiga adalah menempatkan objek besar pada toArray(). Hasil metode ini dikodekan menjadi JSON dan disimpan utuh pada kolom data, sehingga menyertakan seluruh atribut model membuat baris notifikasi berukuran besar dan sulit digunakan oleh antarmuka. Kirim hanya data ringkas yang memang ditampilkan, misalnya judul dan identifier catatan, lalu muat ulang model dari database ketika detailnya dibutuhkan.
Ringkasan#
- Satu kelas notifikasi melayani banyak kanal, dengan daftar kanal pada
via()dan satu metode isi per kanal, yaitutoMail()untuk surel dantoArray()untuk kanal database. - Notifikasi dikirim dengan
notify()pada model yang memuat traitNotifiable, dan kanal database menyimpannya pada tabelnotificationsdalam bentuk JSON beserta kolomread_at. markAsRead()menandai notifikasi sebagai sudah dibaca, sedangkanunreadNotifications()danreadNotifications()memilah keduanya.- Kanal Slack disediakan paket
laravel/slack-notification-channel, dan pengujian pengiriman menggunakanNotification::fake()besertaassertSentTo().
Referensi#
- Notifications — Laravel
— Dokumentasi resmi kelas notifikasi, kanal database, mail, dan Slack, beserta pengujian dengan
Notification::fake(). - Mail — Laravel — Dokumentasi mailable dan mailer yang menjadi dasar kerja kanal mail pada notifikasi.