| Current Path : /var/www/html/reddsis/docs/module/ |
| Current File : /var/www/html/reddsis/docs/module/activity-log.md |
# Activity Log Module — Dokumentasi Penuh
## 1. Pengenalan
Activity Log module merekod dan memaparkan setiap aktiviti penting dalam sistem — bila user create, update, delete data, dan siapa yang buat. Module ni guna package **Spatie Activitylog** (`spatie/laravel-activitylog`).
---
## 2. Struktur Database
**Table:** `activity_log`
| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint (PK) | Primary key |
| `log_name` | string (nullable) | Nama kumpulan log (contoh: `Backend User`, `Frontend User`) |
| `description` | text | Penerangan aktiviti |
| `subject_type` | string (nullable) | Model class yang kena action (polymorphic) |
| `subject_id` | bigint (nullable) | ID record yang kena action |
| `event` | string (nullable) | Jenis event: `created`, `updated`, `deleted`, `login`, dll |
| `causer_type` | string (nullable) | Model class yang buat action (polymorphic) |
| `causer_id` | bigint (nullable) | ID user yang buat action |
| `attribute_changes` | json (nullable) | Senarai field yang berubah (old vs new) |
| `properties` | json (nullable) | Data tambahan (nullable) |
| `url` | string (nullable) | URL tempat action berlaku |
| `ip_address` | string(45) (nullable) | IP pengguna (support IPv6) |
| `user_agent` | text (nullable) | Browser/device info |
| `created_at` | timestamp | Bila log direkod |
| `updated_at` | timestamp | Bila log dikemaskini |
**Indexes:**
- `log_name` (index)
- `subject_type + subject_id` (morph index)
- `causer_type + causer_id` (morph index)
- `ip_address` (index)
- `created_at` (index)
---
## 3. Aliran Data (Data Flow)
```
┌────────────────────────────────────────────────────────────────────────┐
│ AUTO LOGGING (Spatie) │
│ │
│ BackendUser model ──LogsActivity trait──> Spatie Activitylog │
│ FrontendUser model ──LogsActivity trait──> Spatie Activitylog │
│ │
│ Setiap kali model di-create/update/delete, Spatie auto-rekod: │
│ - subject_type, subject_id (model & record ID) │
│ - event (created/updated/deleted) │
│ - description (default: "This model has been {event}") │
│ - attribute_changes (field yang berubah) │
└────────────────────────────────┬──────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ ACTIVITY OBSERVER │
│ │
│ ActivityObserver@creating() dipanggil SEBELUM activity disimpan: │
│ - Set url (current request URL) │
│ - Set ip_address (client IP) │
│ - Set user_agent (browser) │
│ - Determine causer berdasarkan auth guard (admin/user) │
│ - Set log_name berdasarkan causer_type │
└────────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ activity_log table │
└─────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ DISPLAY DI BROWSER │
│ │
│ BackendHomeController -> index.blade.php │
│ - Search by: log_name, description, event │
│ - Paginate 10 records per page │
│ - Columns: No, Log Name, Performed By, Model Name, │
│ Action, IP Address, User Agent, Timestamp │
│ - Action badges: created=green, updated=orange, deleted=red │
└────────────────────────────────────────────────────────────────────────┘
```
---
## 4. Penjelasan Setiap File
### 4.1 Package: `spatie/laravel-activitylog` (composer.json)
```
"spatie/laravel-activitylog": "^5.0"
```
Package ni yang handle:
- Auto record activity bila model diubah
- Polymorphic relationship (`subject`, `causer`)
- Batch cleanup log lama
### 4.2 Config: `config/activitylog.php`
```php
return [
'enabled' => env('ACTIVITYLOG_ENABLED', true),
'clean_after_days' => 365, // Auto delete log lebih 1 tahun
'default_log_name' => 'default',
'activity_model' => Activity::class,
'buffer' => [
'enabled' => env('ACTIVITYLOG_BUFFER_ENABLED', false),
],
];
```
- Boleh disable total guna `ACTIVITYLOG_ENABLED=false` dalam `.env`
- Log lebih 365 hari akan di-clean (via artisan command)
- Buffer mode disabled (log terus ke DB)
### 4.3 Migration: `2026_04_22_220206_create_activity_log_table.php`
Migration standard Spatie dengan custom columns tambahan:
```php
$table->string('url')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->json('attribute_changes')->nullable();
$table->index('ip_address');
$table->index('created_at');
```
**Custom columns** (`url`, `ip_address`, `user_agent`) tak ada dalam Spatie default — ditambah khas untuk tracking detail request.
### 4.4 Observer: `app/Observers/ActivityObserver.php`
**Registered in** `AppServiceProvider@boot`:
```php
Activity::observe(ActivityObserver::class);
```
**Function `creating(Activity $activity)`:**
Observer ni auto-populate data sebelum activity disimpan:
```
Step 1: Skip jika runningInConsole() — log dari cron/queue tak perlu URL/IP
Step 2: Set url, ip_address, user_agent dari current request
Step 3: Set causer jika belum ada:
├─ check auth:admin guard → set BackendUser
├─ check auth:user guard → set FrontendUser
└─ fallback → guna subject sebagai causer
Step 4: Set log_name berdasarkan causer_type:
├─ BackendUser → "Backend User"
└─ FrontendUser → "Frontend User"
```
**Kenapa observer penting?**
- Spatie default TIDAK rekod URL, IP, User-Agent
- Observer ni tambah detail tu secara automatik
- Juga ensure log_name konsisten (Backend User / Frontend User)
### 4.5 Model: Tidak Ada Model Khas
Module ni **tidak** guna model custom. Terus guna `Spatie\Activitylog\Models\Activity`.
```php
use Spatie\Activitylog\Models\Activity;
```
**Eloquent relationships available:**
- `$activity->subject` — polymorphic: model yang kena action
- `$activity->causer` — polymorphic: user yang buat action
- `$activity->causer()->withoutGlobalScopes()->first()` — kalau subject dah soft-deleted
### 4.6 Model dengan LogsActivity Trait
Dua model guna `LogsActivity` trait:
#### `BackendUser`
```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class BackendUser extends Authenticatable
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['username', 'email', 'first_name', 'last_name', 'profile_picture', 'is_active'])
->useLogName('Backend User')
->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
->dontLogEmptyChanges();
}
}
```
#### `FrontendUser`
```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;
class FrontendUser extends Authenticatable
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['username', 'email', 'first_name', 'last_name', 'profile_picture', 'is_active'])
->useLogName('Backend User') // Note: guna 'Backend User' juga!
->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
->dontLogEmptyChanges();
}
}
```
**Apa yang di-log:**
- Hanya field yang listed dalam `logOnly()` — bukan semua field
- Empty changes tak di-log (`dontLogEmptyChanges`)
- Description generic: "This model has been created/updated/deleted"
### 4.7 Controller: `app/Http/Controllers/Backend/ActivityLogController.php`
```php
class ActivityLogController extends Controller
{
public function index(Request $request)
{
$search = $request->get('search');
$activities = Activity::with('causer')
->when($search, function ($q) use ($search) {
$q->where(function ($sub) use ($search) {
$sub->where('log_name', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%")
->orWhere('event', 'like', "%{$search}%");
});
})
->latest()
->paginate(10)->onEachSide(1)->withQueryString();
return view('backend.module.activityLog.index', [
'activities' => $activities,
'search' => $search,
]);
}
}
```
**Fungsi:**
- `index()` — Paginated list (10/page) dengan search on `log_name`, `description`, `event`
- Eager load `causer` relationship untuk display nama user
- Order by latest (descending)
- Ada commented-out `indexApi()` untuk JSON endpoint
**Route:**
```php
Route::get('/activity-log', [ActivityLogController::class, 'index'])
->middleware('permission:activity-log.view,admin')
->name('activity-log.index');
```
### 4.8 View: `resources/views/backend/module/activityLog/index.blade.php`
**Table columns:**
| Column | Source | Format |
|--------|--------|--------|
| No | `$loop->iteration` | Number |
| Log Name | `$activity->log_name` | String |
| Performed By | `$activity->causer` | Username + email / "User #ID (Deleted)" / "System/Automated" |
| Model Name | `$activity->causer_type` | Class name (short) |
| Action | `$activity->event` | Badge: green=created, orange=updated, red=deleted, blue=other |
| IP Address | `$activity->ip_address` | Monospace font |
| User Agent | `$activity->user_agent` | Truncated 50 chars with tooltip |
| Timestamp | `$activity->created_at` | `h:i A` + `d-m-Y` |
**Features:**
- **Search** — debounce 400ms, auto-submit, search across log_name/description/event
- **Pagination** — `vendor.pagination.custom` dengan `onEachSide(1)`
- **Empty state** — icon + "No activity logs found" message
- **Causer fallback** — handle deleted user atau system action
### 4.9 Seeders
#### `BackendMenuSeeder.php`
- Create permission: `activity-log.view`
- Add route `activity-log.index` to Backend menu
- Create menu item "Activity Log" dengan icon `fa-clock-rotate-left`
#### `AdminRolePermissionSeeder.php`
- Assign `activity-log.view` to `editor` dan `viewer` roles
#### `BackendUserSeeder.php` & `FrontendUserSeeder.php`
- Disable auto-logging semasa seeder: `activity()->disableLogging()`
- Manual create satu log entry dengan event `'seeded'`
### 4.10 Permissions
| Permission | Guard | Routes Protected |
|------------|-------|-----------------|
| `activity-log.view` | admin | `activity-log.index` |
- Permission ni **protected** — tak boleh delete dari UI (dalam `PermissionController`)
- Auto-generated dari route prefix `activity-log` → module `ActivityLog` (dalam `AutoPermissionController`)
---
## 5. Cara Guna
### 5.1 Auto Logging (Models)
Untuk enable auto logging kat mana-mana model baru:
```php
use Spatie\Activitylog\Models\Concerns\LogsActivity;
use Spatie\Activitylog\Support\LogOptions;
class YourModel extends Model
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['field1', 'field2'])
->useLogName('Your Module')
->setDescriptionForEvent(fn(string $eventName) => "This model has been {$eventName}")
->dontLogEmptyChanges();
}
}
```
### 5.2 Manual Logging
Guna dalam controller/command:
```php
use Spatie\Activitylog\Models\Activity;
// Disable auto-logging
activity()->disableLogging();
// Manual log
activity()
->performedOn($model)
->causedBy($user)
->withProperties(['key' => 'value'])
->event('custom_event')
->log('Manual description');
```
### 5.3 Query Activities
```php
use Spatie\Activitylog\Models\Activity;
// Get recent 50 logs
$logs = Activity::with('causer')->latest()->take(50)->get();
// Filter by event
$creates = Activity::where('event', 'created')->get();
// By user
$userLogs = Activity::where('causer_type', BackendUser::class)
->where('causer_id', $userId)
->get();
// By subject model
$modelLogs = Activity::where('subject_type', YourModel::class)
->where('subject_id', $recordId)
->get();
```
### 5.4 Clean Old Logs
```bash
# Manual clean log lebih 365 hari (ikut config)
php artisan activitylog:clean
```
---
## 6. Hubungan Antara Komponen
```
┌──────────────┐ ┌──────────────────┐ ┌─────────────────────────┐
│ Routes │────>│ Controllers │────>│ Views │
│ web.php │ │ │ │ │
│ │ │ ActivityLogCtrl │ │ activityLog/index.blade │
│ /activity-log│────>│ → index() │────>│ ├─ Search │
│ │ │ → with('causer')│ │ ├─ Table + Badges │
│ │ │ → paginate(10) │ │ └─ Pagination │
└──────────────┘ └──────────────────┘ └─────────────────────────┘
│
▼
┌──────────────────┐
│ Spatie\Model │
│ Activity (no custom model) │
└──────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Database: activity_log │
│ ├─ Auto populated by LogsActivity trait │
│ │ (BackendUser, FrontendUser) │
│ ├─ Observer enrich with URL/IP/UserAgent │
│ └─ Causer determined by auth guard │
└──────────────────────────────────────────────┘
```
---
## 7. Tips & Troubleshooting
### Log tak direkod
- Check `.env`: `ACTIVITYLOG_ENABLED=true`
- Confirm model guna `LogsActivity` trait
- Confirm `getActivitylogOptions()` defined
- Check observer tak skip (runningInConsole?)
### Causer sentiasa null
- Observer check middleware: `auth:admin` atau `auth:user`
- Route mesti ada middleware auth yang sesuai
- Kalau console/queue, causer tak auto-set
### IP/URL/User-Agent kosong
- Observer mungkin tak register — check `AppServiceProvider@boot`
- Observer skip jika `runningInConsole()` — log dari artisan takde request context
### Log terlalu banyak
- Adjust `clean_after_days` dalam `config/activitylog.php`
- Jalankan `php artisan activitylog:clean` secara berkala (cron)
- Limit `logOnly()` dalam `getActivitylogOptions()` — jangan log semua field
### User deleted — nama tak muncul
- View ada fallback: "User #ID (Deleted)" jika causer record dah dipadam
- Guna `$activity->causer()->withoutGlobalScopes()->first()` untuk access deleted user
### Custom log_name tak set
- Observer set `log_name` berdasarkan `causer_type`
- Kalau nak guna `log_name` berbeza, set manually dalam observer atau override