Skip to content

Instantly share code, notes, and snippets.

@ahmadrio
Created September 3, 2025 09:29
Show Gist options
  • Save ahmadrio/3f198159bfcd06cc4a731de4e19cd26d to your computer and use it in GitHub Desktop.
Save ahmadrio/3f198159bfcd06cc4a731de4e19cd26d to your computer and use it in GitHub Desktop.

Tambah activitylog tanpa observer

User:
Created:
Updated:
Exported: 9/3/2025 16:28:02
Link: https://chatgpt.com/share/68b6c2c6-9994-8006-b477-1da22cd484a1

Prompt:

laravel filament versi 4, pada action default delete, saya ingin menambahkan activitylog namun saya tidak ingin menggunakan observer ?

Response:

Kalau kamu ingin menambahkan activitylog ke dalam action default Delete di Laravel Filament v4 tanpa menggunakan Observer, kamu bisa override langsung DeleteAction bawaan Filament pada resource terkait.

Contohnya, di dalam YourModelResource.php:

use Filament\Tables;
use Filament\Tables\Actions\DeleteAction;
use Spatie\Activitylog\Models\Activity;

class YourModelResource extends Resource
{
    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                // Kolom kamu...
            ])
            ->actions([
                DeleteAction::make()
                    ->after(function ($record) {
                        activity()
                            ->performedOn($record)
                            ->causedBy(auth()->user())
                            ->withProperties([
                                'attributes' => $record->toArray(),
                            ])
                            ->log('Data dihapus melalui Filament');
                    }),
            ]);
    }
}

Penjelasan:

  • DeleteAction::make() → memanggil default action delete.
  • ->after() → callback setelah proses delete berhasil.
  • activity() → fungsi dari package spatie/laravel-activitylog.
  • ->performedOn($record) → mengaitkan log ke model yang dihapus.
  • ->causedBy(auth()->user()) → menyimpan siapa user yang melakukan aksi.
  • ->withProperties() → tambahan detail, misalnya snapshot data sebelum terhapus.
  • ->log('...') → deskripsi aktivitas.

Dengan cara ini, kamu tidak perlu Observer, tapi tetap bisa log otomatis setiap kali ada delete lewat Filament.


Mau saya buatin juga contoh untuk Create dan Edit action dengan pattern yang sama biar konsisten semua aktivitas tercatat?

Prompt:

bantu saya buatkan queue untuk handle activitylog ?

Response:

Thought for 22s

The user wants to handle activity log via queue in Laravel Filament v4. Spatie’s activity log package can be configured to queue logs, but I’m considering safer approaches, like creating a custom job (LogActivityJob). This job would handle the snapshot and dispatch data for saving, ensuring everything’s serializable. I’ll look into using ShouldQueue.

I need to create a LogActivityJob for logging activity. The job would handle soft deletes by using withTrashed and pass the necessary properties, including ID and type. For hard deletes, I’d use subject_type and subject_id directly, and consider using tapActivity or custom properties to modify the log entry.

Done

Siap. Kita bikin Job khusus untuk men‐queue penulisan activity log, lalu panggil Job itu dari DeleteAction (atau action lain) di Filament v4. Tanpa observer. Berikut pola yang aman untuk soft delete maupun hard delete.

  1. Buat Job ===========
php artisan make:job LogActivityJob

app/Jobs/LogActivityJob.php

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;

class LogActivityJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public string $subjectClass;
    public ?int $subjectId;
    public ?int $causerId;
    public string $description;
    public ?string $event;
    public array $properties;
    public ?string $logName;

    /**
     * @param  class-string<Model>  $subjectClass
     * @param  array  $properties   // snapshot data, dll
     */
    public function __construct(
        string $subjectClass,
        ?int $subjectId,
        ?int $causerId,
        string $description,
        ?string $event = null,
        array $properties = [],
        ?string $logName = null
    ) {
        $this->subjectClass = $subjectClass;
        $this->subjectId    = $subjectId;
        $this->causerId     = $causerId;
        $this->description  = $description;
        $this->event        = $event;
        $this->properties   = $properties;
        $this->logName      = $logName;
        $this->onQueue('logging');         // optional: khusus antrian "logging"
        // $this->onConnection('redis');    // optional: pilih koneksi
    }

    public function handle(): void
    {
        // Hindari load model besar-besaran — hanya ambil jika perlu
        $causer = $this->causerId ? \App\Models\User::find($this->causerId) : null;

        $subjectModel = null;
        // Coba ambil subject dengan withTrashed (jika pakai SoftDeletes)
        if ($this->subjectId) {
            $builder = ($this->subjectClass)::query();
            if (in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses_recursive($this->subjectClass))) {
                $builder->withTrashed();
            }
            $subjectModel = $builder->find($this->subjectId);
        }

        $logger = activity($this->logName)
            ->event($this->event)
            ->withProperties($this->properties);

        if ($causer) {
            $logger->causedBy($causer);
        }

        if ($subjectModel instanceof Model) {
            // Normal case (termasuk soft-deleted)
            $logger->performedOn($subjectModel)->log($this->description);
        } else {
            // Hard delete: subject sudah tidak ada. Simpan subject info di properties.
            $logger
                ->withProperties(array_merge($this->properties, [
                    '_subject_type' => $this->subjectClass,
                    '_subject_id'   => $this->subjectId,
                    '_note'         => 'Subject tidak ditemukan saat penulisan log (kemungkinan hard delete).',
                ]))
                ->log($this->description);
        }
    }
}

Catatan:

  • Jangan serialisasikan instance model besar ke job. Kirim class name + ID + snapshot properties saja (aman & ringan).
  • Jika Anda pakai SoftDeletes, withTrashed() memastikan performedOn() tetap bisa dipakai pasca delete.
  • Untuk hard delete, kita tetap tulis log dan menaruh subject_type/subject_id di properties.
  1. Panggil dari Filament DeleteAction =====================================

Di YourModelResource:

use Filament\Tables;
use Filament\Tables\Actions\DeleteAction;
use App\Jobs\LogActivityJob;

public static function table(\Filament\Tables\Table $table): \Filament\Tables\Table
{
    return $table
        ->columns([
            // ...
        ])
        ->actions([
            DeleteAction::make()
                ->after(function ($record) {
                    // Ambil data sebelum terhapus (sudah ada di memori saat after())
                    $snapshot = $record->toArray();

                    LogActivityJob::dispatch(
                        subjectClass: get_class($record),
                        subjectId:    $record->getKey(),
                        causerId:     optional(auth()->user())->getKey(),
                        description:  'Data dihapus melalui Filament',
                        event:        'deleted',
                        properties:   [
                            'attributes' => $snapshot,     // snapshot terakhir
                            'ip'         => request()->ip(),
                            'user_agent' => request()->userAgent(),
                            'route'      => request()->path(),
                        ],
                        logName:      'default',           // atau nama log khusus
                    );
                }),
        ]);
}
  1. (Opsional) Panggil juga di Create/Update ===========================================

Contoh untuk EditAction:

use Filament\Tables\Actions\EditAction;

// ...
EditAction::make()
    ->after(function ($record, array $data) {
        LogActivityJob::dispatch(
            subjectClass: get_class($record),
            subjectId:    $record->getKey(),
            causerId:     optional(auth()->user())->getKey(),
            description:  'Data diperbarui melalui Filament',
            event:        'updated',
            properties:   [
                'new' => $data,
            ],
            logName:      'default',
        );
    })
  1. Konfigurasi Queue (singkat) ==============================
  • .env
    QUEUE_CONNECTION=database   # atau redis, sqs, dll
    
  • Jika pakai database:
    php artisan queue:table && php artisan migrate
    php artisan queue:work --queue=logging,default
    (urutkan agar antrian logging diproses lebih dulu kalau mau)
  1. Tips Produksi ================
  • Idempotensi: kalau ada risiko job dikirim dobel, tambahkan guard (mis. deduplicate key di properties).
  • Retensi: atur activitylog.phpdelete_records_older_than_days via laravel-activitylog (kalau Anda pakai fitur pruners).
  • Privacy: jangan simpan attributes yang sensitif; filter terlebih dulu: Arr::except($snapshot, ['password', 'token']).

Kalau mau, saya bisa siapkan trait helper DispatchesActivityLog supaya pemanggilan LogActivityJob jadi one-liner dan konsisten di seluruh Resource/Pages.

Prompt:

iya

Response:

Thought for 7s

The user confirmed with "iya" that they'd like the trait helper and related integrations. I'll keep things concise, providing code snippets for the trait, service class, and middleware for request context. I'll also include batch logging and throttling, ensuring the language is Indonesian and maintaining brevity.

Done

Mantap. Kita bikin trait helper supaya nge-dispatch activity log jadi one-liner di mana pun (Filament Resource/Pages/Actions, controller, service), plus konteks request (IP, user-agent, route) otomatis ikut.


  1. Helper konteks request (opsional tapi praktis) =================================================

app/Support/RequestContext.php

<?php

namespace App\Support;

class RequestContext
{
    public static function capture(array $extra = []): array
    {
        $req = request();
        return array_filter(array_merge([
            'ip'         => optional($req)->ip(),
            'user_agent' => optional($req)->userAgent(),
            'route'      => optional($req)->path(),
            'url'        => optional($req)->fullUrl(),
            'method'     => optional($req)->method(),
        ], $extra), fn ($v) => !is_null($v));
    }
}

  1. Trait: DispatchesActivityLog =================================

app/Traits/DispatchesActivityLog.php

<?php

namespace App\Traits;

use App\Jobs\LogActivityJob;
use App\Support\RequestContext;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;

trait DispatchesActivityLog
{
    /**
     * Kirim job pencatatan activity ke queue.
     *
     * @param  Model|string  $subject   Model instance ATAU class-string<Model>
     * @param  string|null   $description
     * @param  string|null   $event     contoh: created|updated|deleted|restored|force_deleted
     * @param  array         $properties data tambahan/snapshot
     * @param  string|null   $logName   nama log (default null -> default log)
     * @param  array|null    $options   ['queue'=>'logging','sanitize'=>['password','token']]
     */
    public static function dispatchActivity(
        Model|string $subject,
        ?string $description,
        ?string $event = null,
        array $properties = [],
        ?string $logName = null,
        ?array $options = null
    ): void {
        $options   = array_merge(['queue' => 'logging', 'sanitize' => []], $options ?? []);
        $subjectId = $subject instanceof Model ? $subject->getKey() : null;
        $subjectCl = $subject instanceof Model ? get_class($subject) : $subject;

        // Gabung properties + context request
        $props = array_merge(
            RequestContext::capture(),
            $properties
        );

        // Sanitasi sederhana jika perlu
        if (!empty($options['sanitize'])) {
            $props = Arr::except($props, $options['sanitize']);
            if (isset($props['attributes']) && is_array($props['attributes'])) {
                $props['attributes'] = Arr::except($props['attributes'], $options['sanitize']);
            }
            if (isset($props['new']) && is_array($props['new'])) {
                $props['new'] = Arr::except($props['new'], $options['sanitize']);
            }
            if (isset($props['old']) && is_array($props['old'])) {
                $props['old'] = Arr::except($props['old'], $options['sanitize']);
            }
        }

        // Kirim job
        $job = new LogActivityJob(
            subjectClass: $subjectCl,
            subjectId:    $subjectId,
            causerId:     auth()->id(),
            description:  $description ?? '-',
            event:        $event,
            properties:   $props,
            logName:      $logName
        );

        if (!empty($options['queue'])) {
            $job->onQueue($options['queue']);
        }

        dispatch($job);
    }
}

Catatan:

  • Bisa dipakai static di Resource/Page (self::dispatchActivity(...)) atau di class biasa (DispatchesActivityLog sebagai trait instance method juga berfungsi).
  • Ada opsi sanitize untuk nge-drop field sensitif (mis. ['password','remember_token']).

  1. Pakai di Filament Resource (Delete/Edit/Create) ==================================================

app/Filament/Resources/YourModelResource.php

use App\Traits\DispatchesActivityLog;
use Filament\Tables;
use Filament\Tables\Actions\{DeleteAction, EditAction, CreateAction};

class YourModelResource extends Resource
{
    use DispatchesActivityLog;

    public static function table(\Filament\Tables\Table $table): \Filament\Tables\Table
    {
        return $table
            ->columns([
                // ...
            ])
            ->actions([
                DeleteAction::make()
                    ->after(function ($record) {
                        // snapshot sebelum objek hilang dari memori
                        $snapshot = $record->toArray();

                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data dihapus melalui Filament',
                            event:       'deleted',
                            properties:  ['attributes' => $snapshot],
                            logName:     'default',
                            options:     ['queue' => 'logging', 'sanitize' => ['password','token']]
                        );
                    }),

                EditAction::make()
                    ->after(function ($record, array $data) {
                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data diperbarui melalui Filament',
                            event:       'updated',
                            properties:  ['new' => $data],
                            logName:     'default'
                        );
                    }),

                CreateAction::make()
                    ->after(function ($record, array $data) {
                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data dibuat melalui Filament',
                            event:       'created',
                            properties:  ['new' => $data],
                            logName:     'default'
                        );
                    }),
            ]);
    }
}

Untuk hard delete (force delete di tempat lain), tinggal kirim pakai class name + ID:

self::dispatchActivity(
    subject:     \App\Models\YourModel::class,
    description: 'Data dihapus permanen',
    event:       'force_deleted',
    properties:  ['_reason' => 'maintenance cleanup', '_subject_id' => $id],
);

  1. Bonus (opsional): deduplikasi Job ====================================

Kalau khawatir double dispatch, Anda bisa ubah LogActivityJob agar unik untuk beberapa detik:

use Illuminate\Contracts\Queue\ShouldBeUnique;

class LogActivityJob implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 10; // detik

    public function uniqueId(): string
    {
        return sha1(json_encode([
            $this->subjectClass, $this->subjectId, $this->causerId,
            $this->description, $this->event, $this->properties, $this->logName,
        ]));
    }
}

  1. Ringkas setup Queue ======================
  • .env
    QUEUE_CONNECTION=database
    
  • Migrasi & worker
    php artisan queue:table && php artisan migrate
    php artisan queue:work --queue=logging,default

Kalau mau, saya bisa tambahkan macro helper (mis. activity_dispatch(...)) via Service Provider supaya bisa dipanggil global tanpa trait. Mau dibuatkan juga?

Prompt:

jika handle data banyak atau bulk action

Response:

Thought for 16s

I’m figuring out how to handle bulk delete actions in Filament v4, specifically focusing on efficient activity logging with queue job batching. I need to demonstrate options like per-record dispatch, chunked dispatch, and Bus::batch. Performance considerations will involve avoiding heavy toArray calls, using eager loading, and rate limiting. I'll provide code examples for DeleteBulkAction::make()->after() and handle progress and failure with Bus::batch, keeping transaction management in mind.

Done

Sip, untuk bulk action (mis. DeleteBulkAction) di Filament v4 tanpa observer, kamu punya 3 pola aman & efisien:

  1. Dispatch per-record (paling sederhana)
  2. Dispatch satu job untuk sekumpulan record (ringan di dispatcher)
  3. Gunakan Bus::batch() supaya eksekusi paralel & ada toleransi error

Di bawah ini aku kasih implementasi siap tempel untuk ketiganya. Pilih salah satu sesuai beban data & kebutuhan monitoring.


  1. Prasyarat singkat ====================
  • Pastikan kamu sudah punya LogActivityJob (dari langkah sebelumnya).
  • Worker jalan:
    php artisan queue:work --queue=logging,default

  1. Filament: DeleteBulkAction → dispatch per-record (simpel) ================================================================
use Filament\Tables;
use Filament\Tables\Actions\DeleteBulkAction;
use Illuminate\Support\Collection;
use App\Traits\DispatchesActivityLog; // trait helper yang kita buat

public static function table(\Filament\Tables\Table $table): \Filament\Tables\Table
{
    return $table
        ->columns([
            // ...
        ])
        ->bulkActions([
            DeleteBulkAction::make()
                // Simpan snapshot SEBELUM delete agar data masih ada
                ->before(function (Collection $records) {
                    // inject snapshot ringan ke container action (opsional)
                    // atau, paling mudah: simpan ke cache sementara
                    cache()->put(
                        self::class.':bulk-delete-snapshots:'.auth()->id(),
                        $records->mapWithKeys(fn ($r) => [$r->getKey() => $r->toArray()])->all(),
                        now()->addMinutes(5)
                    );
                })
                ->after(function (Collection $records) {
                    $snapshots = cache()->pull(self::class.':bulk-delete-snapshots:'.auth()->id(), []);

                    $records->each(function ($record) use ($snapshots) {
                        $snapshot = $snapshots[$record->getKey()] ?? $record->toArray(); // fallback

                        // one-liner dari trait
                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data dihapus (bulk) melalui Filament',
                            event:       'deleted',
                            properties:  ['attributes' => $snapshot],
                            logName:     'default',
                            options:     ['queue' => 'logging', 'sanitize' => ['password','token']]
                        );
                    });
                }),
        ]);
}

Kapan cocok: jumlah record tidak besar (puluhan—ratusan) dan kamu ingin log tetap granular per baris.


  1. Filament: DeleteBulkAction → satu job untuk sekumpulan record ====================================================================

Lebih hemat overhead dispatch (satu job), job yang memproses chunk di worker.

2a) Job khusus batch: BulkLogActivityJob

// app/Jobs/BulkLogActivityJob.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class BulkLogActivityJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /** @var array<int, array{class:string,id:int|null,description:string,event:?string,properties:array,logName:?string,causerId:?int}> */
    public array $items;

    public function __construct(array $items)
    {
        $this->items = $items;
        $this->onQueue('logging');
    }

    public function handle(): void
    {
        foreach (array_chunk($this->items, 200) as $chunk) {
            foreach ($chunk as $item) {
                activity($item['logName'] ?? null)
                    ->event($item['event'] ?? null)
                    ->causedBy(optional(\App\Models\User::find($item['causerId'])) ?? null)
                    ->tap(function ($logger) use ($item) {
                        // coba ambil subject; jika soft-deletes, withTrashed
                        $builder = ($item['class'])::query();
                        if (in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses_recursive($item['class']))) {
                            $builder->withTrashed();
                        }
                        $subject = $item['id'] ? $builder->find($item['id']) : null;

                        if ($subject) {
                            $logger->performedOn($subject);
                        } else {
                            $props = $item['properties'] ?? [];
                            $props['_subject_type'] = $item['class'];
                            $props['_subject_id']   = $item['id'];
                            $logger->withProperties($props);
                            return;
                        }

                        $logger->withProperties($item['properties'] ?? []);
                    })
                    ->log($item['description'] ?? '-');
            }
        }
    }
}

2b) Pakai di Bulk Action

use Filament\Tables\Actions\DeleteBulkAction;
use Illuminate\Support\Collection;
use App\Jobs\BulkLogActivityJob;
use App\Support\RequestContext;

DeleteBulkAction::make()
    ->before(function (Collection $records) {
        cache()->put(
            self::class.':bulk-delete-snapshots:'.auth()->id(),
            $records->mapWithKeys(fn ($r) => [$r->getKey() => $r->toArray()])->all(),
            now()->addMinutes(5)
        );
    })
    ->after(function (Collection $records) {
        $snapshots = cache()->pull(self::class.':bulk-delete-snapshots:'.auth()->id(), []);
        $ctx = RequestContext::capture();

        $items = $records->map(function ($r) use ($snapshots, $ctx) {
            return [
                'class'       => get_class($r),
                'id'          => $r->getKey(),
                'causerId'    => auth()->id(),
                'description' => 'Data dihapus (bulk) melalui Filament',
                'event'       => 'deleted',
                'properties'  => array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                'logName'     => 'default',
            ];
        })->values()->all();

        BulkLogActivityJob::dispatch($items);
    });

Kapan cocok: ratusan—ribuan record; ingin minim overhead dispatch dan tetap satu titik eksekusi.


  1. Filament: DeleteBulkAction → Bus::batch() (monitoring & retry individual) ==================================================================================

Kamu dapat visibilitas progress & kegagalan tiap item.

use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
use Filament\Tables\Actions\DeleteBulkAction;
use Illuminate\Support\Collection;
use App\Jobs\LogActivityJob;
use App\Support\RequestContext;

DeleteBulkAction::make()
    ->before(function (Collection $records) {
        cache()->put(
            self::class.':bulk-delete-snapshots:'.auth()->id(),
            $records->mapWithKeys(fn ($r) => [$r->getKey() => $r->toArray()])->all(),
            now()->addMinutes(5)
        );
    })
    ->after(function (Collection $records) {
        $snapshots = cache()->pull(self::class.':bulk-delete-snapshots:'.auth()->id(), []);
        $ctx = RequestContext::capture();

        $jobs = $records->map(function ($r) use ($snapshots, $ctx) {
            return new LogActivityJob(
                subjectClass: get_class($r),
                subjectId:    $r->getKey(),
                causerId:     auth()->id(),
                description:  'Data dihapus (bulk) melalui Filament',
                event:        'deleted',
                properties:   array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                logName:      'default'
            );
        })->values()->all();

        Bus::batch($jobs)
            ->onQueue('logging')
            ->name('bulk-delete-activitylog:'.class_basename(static::class))
            ->allowFailures() // biarkan sebagian gagal tanpa menggagalkan batch
            ->dispatch();
    });

Kapan cocok: ratusan—puluhan ribu record; kamu ingin progress, partial failures, & kemudahan observabilitas.


Catatan Penting Bulk

  • Ambil snapshot sebelum delete (->before) supaya data masih lengkap.
    Kalau data besar, simpan field minimal saja:
    $records->mapWithKeys(fn ($r) => [$r->getKey() => $r->only(['id','kode','nama'])]);
  • Soft delete vs hard delete sudah ditangani: job mencoba withTrashed(), kalau gagal, subjek dicatat di properties.
  • Chunking: di BulkLogActivityJob sudah ada array_chunk(…, 200). Ubah sesuai kapasitas.
  • Sanitasi: hindari menyimpan data sensitif (password, token, dsb).
  • Transaksi: kalau operasi delete kamu dibungkus transaksi manual, dispatch job setelah commit:
    DB::afterCommit(fn () => Bus::batch($jobs)->dispatch());
  • Kustom queue: jalankan worker khusus logging:
    php artisan queue:work --queue=logging --tries=3

Kalau kamu juga pakai Bulk Restore atau Bulk Force Delete, tinggal duplikasi pola yang sama:

  • event: 'restored' untuk restore,
  • event: 'force_deleted' untuk hard delete,
  • dan pada force delete, kirim subjectClass + id kalau model sudah tidak tersedia.

Butuh versi yang di-wrap jadi helper Bulk (satu fungsi util yang kamu panggil dari berbagai Resource)? Aku bisa rapikan jadi BulkActivityLogger service juga.

Prompt:

iya silakan

Response:

Siap. Berikut service helper yang merapikan seluruh pola bulk logging (per-record, satu job, atau Bus::batch) + util untuk snapshot sebelum delete. Tujuannya: cukup panggil 1–2 baris di tiap Bulk Action.


  1. Service: BulkActivityLogger ================================

app/Services/BulkActivityLogger.php

<?php

namespace App\Services;

use App\Jobs\LogActivityJob;
use App\Jobs\BulkLogActivityJob;
use App\Support\RequestContext;
use Illuminate\Bus\Batch;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;

class BulkActivityLogger
{
    /**
     * Simpan snapshot ringan ke cache sebelum operasi bulk (mis. delete).
     * Pakai di callback ->before() Filament BulkAction.
     *
     * @param  iterable  $records    Eloquent\Collection|array
     * @param  string    $cacheKey   unik per user/aksi
     * @param  array|null $only      field yang disimpan (null = toArray())
     * @param  int       $ttlMinutes TTL cache
     */
    public static function cacheSnapshots(iterable $records, string $cacheKey, ?array $only = null, int $ttlMinutes = 10): void
    {
        $map = collect($records)->mapWithKeys(function ($r) use ($only) {
            /** @var \Illuminate\Database\Eloquent\Model $r */
            $data = $only ? $r->only($only) : $r->toArray();
            return [$r->getKey() => $data];
        })->all();

        Cache::put($cacheKey, $map, now()->addMinutes($ttlMinutes));
    }

    /**
     * Ambil & hapus snapshot dari cache (sekali pakai).
     */
    public static function pullSnapshots(string $cacheKey, array $default = []): array
    {
        return Cache::pull($cacheKey, $default);
    }

    /**
     * Dispatch logging per-record (granular).
     *
     * @param  iterable $records
     * @param  string   $description
     * @param  string|null $event
     * @param  array    $snapshots   [id => snapshotArray]
     * @param  string|null $logName
     * @param  array    $options ['queue'=>'logging','sanitize'=>[...] ]
     */
    public static function dispatchPerRecord(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging', 'sanitize' => ['password','token']]
    ): void {
        $ctx = RequestContext::capture();

        collect($records)->each(function ($r) use ($description, $event, $snapshots, $logName, $ctx, $options) {
            $snapshot = $snapshots[$r->getKey()] ?? [];
            // Kirim job satu per record
            $job = new LogActivityJob(
                subjectClass: get_class($r),
                subjectId:    $r->getKey(),
                causerId:     auth()->id(),
                description:  $description,
                event:        $event,
                properties:   array_merge($ctx, ['attributes' => $snapshot]),
                logName:      $logName,
            );

            if (!empty($options['queue'])) {
                $job->onQueue($options['queue']);
            }

            dispatch($job);
        });
    }

    /**
     * Dispatch satu job untuk sekumpulan record (ringan di dispatcher).
     */
    public static function dispatchSingleJob(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging']
    ): void {
        $ctx = RequestContext::capture();

        $items = collect($records)->map(function ($r) use ($description, $event, $snapshots, $logName, $ctx) {
            return [
                'class'       => get_class($r),
                'id'          => $r->getKey(),
                'causerId'    => auth()->id(),
                'description' => $description,
                'event'       => $event,
                'properties'  => array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                'logName'     => $logName,
            ];
        })->values()->all();

        $job = new BulkLogActivityJob($items);
        if (!empty($options['queue'])) {
            $job->onQueue($options['queue']);
        }
        dispatch($job);
    }

    /**
     * Dispatch sebagai batch (monitor progress & partial failures).
     */
    public static function dispatchAsBatch(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging', 'name' => null, 'allowFailures' => true]
    ): Batch {
        $ctx = RequestContext::capture();

        $jobs = collect($records)->map(function ($r) use ($description, $event, $snapshots, $logName, $ctx) {
            return (new LogActivityJob(
                subjectClass: get_class($r),
                subjectId:    $r->getKey(),
                causerId:     auth()->id(),
                description:  $description,
                event:        $event,
                properties:   array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                logName:      $logName,
            ))->onQueue($options['queue'] ?? 'logging');
        })->values()->all();

        $batch = Bus::batch($jobs)
            ->onQueue($options['queue'] ?? 'logging')
            ->name($options['name'] ?? ('bulk-activitylog-'.now()->format('YmdHis')))
            ->when($options['allowFailures'] ?? true, fn ($b) => $b->allowFailures())
            ->dispatch();

        return $batch;
    }
}

  1. Contoh pakai di Filament (DeleteBulkAction) ==============================================
use Filament\Tables\Actions\DeleteBulkAction;
use App\Services\BulkActivityLogger;

DeleteBulkAction::make()
    ->before(function (\Illuminate\Support\Collection $records) {
        // Simpan snapshot ringan sebelum delete
        $cacheKey = static::class.':bulk-delete-snapshots:'.auth()->id();
        BulkActivityLogger::cacheSnapshots($records, $cacheKey, only: ['id','kode','nama']); // pilih field penting saja
    })
    ->after(function (\Illuminate\Support\Collection $records) {
        $cacheKey  = static::class.':bulk-delete-snapshots:'.auth()->id();
        $snapshots = BulkActivityLogger::pullSnapshots($cacheKey, []);

        // PILIH SALAH SATU pola di bawah:

        // (A) Granular per-record
        // BulkActivityLogger::dispatchPerRecord($records, 'Data dihapus (bulk) via Filament', 'deleted', $snapshots);

        // (B) Satu job untuk semua
        // BulkActivityLogger::dispatchSingleJob($records, 'Data dihapus (bulk) via Filament', 'deleted', $snapshots);

        // (C) Batch (punya progress & partial failure)
        BulkActivityLogger::dispatchAsBatch(
            $records,
            description: 'Data dihapus (bulk) via Filament',
            event: 'deleted',
            snapshots: $snapshots,
            logName: 'default',
            options: ['queue' => 'logging', 'name' => 'bulk-delete-'.class_basename(static::class)]
        );
    });

Ganti only: ['id','kode','nama'] sesuai kebutuhan. Hindari field sensitif.


  1. Tambahan: Bulk Restore & Force Delete ========================================
// Restore bulk
BulkActivityLogger::dispatchPerRecord($records, 'Data dipulihkan (bulk) via Filament', 'restored', $snapshots);

// Force delete bulk (hard delete):
// Kalau subject sudah hilang, job akan menaruh _subject_type & _subject_id di properties.
BulkActivityLogger::dispatchAsBatch(
    $records,
    'Data dihapus permanen (bulk) via Filament',
    'force_deleted',
    $snapshots
);

  1. Best Practices Singkat =========================
  • Snapshot minimal: pilih field penting dengan only agar cache & log ringan.
  • Transaksi: bila delete dibungkus transaksi manual, panggil dispatcher setelah commit (DB::afterCommit()).
  • Queue khusus: jalankan worker terpisah untuk logging:
    php artisan queue:work --queue=logging --tries=3
  • Sanitasi: filter password, token, dsb di snapshot sebelum dikirim.
  • Observabilitas: untuk beban besar, pilih batch agar bisa pantau progress & failure.

Kalau kamu mau, saya bisa tambahkan Facade (BulkActivityLog) atau binding di Service Provider supaya pemanggilan jadi lebih singkat.

Prompt:

mgkn bisa kasih saya versi lengkapnya step by step nya

Response:

Thought for 30s

I need to create a full guide for queued activity logging in Laravel Filament v4, covering default delete actions, bulk actions, job classes, and specific hooks. I'll include step-by-step instructions, configuring the necessary packages and templates, testing steps, and privacy considerations while maintaining performance optimization tips. I'll keep it concise and detailed with code snippets but avoid unnecessary fluff.

I need to give a detailed, step-by-step guide in Bahasa Indonesia for implementing queued activity logging in Filament v4. I’ll structure the guide with file paths, include options for both per-record and batch logging, and recommend a default choice. I'll cover integration for Filament’s BulkAction and Model actions, ensure proper namespaces, and highlight potential issues like callback signatures in DeleteAction and DeleteBulkAction.

I’ll provide a clear step-by-step process, including commands for installing the necessary package, publishing the config, and setting up the activity_log table. I’ll cover queue setup with command-line instructions, and also include troubleshooting, privacy, and security details. I’ll stay concise and provide Filament resource examples without extra fluff. A small test scenario for verification will help too.

Done

Siap—ini versi lengkap & runut step-by-step untuk menambahkan activity log via queue pada Filament v4 (termasuk single delete & bulk action) tanpa observer. Kamu bisa copy–paste per langkah.


Step 0 — Pra-syarat

  • Laravel 10/11 + Filament v4 sudah terpasang.
  • (Jika belum) pasang Spatie Activitylog:
composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config"
php artisan migrate

Step 1 — Konfigurasi Queue

Pilih koneksi (contoh: database).

.env

QUEUE_CONNECTION=database

Buat tabel antrian & failed jobs:

php artisan queue:table
php artisan queue:failed-table
php artisan migrate

Jalankan worker (antrian khusus logging + default):

php artisan queue:work --queue=logging,default --tries=3

Bisa pakai Redis/SQS juga—cukup ubah QUEUE_CONNECTION dan worker-nya.


Step 2 — Helper konteks request (opsional tapi berguna)

app/Support/RequestContext.php

<?php

namespace App\Support;

class RequestContext
{
    public static function capture(array $extra = []): array
    {
        $req = request();

        return array_filter(array_merge([
            'ip'         => optional($req)->ip(),
            'user_agent' => optional($req)->userAgent(),
            'url'        => optional($req)->fullUrl(),
            'route'      => optional($req)->path(),
            'method'     => optional($req)->method(),
        ], $extra), fn ($v) => !is_null($v));
    }
}

Step 3 — Job dasar untuk 1 record

app/Jobs/LogActivityJob.php

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Database\Eloquent\SoftDeletes;

class LogActivityJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public string $subjectClass;
    public ?int $subjectId;
    public ?int $causerId;
    public string $description;
    public ?string $event;
    public array $properties;
    public ?string $logName;

    public function __construct(
        string $subjectClass,
        ?int $subjectId,
        ?int $causerId,
        string $description,
        ?string $event = null,
        array $properties = [],
        ?string $logName = null
    ) {
        $this->subjectClass = $subjectClass;
        $this->subjectId    = $subjectId;
        $this->causerId     = $causerId;
        $this->description  = $description;
        $this->event        = $event;
        $this->properties   = $properties;
        $this->logName      = $logName;

        $this->onQueue('logging');
    }

    public function handle(): void
    {
        $causer = $this->causerId ? User::find($this->causerId) : null;

        $subject = null;
        if ($this->subjectId) {
            $builder = ($this->subjectClass)::query();

            // dukung SoftDeletes (termasuk yang sudah di-soft delete)
            if (in_array(SoftDeletes::class, class_uses_recursive($this->subjectClass))) {
                $builder->withTrashed();
            }
            $subject = $builder->find($this->subjectId);
        }

        $logger = activity($this->logName)
            ->event($this->event)
            ->withProperties($this->properties);

        if ($causer) {
            $logger->causedBy($causer);
        }

        if ($subject instanceof Model) {
            $logger->performedOn($subject)->log($this->description);
            return;
        }

        // Hard delete: subject tidak ditemukan — simpan identitasnya di properties
        $logger->withProperties(array_merge($this->properties, [
            '_subject_type' => $this->subjectClass,
            '_subject_id'   => $this->subjectId,
            '_note'         => 'Subject tidak ditemukan saat menulis log (kemungkinan hard delete).',
        ]))->log($this->description);
    }
}

Step 4 — Job untuk banyak record (satu job)

app/Jobs/BulkLogActivityJob.php

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

/**
 * @property array<int, array{
 *   class:string,
 *   id:int|null,
 *   causerId:int|null,
 *   description:string,
 *   event:?string,
 *   properties:array,
 *   logName:?string
 * }> $items
 */
class BulkLogActivityJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public array $items;

    public function __construct(array $items)
    {
        $this->items = $items;
        $this->onQueue('logging');
    }

    public function handle(): void
    {
        foreach (array_chunk($this->items, 200) as $chunk) {
            foreach ($chunk as $item) {
                $logger = activity($item['logName'] ?? null)
                    ->event($item['event'] ?? null)
                    ->withProperties($item['properties'] ?? []);

                $causer = $item['causerId'] ? User::find($item['causerId']) : null;
                if ($causer) {
                    $logger->causedBy($causer);
                }

                $subject = null;
                if (!empty($item['id'])) {
                    $builder = ($item['class'])::query();
                    if (in_array(SoftDeletes::class, class_uses_recursive($item['class']))) {
                        $builder->withTrashed();
                    }
                    $subject = $builder->find($item['id']);
                }

                if ($subject instanceof Model) {
                    $logger->performedOn($subject)->log($item['description'] ?? '-');
                } else {
                    $logger->withProperties(array_merge(($item['properties'] ?? []), [
                        '_subject_type' => $item['class'],
                        '_subject_id'   => $item['id'] ?? null,
                        '_note'         => 'Subject tidak ditemukan saat menulis log (kemungkinan hard delete).',
                    ]))->log($item['description'] ?? '-');
                }
            }
        }
    }
}

Step 5 — Trait helper (one-liner)

app/Traits/DispatchesActivityLog.php

<?php

namespace App\Traits;

use App\Jobs\LogActivityJob;
use App\Support\RequestContext;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;

trait DispatchesActivityLog
{
    /**
     * @param  Model|string  $subject   Model instance atau class-string<Model>
     * @param  string|null   $description
     * @param  string|null   $event
     * @param  array         $properties
     * @param  string|null   $logName
     * @param  array|null    $options   ['queue'=>'logging','sanitize'=>['password','token']]
     */
    public static function dispatchActivity(
        Model|string $subject,
        ?string $description,
        ?string $event = null,
        array $properties = [],
        ?string $logName = null,
        ?array $options = null
    ): void {
        $options   = array_merge(['queue' => 'logging', 'sanitize' => []], $options ?? []);
        $subjectId = $subject instanceof Model ? $subject->getKey() : null;
        $subjectCl = $subject instanceof Model ? get_class($subject) : $subject;

        // Tambahkan konteks request
        $props = array_merge(RequestContext::capture(), $properties);

        // Sanitasi field sensitif
        if (!empty($options['sanitize'])) {
            $props = Arr::except($props, $options['sanitize']);
            foreach (['attributes','new','old'] as $bag) {
                if (isset($props[$bag]) && is_array($props[$bag])) {
                    $props[$bag] = Arr::except($props[$bag], $options['sanitize']);
                }
            }
        }

        $job = new LogActivityJob(
            subjectClass: $subjectCl,
            subjectId:    $subjectId,
            causerId:     auth()->id(),
            description:  $description ?? '-',
            event:        $event,
            properties:   $props,
            logName:      $logName
        );

        if (!empty($options['queue'])) {
            $job->onQueue($options['queue']);
        }

        dispatch($job);
    }
}

Step 6 — Service untuk Bulk (rapi & reusable)

app/Services/BulkActivityLogger.php

<?php

namespace App\Services;

use App\Jobs\LogActivityJob;
use App\Jobs\BulkLogActivityJob;
use App\Support\RequestContext;
use Illuminate\Bus\Batch;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;

class BulkActivityLogger
{
    public static function cacheSnapshots(iterable $records, string $cacheKey, ?array $only = null, int $ttlMinutes = 10): void
    {
        $map = collect($records)->mapWithKeys(function ($r) use ($only) {
            $data = $only ? $r->only($only) : $r->toArray();
            return [$r->getKey() => $data];
        })->all();

        Cache::put($cacheKey, $map, now()->addMinutes($ttlMinutes));
    }

    public static function pullSnapshots(string $cacheKey, array $default = []): array
    {
        return Cache::pull($cacheKey, $default);
    }

    /** Per-record jobs (granular) */
    public static function dispatchPerRecord(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging']
    ): void {
        $ctx = RequestContext::capture();

        collect($records)->each(function ($r) use ($description, $event, $snapshots, $logName, $ctx, $options) {
            $job = (new LogActivityJob(
                subjectClass: get_class($r),
                subjectId:    $r->getKey(),
                causerId:     auth()->id(),
                description:  $description,
                event:        $event,
                properties:   array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                logName:      $logName
            ))->onQueue($options['queue'] ?? 'logging');

            dispatch($job);
        });
    }

    /** Satu job untuk sekumpulan record (hemat dispatcher) */
    public static function dispatchSingleJob(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging']
    ): void {
        $ctx = RequestContext::capture();

        $items = collect($records)->map(function ($r) use ($description, $event, $snapshots, $logName, $ctx) {
            return [
                'class'       => get_class($r),
                'id'          => $r->getKey(),
                'causerId'    => auth()->id(),
                'description' => $description,
                'event'       => $event,
                'properties'  => array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                'logName'     => $logName,
            ];
        })->values()->all();

        dispatch((new BulkLogActivityJob($items))->onQueue($options['queue'] ?? 'logging'));
    }

    /** Batch (progress & partial failures) */
    public static function dispatchAsBatch(
        iterable $records,
        string $description,
        ?string $event = null,
        array $snapshots = [],
        ?string $logName = 'default',
        array $options = ['queue' => 'logging', 'name' => null, 'allowFailures' => true]
    ): Batch {
        $ctx = RequestContext::capture();

        $jobs = collect($records)->map(function ($r) use ($description, $event, $snapshots, $logName, $ctx, $options) {
            return (new LogActivityJob(
                subjectClass: get_class($r),
                subjectId:    $r->getKey(),
                causerId:     auth()->id(),
                description:  $description,
                event:        $event,
                properties:   array_merge($ctx, ['attributes' => $snapshots[$r->getKey()] ?? []]),
                logName:      $logName
            ))->onQueue($options['queue'] ?? 'logging');
        })->values()->all();

        return Bus::batch($jobs)
            ->onQueue($options['queue'] ?? 'logging')
            ->name($options['name'] ?? ('bulk-activitylog-'.now()->format('YmdHis')))
            ->when($options['allowFailures'] ?? true, fn ($b) => $b->allowFailures())
            ->dispatch();
    }
}

