Notifikasi yang mengirim email butuh waktu — merender template HTML tiga belas ribu byte, menyusun MIME, menunggu SMTP menjawab. Jika semua itu terjadi di dalam request user yang menekan tombol “bayar”, response terasa lambat padahal user hanya butuh konfirmasi. Solusinya sudah kita kenal dari pembahasan queue: pindahkan pekerjaan ke antrean. Kabar baiknya, notifikasi terintegrasi dengan antrean lewat satu interface — tambahkan implements ShouldQueue dan pengiriman berubah dari sinkron ke async tanpa mengubah satu baris pemanggilan. Pada chapter ini kita bedah mekanismenya, properti kontrol per-notifikasi, dan cara mengujinya.
Interface ShouldQueue #
Kita buat notifikasi kegagalan pembayaran dengan antrean aktif:
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class PaymentFailed extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 3;
public $timeout = 30;
public function __construct(
public string $orderId,
public string $alasan,
) {}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Pembayaran Pesanan #'.$this->orderId.' Gagal')
->greeting('Halo '.($notifiable->name ?? 'Pelanggan').'!')
->line($this->alasan)
->error()
->action('Perbarui Metode Pembayaran', url('/kartu'))
->line('Pesanan Anda kami tahan selama 24 jam.');
}
}Bedah elemen antreannya:
implements ShouldQueue— penanda bagiNotificationSenderbahwa pengiriman harus dibungkus jobSendQueuedNotificationsdan didorong ke antrean, bukan dijalankan langsung.use Queueable— trait yang membawa properti antrean ($queue,$connection,$delay,$afterCommit,$tries,$backoff,$timeout,$middleware) plus method perantai sepertionQueue().$tries = 3— job boleh mencoba ulang sampai tiga kali sebelum dinyatakan gagal permanen.$timeout = 30— worker membatasi eksekusi 30 detik agar job menggantung tidak menyandera worker.
Trait Queueable di framework mendeklarasikan properti tanpa type. Jika kita menimpanya di class anak, tipe harus cocok persis — cara aman menyetel nilai adalah lewat method onQueue() di constructor, bukan mendeklarasikan ulang properti:
public function __construct(
public string $produk,
public int $sisa,
) {
$this->onQueue('notifikasi');
}Bukti di Tabel jobs #
Sandbox memakai QUEUE_CONNECTION=database, jadi antrean hidup di tabel jobs. Runner berikut mengirim notifikasi dan membedah baris job yang tercipta:
use App\Models\User;
use App\Notifications\LowStockAlert;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\DB;
DB::table('jobs')->delete();
$user = User::where('email', '[email protected]')->firstOrFail();
Notification::send([$user], new LowStockAlert('Buku Laravel', 3));
$job = DB::table('jobs')->first();
echo 'nama_queue: ' . $job->queue . "\n";
echo 'displayName: ' . json_decode($job->payload)->displayName . "\n";Output yang Dihasilkan:
nama_queue: notifikasi
displayName: App\Notifications\LowStockAlertKolom queue berisi notifikasi — bukti onQueue('notifikasi') di constructor sampai ke job. Payload displayName memuat nama class notifikasi (di sini LowStockAlert yang meng-queue PaymentFailed), dan isi payload lengkapnya berupa notifikasi terserialisasi plus daftar penerima. Worker memproses job ini dengan php artisan queue:work --queue=notifikasi,default agar antrean bernama ikut diawasi.
Memproses antrean membuktikan email benar-benar terkirim:
use Illuminate\Support\Facades\Artisan;
Notification::send([$user], new PaymentFailed('321', 'Limit kartu terlampaui.'));
echo 'jobs_sebelum: ' . DB::table('jobs')->count() . "\n";
Artisan::call('queue:work', ['--stop-when-empty' => true, '--max-time' => 20]);
echo 'jobs_sesudah: ' . DB::table('jobs')->count() . "\n";
$log = file_get_contents($dir.'/storage/logs/laravel.log');
echo 'subject_tersimpan: ' . var_export(
str_contains($log, 'Pembayaran Pesanan #321 Gagal'), true) . "\n";Output yang Dihasilkan:
jobs_sebelum: 2
jobs_sesudah: 1
subject_tersimpan: trueJob yang tersisa satu adalah notifikasi berdelay 5 menit dari subbab berikutnya — --stop-when-empty memproses job yang jatuh temponya saja. Setelah queue:work selesai, berkas log memuat subject email Pembayaran Pesanan #321 Gagal — pengiriman async tuntas tanpa request user menunggu.
Delay dan afterCommit #
Pengiriman bisa dijadwalkan ke depan. Method delay() menerima DateTimeInterface atau jumlah detik:
Notification::send([$user], (new PaymentFailed('789', 'Kartu kedaluwarsa.'))
->delay(now()->addMinutes(5)));
$job = DB::table('jobs')->first();
echo 'available_at_minus_now: ' . ($job->available_at - time()) . " detik dari sekarang\n";Output yang Dihasilkan:
available_at_minus_now: 300 detik dari sekarangKolom available_at job terisi lima menit ke depan — worker baru memprosesnya setelah waktu itu. Untuk kasus transaksi database, properti afterCommit menjaga job tidak dieksekusi sebelum data benar-benar tersimpan. Runner berikut membandingkan notifikasi afterCommit = true dengan yang biasa di dalam transaksi:
class AfterCommitNotification extends PaymentFailed
{
public $afterCommit = true;
}
DB::beginTransaction();
Notification::send([$user], new AfterCommitNotification('AC-2', 'x'));
Notification::send([$user], new BiasaNotification('BIAS-2', 'x'));
$sebelum = DB::table('jobs')->count();
DB::commit();
$sesudah = DB::table('jobs')->count();
echo 'jobs_sebelum_commit: ' . $sebelum . "\n";
echo 'jobs_sesudah_commit: ' . $sesudah . "\n";Output yang Dihasilkan:
jobs_sebelum_commit: 1
jobs_sesudah_commit: 2Notifikasi biasa langsung masuk antrean meski transaksi belum commit; notifikasi afterCommit menunggu DB::commit() dulu. Perilaku ini penting bila worker berjalan sangat cepat: tanpa afterCommit, worker bisa memproses notifikasi untuk data yang belum terlihat — atau bila transaksi di-rollback, notifikasi untuk data yang tidak pernah ada.
Menguji Notifikasi Berantrean #
Notification::fake() tetap bekerja untuk notifikasi berantrean — fake mencatat notifikasi seolah terkirim sehingga closure inspeksi bisa memeriksa antarmuka dan propertinya:
use App\Models\User;
use App\Notifications\PaymentFailed;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
public function test_notifikasi_queued_tidak_dikirim_sebelum_worker_jalan(): void
{
Notification::fake();
$user = User::factory()->create([
'name' => 'Budi',
'email' => '[email protected]',
]);
$user->notify(new PaymentFailed('123', 'Kartu ditolak bank.'));
Notification::assertSentTo($user, PaymentFailed::class, function ($n) {
fwrite(STDERR, 'implements_should_queue: '
. var_export($n instanceof ShouldQueue, true) . "\n");
fwrite(STDERR, 'tries: ' . $n->tries . ' timeout: ' . $n->timeout . "\n");
return true;
});
fwrite(STDERR, "queued_notification_test: lulus\n");
}Output yang Dihasilkan:
implements_should_queue: true
tries: 3 timeout: 30
queued_notification_test: lulus
PASS Tests\Feature\QueuedNotificationTest
✓ notifikasi queued tidak dikirim sebelum worker jalan 0.14s
Tests: 1 passed (1 assertions)Untuk memeriksa nama antrean secara presisi, Queue::fake() menawarkan assertPushedOn — job SendQueuedNotifications yang membungkus notifikasi bisa diperiksa queue-nya:
Queue::fake();
$user->notify(new LowStockAlert('Buku Laravel', 3));
Queue::assertPushedOn('notifikasi', SendQueuedNotifications::class);
Queue::assertPushed(SendQueuedNotifications::class, 1);Output yang Dihasilkan:
queue_channel_test: lulus (job di antrean 'notifikasi')
PASS Tests\Feature\QueueChannelTest
✓ notifikasi masuk queue tertentu 0.15s
Tests: 1 passed (2 assertions)Kombinasi dua fake ini menutup celah klasik pengujian antrean: Notification::fake menjawab “apakah notifikasi dibuat”, Queue::fake menjawab “apakah notifikasi benar-benar masuk antrean yang benar”.
Dengan pengiriman async beres, tinggal satu pilar terakhir section ini: strategi pengujian menyeluruh untuk email dan notifikasi — fake yang tepat, assertion yang presisi, dan alasan mengapa pendekatan ini mengalahkan log mailer di ranah test. Semuanya dibahas di chapter Pengujian Email . Dokumentasi antrean notifikasi lengkap ada di dokumentasi Notifications .