Listener adalah pasangan kerja dari event: satu class dengan satu tugas, bereaksi atas satu fakta. Karena bentuknya class biasa, listener bisa punya dependensi sendiri (mailer, repository, API client) yang di-inject lewat constructor tanpa mengotori pengirim event. Pada chapter ini kita membuat listener dengan artisan, mengisi handle() dengan logika reaksi, mendaftarkannya secara manual di AppServiceProvider, lalu membuktikan alurnya bekerja dari dispatch sampai efek log yang tercatat.
Membuat Listener dengan Artisan #
Perintah make:listener menerima opsi --event agar kerangka yang dihasilkan langsung type-hint event yang dituju:
php artisan make:listener SendOrderConfirmation --event=OrderPlacedOutput yang Dihasilkan:
INFO Listener [app/Listeners/SendOrderConfirmation.php] created successfully.Berkas app/Listeners/SendOrderConfirmation.php berisi kerangka berikut:
<?php
namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendOrderConfirmation
{
public function __construct()
{
//
}
public function handle(OrderPlaced $event): void
{
//
}
}Dua hal penting ada di sini. Pertama, parameter handle(OrderPlaced $event) adalah kontrak yang mengikat listener pada event — type hint inilah yang nantinya dipakai Laravel untuk mencocokkan keduanya. Kedua, import ShouldQueue dan InteractsWithQueue sengaja disiapkan tapi belum dipakai; kita aktifkan pada chapter Queued Listeners
. Konstruktor yang kosong boleh dihapus, atau bisa kita isi dengan dependensi lewat service container.
Mengisi Method handle #
Kita lengkapi listener agar mencatat setiap konfirmasi ke log — efek yang mudah kita buktikan tanpa server SMTP:
<?php
namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Log;
class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
Log::info('konfirmasi-dikirim', [
'orderId' => $event->orderId,
'email' => $event->email,
]);
}
}Method handle() menerima event lewat type hint, sehingga properti seperti $event->orderId langsung tersedia. Pada aplikasi nyata isi method ini biasanya Mail::to($event->email)->send(...) — dengan MAIL_MAILER=log, email pun tertulis ke berkas log dan tetap bisa kita periksa. Contoh kita memakai Log::info() langsung agar efeknya lebih mudah dibuktikan tanpa keterlibatan mailer sama sekali.
Registrasi Manual di AppServiceProvider #
Sebelum event dan listener saling mengenal, keduanya harus didaftarkan ke dispatcher. Struktur Laravel 13 tidak lagi menyediakan EventServiceProvider terpisah, sehingga tempat registrasi yang alami adalah method boot() di app/Providers/AppServiceProvider.php:
use App\Events\OrderPlaced;
use App\Listeners\SendOrderConfirmation;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(OrderPlaced::class, SendOrderConfirmation::class);
}Baris di atas memberi tahu dispatcher: setiap OrderPlaced yang lewat harus diteruskan ke SendOrderConfirmation. Laravel akan meng-instantiate listener lewat service container (sehingga dependensi constructor terpenuhi) lalu memanggil method handle()-nya.
Bukti Ujung ke Ujung #
Mari kita buktikan alurnya dengan mengirim satu event lalu membaca isi storage/logs/laravel.log:
use App\Events\OrderPlaced;
OrderPlaced::dispatch('INV-401', '[email protected]', 50000);Output yang Dihasilkan:
[2026-10-13 20:53:19] local.INFO: konfirmasi-dikirim {"orderId":"INV-401","email":"[email protected]"}
[2026-10-13 20:53:19] local.INFO: konfirmasi-dikirim {"orderId":"INV-401","email":"[email protected]"}Rencananya kita berharap satu baris, tetapi log berisi dua baris identik. Ada dua registrasi yang sama-sama aktif: satu dari Event::listen yang barusan kita tulis, dan satu lagi dari event discovery — mekanisme bawaan Laravel 13 yang otomatis mendaftarkan setiap class di app/Listeners berdasarkan type hint pada handle(). Karena SendOrderConfirmation sudah type-hint OrderPlaced, Laravel mendaftarkannya sendiri tanpa menunggu perintah kita.
Menghentikan Propagasi dengan return false #
Kadang satu listener perlu memutuskan bahwa listener lain tidak perlu lagi berjalan — misalnya modul logistik menahan pesanan bermasalah sehingga persiapan gudang batal. Laravel mendukungnya dengan satu aturan sederhana: listener yang mengembalikan false akan menghentikan propagasi ke listener berikutnya:
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Event;
$jalur = [];
Event::listen(OrderPlaced::class, function () use (&$jalur) {
$jalur[] = 'logistik: menahan pengiriman (return false)';
return false;
});
Event::listen(OrderPlaced::class, function () use (&$jalur) {
$jalur[] = 'gudang: menyiapkan paket';
});
OrderPlaced::dispatch('INV-201', '[email protected]', 60000);
echo "yang_terpanggil:\n";
foreach ($jalur as $j) {
echo ' - ', $j, "\n";
}Output yang Dihasilkan:
yang_terpanggil:
- logistik: menahan pengiriman (return false)Listener kedua tidak pernah terpanggil karena listener pertama mengembalikan false. Gunakan kekuasaan ini hemat: listener yang saling membungkam membuat urutan eksekusi sulit dinalar, dan hasilnya berbeda bila listener berjalan lewat queue.
Selain menghentikan, ada juga mode tanya: Event::until() menjalankan listener satu per satu sampai salah satu mengembalikan nilai, lalu mengembalikan nilai itu ke pemanggil:
Event::listen(OrderShipped::class, function () {
return 'resi: JNE-123456';
});
Event::listen(OrderShipped::class, function () {
return 'resi-lain';
});
echo 'until: ', Event::until(new OrderShipped('INV-201', 'JNE Express')), "\n";Output yang Dihasilkan:
until: resi: JNE-123456Listener kedua yang juga mengembalikan nilai tidak pernah dieksekusi — until() berhenti di respons pertama yang tidak null. Mode ini berguna ketika event dipakai layaknya pertanyaan (“apa nomor resinya?”) alih-alih pengumuman.
Registrasi manual memberi kendali penuh, tetapi seperti yang baru kita saksikan, Laravel 13 sebenarnya sudah mendaftarkan listener ini tanpa satu baris konfigurasi pun. Cara kerja event discovery itu, cara mengujinya, dan kapan memilih jalur manual akan kita bahas tuntas pada chapter berikutnya.