Step 7 — Pakai di Filament (Single Delete, Create, Edit)

Di YourModelResource:

<?php

namespace App\Filament\Resources;

use App\Filament\Resources\YourModelResource\Pages;
use App\Models\YourModel;
use App\Traits\DispatchesActivityLog;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Actions\{DeleteAction, EditAction, CreateAction};

class YourModelResource extends Resource
{
    use DispatchesActivityLog;

    protected static ?string $model = YourModel::class;

    public static function table(Tables\Table $table): Tables\Table
    {
        return $table
            ->columns([
                // kolom kamu...
            ])
            ->actions([
                DeleteAction::make()
                    ->after(function ($record) {
                        // snapshot sebelum objek lenyap dari request
                        $snapshot = $record->toArray();

                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data dihapus melalui Filament',
                            event:       'deleted',
                            properties:  ['attributes' => $snapshot],
                            logName:     'default',
                            options:     ['queue' => 'logging', 'sanitize' => ['password','token']]
                        );
                    }),

                EditAction::make()
                    ->after(function ($record, array $data) {
                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data diperbarui melalui Filament',
                            event:       'updated',
                            properties:  ['new' => $data],
                            logName:     'default'
                        );
                    }),

                CreateAction::make()
                    ->after(function ($record, array $data) {
                        self::dispatchActivity(
                            subject:     $record,
                            description: 'Data dibuat melalui Filament',
                            event:       'created',
                            properties:  ['new' => $data],
                            logName:     'default'
                        );
                    }),
            ]);
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListYourModels::route('/'),
            // lainnya...
        ];
    }
}

Hard delete (force delete) di tempat lain? Pakai:

self::dispatchActivity(
    subject: \App\Models\YourModel::class, // class string
    description: 'Data dihapus permanen',
    event: 'force_deleted',
    properties: ['_subject_id' => $id]
);

Step 8 — Pakai di Filament (Bulk Delete)

Contoh paling praktis: pakai batch supaya ada toleransi error.

use Filament\Tables\Actions\DeleteBulkAction;
use App\Services\BulkActivityLogger;

// ...
->bulkActions([
    DeleteBulkAction::make()
        ->before(function (\Illuminate\Support\Collection $records) {
            // simpan snapshot ringan sebelum delete
            $cacheKey = static::class.':bulk-delete-snapshots:'.auth()->id();
            BulkActivityLogger::cacheSnapshots($records, $cacheKey, only: ['id','kode','nama']);
        })
        ->after(function (\Illuminate\Support\Collection $records) {
            $cacheKey  = static::class.':bulk-delete-snapshots:'.auth()->id();
            $snapshots = BulkActivityLogger::pullSnapshots($cacheKey, []);

            // PILIH SATU pola:
            // A) granular per-record:
            // BulkActivityLogger::dispatchPerRecord($records, 'Data dihapus (bulk) via Filament', 'deleted', $snapshots);

            // B) satu job untuk semua:
            // BulkActivityLogger::dispatchSingleJob($records, 'Data dihapus (bulk) via Filament', 'deleted', $snapshots);

            // C) batch (disarankan untuk ratusan+):
            \Illuminate\Support\Facades\DB::afterCommit(function () use ($records, $snapshots) {
                BulkActivityLogger::dispatchAsBatch(
                    $records,
                    description: 'Data dihapus (bulk) via Filament',
                    event: 'deleted',
                    snapshots: $snapshots,
                    logName: 'default',
                    options: ['queue' => 'logging', 'name' => 'bulk-delete-'.class_basename(static::class)]
                );
            });
        }),
])

Kenapa DB::afterCommit? Kalau operasi delete-mu memakai transaksi, kita pastikan job dikirim setelah commit, sehingga snapshot & ID konsisten.


Step 9 — Konfigurasi & Kebijakan Log (opsional)

Ubah config/activitylog.php sesuai kebutuhan:

  • Log name default, event names, dsb.
  • Pruning: hapus otomatis log lama (mis. 90 hari):
    'default_log_name' => 'default',
    'delete_records_older_than_days' => 90,
  • Privacy: pastikan tidak menyimpan field sensitif di properties (gunakan sanitize atau only([...]) saat snapshot).

Step 10 — Verifikasi Cepat

  1. Jalankan worker:
php artisan queue:work --queue=logging,default
  1. Lakukan Create/Edit/Delete atau Bulk Delete di Filament.
  2. Cek tabel activity_log:
SELECT id, log_name, description, event, causer_id, subject_type, subject_id, properties, created_at
FROM activity_log
ORDER BY id DESC
LIMIT 20;

Kamu harus melihat entri:

  • description: “Data dibuat/diperbarui/dihapus…”
  • event: created|updated|deleted|force_deleted|restored
  • properties: memuat attributes (snapshot), ip, user_agent, dll.

Best Practice & Tuning

  • Snapshot minimal: pakai only(['id','kode','nama']) agar ringan.
  • Chunk: di BulkLogActivityJob default 200; sesuaikan beban.
  • Queue terpisah: jalankan worker khusus logging untuk isolasi.
  • Retry: --tries=3 + failed jobs table sudah siap.
  • Observabilitas: Jika pakai Redis, pertimbangkan Laravel Horizon.

Kalau mau, aku bisa ringkas semua ini ke sebuah package internal (Service Provider + Facade) biar instalasinya tinggal composer require + publish config. Mau?


Powered by ChatGPT Exporter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment