Contoh Implementasi Observer di Laravel 12: Auto-Slug, Audit Trail, dan Cache

Artikel ini melanjutkan penjelasan konsep Observer di Laravel 12 dengan studi kasus implementasi lengkap: sistem audit trail dan auto-slug generation.

Studi Kasus 1: Auto-Slug Generation

Masalah umum: setiap kali artikel dibuat atau diupdate, slug harus di-generate dari title. Tanpa Observer, logika ini tersebar di berbagai controller.

Dengan Observer, cukup satu tempat:

<?php

namespace AppObservers;

use AppModelsArticle;
use IlluminateSupportStr;

class ArticleObserver
{
    public function creating(Article $article): void
    {
        $article->slug = $this->generateUniqueSlug($article->title);
    }

    public function updating(Article $article): void
    {
        if ($article->isDirty('title')) {
            $article->slug = $this->generateUniqueSlug($article->title, $article->id);
        }
    }

    private function generateUniqueSlug(string $title, ?int $excludeId = null): string
    {
        $slug  = Str::slug($title);
        $query = Article::where('slug', $slug);

        if ($excludeId) {
            $query->where('id', '!=', $excludeId);
        }

        if (!$query->exists()) {
            return $slug;
        }

        // Tambah angka kalau slug sudah ada
        $counter = 1;
        while (Article::where('slug', "{$slug}-{$counter}")
                      ->when($excludeId, fn ($q) => $q->where('id', '!=', $excludeId))
                      ->exists()) {
            $counter++;
        }

        return "{$slug}-{$counter}";
    }
}

Studi Kasus 2: Audit Trail Otomatis

Rekam semua perubahan pada model penting, berguna untuk compliance, debugging, atau fitur “lihat riwayat perubahan”:

<?php

namespace AppObservers;

use AppModelsArticle;
use AppModelsAuditLog;

class ArticleObserver
{
    public function created(Article $article): void
    {
        $this->log('created', $article, [], $article->getAttributes());
    }

    public function updated(Article $article): void
    {
        $this->log('updated', $article, $article->getOriginal(), $article->getChanges());
    }

    public function deleted(Article $article): void
    {
        $this->log('deleted', $article, $article->getAttributes(), []);
    }

    private function log(string $action, Article $article, array $old, array $new): void
    {
        AuditLog::create([
            'user_id'     => auth()->id(),
            'model_type'  => Article::class,
            'model_id'    => $article->id,
            'action'      => $action,
            'old_values'  => $old,
            'new_values'  => $new,
            'ip_address'  => request()->ip(),
        ]);
    }
}

Model AuditLog:

Schema::create('audit_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('model_type');
    $table->unsignedBigInteger('model_id');
    $table->string('action'); // created, updated, deleted
    $table->json('old_values')->nullable();
    $table->json('new_values')->nullable();
    $table->string('ip_address')->nullable();
    $table->timestamp('created_at');

    $table->index(['model_type', 'model_id']);
});

Studi Kasus 3: Cache Invalidation

Cache artikel halaman statis dan harus di-clear saat artikel berubah:

<?php

namespace AppObservers;

use AppModelsArticle;
use IlluminateSupportFacadesCache;

class ArticleObserver
{
    public function saved(Article $article): void
    {
        // Clear cache artikel individual
        Cache::forget("article:{$article->id}");
        Cache::forget("article:{$article->slug}");

        // Clear cache daftar artikel
        Cache::forget('articles:latest');
        Cache::forget("articles:category:{$article->category_id}");
    }

    public function deleted(Article $article): void
    {
        Cache::forget("article:{$article->id}");
        Cache::forget("article:{$article->slug}");
        Cache::forget('articles:latest');
    }
}

Studi Kasus 4: Observer dengan Multiple Models

Kalau beberapa model butuh audit trail yang sama, buat Observer yang reusable:

<?php

namespace AppObservers;

use AppModelsAuditLog;

class AuditableObserver
{
    public function created($model): void
    {
        AuditLog::create([
            'user_id'    => auth()->id(),
            'model_type' => get_class($model),
            'model_id'   => $model->id,
            'action'     => 'created',
            'new_values' => $model->getAttributes(),
        ]);
    }

    public function updated($model): void
    {
        AuditLog::create([
            'user_id'    => auth()->id(),
            'model_type' => get_class($model),
            'model_id'   => $model->id,
            'action'     => 'updated',
            'old_values' => $model->getOriginal(),
            'new_values' => $model->getChanges(),
        ]);
    }
}

Register ke beberapa model sekaligus:

// Di AppServiceProvider
Article::observe(AuditableObserver::class);
Product::observe(AuditableObserver::class);
Order::observe(AuditableObserver::class);

Bypass Observer saat Seeding

<?php

namespace DatabaseSeeders;

use AppModelsArticle;

class ArticleSeeder extends Seeder
{
    public function run(): void
    {
        // Bypass observer agar tidak trigger audit trail saat seeding
        Article::withoutObservers(function () {
            Article::factory(100)->create();
        });
    }
}

Baca Juga

Butuh tim yang bantu implementasi arsitektur yang clean di aplikasi Laravel? Lihat layanan pengembangan aplikasi kami.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *