Setiap model melewati siklus hidup yang bisa diprediksi: ia dibaca dari database, disimpan, diubah, lalu dihapus atau dipulihkan. Sering kali kita punya pekerjaan kecil yang harus menempel tepat di momen-momen itu — mengisi slug sebelum baris baru masuk, membersihkan cache setelah data berubah, atau mengirim notifikasi setelah post terbit. Menaruh logika semacam ini di setiap controller justru berbahaya karena satu jalur simpan yang terlepas berarti logika itu tidak jalan. Model events menyelesaikannya dengan membiarkan kita mendaftarkan listener pada titik-titik siklus hidup model, seperti dijelaskan dokumentasi Eloquent .

Peta Event Siklus Hidup #

Eloquent menyalakan event bernama sesuai momennya, dan penamaannya konsisten: akhiran -ing berarti sebelum operasi, akhiran -ed berarti sesudah:

EventMenyala saat
retrievedmodel selesai dibaca dari database
creating / createdsebelum / sesudah INSERT baris baru
updating / updatedsebelum / sesudah UPDATE
saving / savedsebelum / sesudah INSERT maupun UPDATE
deleting / deletedsebelum / sesudah penghapusan
restoring / restoredsebelum / sesudah restore() soft delete

Karena model Post kita memakai Soft Deletes , tersedia pula event tambahan: trashed saat delete() menandai baris, serta forceDeleting/forceDeleted di sekitar forceDelete(). Pasangan saving/saved terlihat tumpang tindih dengan creating/created, tetapi keduanya melingkupi operasi tulis apa pun — berguna bila logika kita tidak peduli apakah baris itu baru atau lama.

Mengamati Seluruh Siklus #

Cara paling jujur untuk melihat urutannya adalah mendengarkan semuanya sekaligus. Listener wildcard eloquent.* menerima setiap event Eloquent yang menyala:

php
use Illuminate\Support\Facades\Event;

$fired = [];
Event::listen('eloquent.*', function (string $name, array $data) use (&$fired) {
    $fired[] = str_replace(['eloquent.', ': App\\Models\\Post'], '', $name);
});

$post = Post::create([
    'user_id' => $budi->id,
    'judul' => 'Belajar Model Event',
    'slug' => 'belajar-model-event',
    'isi' => 'Isi tulisan.',
]);

$post->update(['views' => 10]);
$post->delete();
Post::withTrashed()->find($post->id)->restore();

foreach ($fired as $e) {
    echo 'eloquent.', $e, ": Post\n";
}

Output yang Dihasilkan:

text
eloquent.booting: Post
eloquent.booted: Post
eloquent.saving: Post
eloquent.creating: Post
eloquent.created: Post
eloquent.saved: Post
eloquent.saving: Post
eloquent.updating: Post
eloquent.updated: Post
eloquent.saved: Post
eloquent.deleting: Post
eloquent.trashed: Post
eloquent.deleted: Post
eloquent.retrieved: Post
eloquent.restoring: Post
eloquent.saving: Post
eloquent.updating: Post
eloquent.updated: Post
eloquent.saved: Post
eloquent.restored: Post

Empat pola penting terbaca dari urutan di atas. Pertama, booting dan booted hanya menyala sekali — saat class Post pertama kali di-boot, bukan pada setiap operasi. Kedua, operasi tulis selalu dibungkus saving → creating → created → saved; pembacaan lewat withTrashed()->find() menyala sebagai retrieved. Ketiga, soft delete memicu deleting → trashed → deleted — tanpa DELETE sungguhan. Keempat, restore() ternyata menembakkan event-event UPDATE di antara restoring dan restored, karena memulihkan baris berarti mengosongkan deleted_at lewat UPDATE. Listener wildcard semacam ini jarang dipakai di aplikasi produksi, tetapi sangat berguna untuk mendiagnosis dan menguji urutan event.

Hook di booted() #

Cara paling ringkas menempel listener adalah method booted() di dalam model. Hook creating berikut mengisi slug otomatis dari judul setiap kali baris baru dibuat:

php
use Illuminate\Support\Str;

class Post extends Model
{
    // ...

    protected static function booted(): void
    {
        static::creating(function (Post $post) {
            if (empty($post->slug)) {
                $post->slug = Str::slug($post->judul);
            }
        });
    }
}

Kita uji dengan mengosongkan slug pada saat membuat post:

php
$post = Post::factory()->create([
    'user_id' => $budi->id,
    'judul' => 'Belajar Model Event',
    'slug' => null,
]);

echo 'judul: ', $post->judul, "\n";
echo 'slug (diisi hook creating): ', $post->slug, "\n";

Output yang Dihasilkan:

text
judul: Belajar Model Event
slug (diisi hook creating): belajar-model-event

Event creating menyala sebelum INSERT, sehingga perubahan atribut di dalam hook ikut tersimpan tanpa query tambahan. Catatan kecil: method statis Post::creating(...) dan kawan-kawannya juga bisa dipanggil dari luar class model untuk pendaftaran sementara — pola yang akan kita pakai di demo veto di bawah.

Event Class Custom dengan $dispatchesEvents #

Bila event bawaan perlu menjadi “warga kelas satu” — masuk queue, dibroadcast, atau dipakai listener dari dokumentasi events — kita bisa memetakannya ke event class sendiri. Artisan menyiapkan kerangkanya:

bash
php artisan make:event PostCreated

Output yang Dihasilkan:

text
   INFO  Event [app/Events/PostCreated.php] created successfully.  

Kita rampingkan class-nya menjadi pembungkus model:

php
namespace App\Events;

use App\Models\Post;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PostCreated
{
    use Dispatchable, SerializesModels;

    public function __construct(public Post $post)
    {
    }
}

Lalu model mengarahkan event created ke class tersebut lewat property $dispatchesEvents:

php
use App\Events\PostCreated;

class Post extends Model
{
    protected $dispatchesEvents = [
        'created' => PostCreated::class,
    ];

Sejak pemetaan ini aktif, setiap created pada Post tidak lagi menyala sebagai string eloquent.created: App\Models\Post, melainkan sebagai object PostCreated:

php
Event::listen(PostCreated::class, function (PostCreated $event) {
    echo 'Listener PostCreated menerima: ', $event->post->judul, "\n";
});

$post = Post::factory()->create([
    'user_id' => $budi->id,
    'judul' => 'Event Kelas Custom',
    'slug' => 'event-kelas-custom',
]);

Output yang Dihasilkan:

text
Listener PostCreated menerima: Event Kelas Custom

Model yang memicu event dibawa sebagai constructor argument, sehingga listener mengaksesnya lewat $event->post.

Membatalkan Operasi dengan return false #

Listener pada event -ing memegang satu kekuasaan khusus: mengembalikan false membatalkan seluruh operasi. Berikut aturan mutlak bahwa judul mengandung kata DRAFT tidak boleh tersimpan:

php
Post::saving(function (Post $post) {
    if (str_contains($post->judul, 'DRAFT')) {
        return false; // veto: batalkan penyimpanan
    }
});

$draft = new Post();
$draft->user_id = $budi->id;
$draft->judul = 'DRAFT Belajar Event';
$draft->slug = 'draft-belajar-event';
$draft->isi = 'Tulisan yang belum boleh tersimpan.';

echo 'save() mengembalikan: ', var_export($draft->save(), true), "\n";
echo 'Post::count(): ', Post::count(), "\n";
echo '$draft->exists: ', var_export($draft->exists, true), "\n";

Output yang Dihasilkan:

text
save() mengembalikan: false
Post::count(): 2
$draft->exists: false

Pada skrip ini dua post telah tersimpan dari demo-demo sebelumnya, dan angkanya tidak bertambah: save() mengembalikan false, query INSERT tidak pernah terkirim, dan model tetap berstatus belum ada di database. Sebaliknya, bila yang kita butuhkan justru melewati semua event, method saveQuietly() dan createQuietly() menyimpan data tanpa menyalakan satu event pun — terbukti pada pengujian yang sama ketika listener wildcard mencatat nol event saat createQuietly() dipanggil sementara barisnya tetap tersimpan.

Sampai di sini kita menulis listener satu per satu di dalam model. Bayangkan bila satu model butuh tujuh hook berbeda — method booted() akan menjadi lemari penuh. Laravel punya rumah khusus untuk koleksi listener itu, dan namanya adalah observer.