Chapter Model Events
pada section Eloquent ORM sudah memperkenalkan event siklus hidup model lewat hook booted() dan class Observer
yang memusatkan hook-hook tersebut. Namun semua itu sebenarnya berdiri di atas sistem yang sedang kita pelajari: setiap kali model disimpan, diubah, atau dihapus, Eloquent menembakkan event melalui dispatcher yang sama dengan OrderPlaced dan kawan-kawannya. Pada chapter ini kita memasuki sisi itu — mendengarkan event model lewat Event::listen() — mulai dari nama eventnya yang berbentuk string, penyaringan atribut yang berubah, hingga jembatannya menuju queue.
Nama Event Eloquent di Balik Layar #
Ketika kita menulis Event::listen(OrderPlaced::class, ...), nama eventnya adalah nama class. Eloquent memakai konvensi yang berbeda: nama string dengan format eloquent.<momen>: <class model> — misalnya eloquent.updated: App\Models\Post menyala tepat setelah baris Post selesai di-update. Bentuk string ini membuat kita bisa memasang listener langsung pada momen dan model tertentu tanpa membuat event class:
use App\Models\Post;
use Illuminate\Support\Facades\Event;
$post = Post::create([
'user_id' => $budi->id,
'judul' => 'Belajar Event Model',
'slug' => 'belajar-event-model',
'isi' => 'Isi tulisan pertama.',
'views' => 0,
]);
Event::listen('eloquent.updated: App\Models\Post', function (Post $post) {
echo "event: eloquent.updated: App\\Models\\Post\n";
echo 'views_lama: ', $post->getOriginal('views'), "\n";
echo 'views_baru: ', $post->views, "\n";
echo 'wasChanged(judul): ', var_export($post->wasChanged('judul'), true), "\n";
echo 'getChanges: ', json_encode($post->getChanges()), "\n";
});
$post->update(['views' => 10, 'judul' => 'Belajar Event Model (Revisi)']);Output yang Dihasilkan:
event: eloquent.updated: App\Models\Post
views_lama: 0
views_baru: 10
wasChanged(judul): true
getChanges: {"judul":"Belajar Event Model (Revisi)","views":10}Listener hanya terpanggil untuk operasi update pada model Post — event eloquent.updated milik model lain tidak menyentuhnya. Pada momen event updated menyala, instance model sudah memuat data baru, tetapi jejak masa lalunya masih tersedia: getOriginal('views') mengembalikan nilai dari database sebelum update (0), siap dibandingkan dengan nilai baru $post->views (10). Dua alat bantu pelengkap merinci perubahannya — getChanges() mencatat kolom-kolom yang benar-benar berubah di query UPDATE terakhir, sedangkan wasChanged('judul') menjawab ya-tidak untuk satu kolom. Kombinasi ketiganya adalah bahan baku log audit yang lengkap.
Wildcard untuk Audit Lintas Momen #
Nama model dalam event juga bisa kita biarkan kosong dengan wildcard eloquent.*. Listener wildcard menerima dua argumen: nama event dan array payload. Dengan menyaring keduanya, satu listener bisa membangun jejak audit lengkap khusus model Post:
$audit = [];
Event::listen('eloquent.*', function (string $namaEvent, array $data) use (&$audit) {
if (! str_ends_with($namaEvent, 'App\\Models\\Post')) {
return; // bukan event milik model Post
}
$momen = str_replace('eloquent.', '', strstr($namaEvent, ':', true) ?: $namaEvent);
if (! in_array($momen, ['created', 'updated', 'deleted'])) {
return; // abaikan booting, saving, dsb.
}
$model = $data[0];
$audit[] = [
'momen' => $momen,
'id' => $model->getKey(),
'perubahan' => $momen === 'updated' ? array_keys($model->getChanges()) : [],
];
});
$post = Post::create([
'user_id' => $siti->id,
'judul' => 'Menulis dengan Event',
'slug' => 'menulis-dengan-event',
'isi' => 'Isi tulisan kedua.',
]);
$post->update(['views' => 5]);
$post->delete();
foreach ($audit as $baris) {
printf("%-8s id=%d %s\n", $baris['momen'], $baris['id'],
$baris['perubahan'] ? 'kolom: '.implode(',', $baris['perubahan']) : '-');
}Output yang Dihasilkan:
created id=3 -
updated id=3 kolom: views
deleted id=3 -Wildcard menangkap semua event Eloquent — termasuk momen-momen teknis seperti saving dan booting — sehingga listener harus menyaring sendiri yang relevan. Pola ini tidak menggantikan observer; ia unggul ketika penyaringannya lintas model atau tidak diketahui di awal, misalnya membangun alat audit umum yang mencatat created, updated, dan deleted untuk semua model sekaligus.
Event Class Typed dan Jembatan ke Queue #
Bentuk ketiga sudah kita lihat di chapter Model Events
: property $dispatchesEvents memetakan momen Eloquent ke event class milik kita sendiri. Rekap singkatnya:
use App\Events\PostUpdated;
class Post extends Model
{
protected $dispatchesEvents = [
'updated' => PostUpdated::class,
];
}Sejak pemetaan ini aktif, eloquent.updated: App\Models\Post digantikan object PostUpdated yang membawa instance model. Keunggulan nyatanya baru terasa di dunia queue: event class bertipe membuat model events bisa dikonsumsi listener ber-queue dengan semua fasilitas dari chapter Queued Listeners
:
class NotifyPostEditors implements ShouldQueue
{
public function handle(PostUpdated $event): void
{
Log::info('editor-diberi-tahu', ['judul' => $event->post->judul]);
}
}Saat $post->update(...) dijalankan, listener ini menjadi job di antrean. Isi payload-nya membuktikan satu hal penting tentang SerializesModels — model tidak ikut dibekukan utuh, melainkan disimpan sebagai identifier ringkas:
$p = json_decode(DB::table('jobs')->value('payload'), true);
$cmd = $p['data']['command'];
echo substr($cmd, strpos($cmd, 's:4:"post"'), 90), "\n";Output yang Dihasilkan:
s:4:"post";O:45:"Illuminate\Contracts\Database\ModelIdentifier":5:{s:5:"class";s:15:"App\MCuplikan di atas dipangkas pada 90 karakter. Payload berisi ModelIdentifier — nama class model beserta primary key-nya — bukan snapshot atribut lengkap. Saat worker menjalankan job, Laravel mengambil ulang barisnya dari database sehingga listener selalu membaca data terbaru. Konsekuensinya perlu diingat: bila baris sudah dihapus sebelum job berjalan, proses pemulihan model melempar ModelNotFoundException dan job dianggap gagal mengikuti aturan retry pada antrean.
Closure, Observer, atau Listener Event? #
| Alat | Bentuk | Registrasi | Cocok untuk |
|---|---|---|---|
Closure di booted() | hook di dalam model | otomatis | logika kecil yang erat pada satu model |
| Observer | class khusus per model | #[ObservedBy] / observe() | mengumpulkan banyak hook satu model |
| Listener event system | closure atau class lewat Event::listen | manual atau discovery | logika lintas model, audit, listener ber-queue |
Ketiganya berdiri di dispatcher yang sama, jadi pilihan murni soal organisasi kode. Aturan praktisnya: reaksi yang hanya menyentuh satu model dan perlu memodifikasi atribut sebelum disimpan lebih pas di observer; reaksi yang mengamati banyak model atau butuh queue lebih pas di listener event system.
Dengan tiga jalur mendengarkan model di tangan, tersisa satu pekerjaan untuk menutup section ini: memastikan semua event dan listener teruji. Chapter penutup berikutnya menyusun test suite dari Event::fake() sampai test integrasi tanpa fake.