Tự động hóa Activity Logs & Notifications không làm rác Controller
Trong các hệ thống lớn, khi một sản phẩm đột nhiên biến mất hoặc một giao dịch bị thay đổi số tiền, nếu bạn không có Activity Logs (Nhật ký hoạt động), các phòng ban sẽ bắt đầu đổ lỗi cho nhau, và dev là người "lãnh đủ" vì phải cắm mặt vào truy vết Database rác.
Rất nhiều anh em giải quyết việc này bằng cách rải các câu lệnh Log::info("User A vừa xóa sản phẩm") đi khắp các Controller. Cách này làm rác code, khó query, và cực kỳ dễ bỏ sót.
Hôm nay, chúng ta sẽ xây dựng một module Tracking tự động bằng Trait/Observer và tích hợp Laravel Database Notifications để tạo ra một hệ thống "Mắt thần" giám sát mọi nhất cử nhất động trong hậu trường.
Lời mở đầu: Nguyên lý "Vô hình" (Invisible Tracking)
Một hệ thống Activity Log tốt là một hệ thống mà các developer khác trong team không cần phải nhớ để gọi nó. Bất cứ khi nào họ tạo, sửa, hoặc xóa một Model (Ví dụ: Product, Order), hệ thống phải tự động âm thầm ghi lại: Ai làm? Vào lúc nào? Dữ liệu cũ là gì? Dữ liệu mới là gì?
Chúng ta sẽ giải quyết bài toán này bằng cách viết một Trait tùy chỉnh để móc (hook) thẳng vào các sự kiện của Eloquent Model.
Bước 1: Khởi tạo và Chuẩn bị Database
Tạo dự án mới:
laravel new enterprise-logging
cd enterprise-logging
1. Tạo bảng Activity Logs:
php artisan make:model ActivityLog -m
Thiết kế bảng lưu log cực kỳ chi tiết, áp dụng quan hệ đa hình (morphs) để biết Model nào bị tác động:
// database/migrations/xxxx_create_activity_logs_table.php
public function up(): void
{
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); // Ai làm? (Null nếu do hệ thống chạy ngầm)
$table->string('action'); // created, updated, deleted
// Lưu thông tin thực thể bị tác động (Ví dụ: Product ID = 5)
$table->morphs('loggable');
// Lưu snapshot dữ liệu để đối chiếu (Chỉ cần thiết khi UPDATE hoặc DELETE)
$table->json('old_payload')->nullable();
$table->json('new_payload')->nullable();
$table->ipAddress('ip_address')->nullable(); // Dấu vết IP
$table->timestamps();
});
}
2. Tạo bảng Notifications (Mặc định của Laravel):
Laravel đã có sẵn bộ khung cho thông báo. Bạn chỉ cần chạy lệnh:
php artisan notifications:table
Chạy migration để khởi tạo các bảng:
php artisan migrate
Bước 2: Chế tạo "Vũ khí" tự động Tracking (Loggable Trait)
Thay vì viết code log ở mọi nơi, ta tạo một Trait. Bất kỳ Model nào use Trait này đều sẽ bị theo dõi tự động.
Tạo thư mục app/Traits và thêm file Loggable.php:
// app/Traits/Loggable.php
namespace App\Traits;
use App\Models\ActivityLog;
trait Loggable
{
/**
* Tự động hook vào các sự kiện vòng đời của Model
*/
public static function bootLoggable()
{
static::created(function ($model) {
self::recordLog($model, 'created');
});
static::updated(function ($model) {
self::recordLog($model, 'updated');
});
static::deleted(function ($model) {
self::recordLog($model, 'deleted');
});
}
/**
* Hàm xử lý ghi log xuống Database
*/
protected static function recordLog($model, string $action)
{
// Lấy dữ liệu thay đổi
$oldPayload = null;
$newPayload = null;
if ($action === 'updated') {
// Lấy những field bị thay đổi (trước khi sửa)
$oldPayload = array_intersect_key($model->getOriginal(), $model->getChanges());
// Lấy những field bị thay đổi (sau khi sửa)
$newPayload = $model->getChanges();
} elseif ($action === 'deleted') {
$oldPayload = $model->toArray(); // Lưu lại di chúc trước khi bay màu
} elseif ($action === 'created') {
$newPayload = $model->toArray();
}
ActivityLog::create([
'user_id' => auth()->id(), // Người đang đăng nhập thực hiện thao tác
'action' => $action,
'loggable_type' => get_class($model),
'loggable_id' => $model->getKey(),
'old_payload' => $oldPayload,
'new_payload' => $newPayload,
'ip_address' => request()->ip(),
]);
}
}
Thử nghiệm gắn vào một Model (Ví dụ: Order):
php artisan make:model Order -m
(Trong migration của Order, tạo vài cột như code, total_amount, status).
Gắn Mắt thần vào Model Order:
// app/Models/Order.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\Loggable; // Kéo vũ khí vào
class Order extends Model
{
use Loggable; // TỪ NAY TRỞ ĐI, MỌI THAY ĐỔI CỦA ORDER ĐỀU BỊ GHI LẠI!
protected $fillable = ['code', 'total_amount', 'status'];
}
Bước 3: Cấu hình Notification (Cảnh báo khẩn cấp)
Log là để tra cứu thụ động, còn Notification là để báo động chủ động. Giả sử có một Đơn hàng mới giá trị lớn, ta muốn bắn thông báo ngay cho Super Admin.
Tạo class Notification:
php artisan make:notification SystemAlertNotification
Sửa lại file Notification để lưu vào Database:
// app/Notifications/SystemAlertNotification.php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class SystemAlertNotification extends Notification
{
use Queueable;
private string $title;
private string $message;
private string $level;
public function __construct(string $title, string $message, string $level = 'info')
{
$this->title = $title;
$this->message = $message;
$this->level = $level; // info, warning, critical
}
// Chọn kênh lưu trữ (Lưu vào bảng notifications)
public function via(object $notifiable): array
{
return ['database'];
}
// Cấu trúc dữ liệu lưu xuống
public function toArray(object $notifiable): array
{
return [
'title' => $this->title,
'message' => $this->message,
'level' => $this->level,
'timestamp' => now()->toDateTimeString()
];
}
}
Bước 4: Viết Controller điều phối và API Admin
Tạo Controller để Admin có thể xem Logs và Notifications.
php artisan make:controller Api/AdminSystemController
// app/Http/Controllers/Api/AdminSystemController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\ActivityLog;
use App\Models\Order;
use App\Models\User;
use App\Notifications\SystemAlertNotification;
class AdminSystemController extends Controller
{
/**
* API: Giả lập việc tạo và sửa Đơn hàng (Trigger Log & Alert)
*/
public function triggerAction(Request $request)
{
// 1. Tạo đơn hàng (Sẽ tự động sinh Log 'created')
$order = Order::create([
'code' => 'ORD-' . rand(1000, 9999),
'total_amount' => 50000000,
'status' => 'pending'
]);
// 2. Thay đổi trạng thái (Sẽ tự động sinh Log 'updated' và chỉ lưu những field thay đổi)
$order->update(['status' => 'cancelled']);
// 3. Bắn thông báo khẩn cho toàn bộ Admin (Giả sử User ID = 1 là Admin)
$admin = User::find(1);
if ($admin) {
$admin->notify(new SystemAlertNotification(
'Cảnh báo hủy đơn!',
"Đơn hàng {$order->code} trị giá 50 triệu vừa bị hủy.",
'critical'
));
}
return response()->json(['message' => 'Đã giả lập thao tác thành công. Hãy kiểm tra Log và Notification!']);
}
/**
* API: Xem lịch sử hoạt động
*/
public function getLogs()
{
$logs = ActivityLog::latest()->take(10)->get();
return response()->json(['data' => $logs]);
}
/**
* API: Xem thông báo chưa đọc của Admin
*/
public function getUnreadNotifications(Request $request)
{
$notifications = $request->user()->unreadNotifications;
// Đánh dấu đã đọc
$request->user()->unreadNotifications->markAsRead();
return response()->json(['data' => $notifications]);
}
}
Khai báo Routes (routes/api.php):
use App\Http\Controllers\Api\AdminSystemController;
Route::middleware('auth:sanctum')->group(function () {
Route::post('/admin/trigger-test', [AdminSystemController::class, 'triggerAction']);
Route::get('/admin/logs', [AdminSystemController::class, 'getLogs']);
Route::get('/admin/notifications', [AdminSystemController::class, 'getUnreadNotifications']);
});
Bước 5: Thử lửa Postman - Mọi thứ hiện hình
(Chuẩn bị: Dùng Tinker tạo 1 User để lấy Sanctum Token test nhé. Mình giả định bạn đã có Token của Admin ID 1).
Khởi động server: php artisan serve
1. Kích hoạt thao tác ngầm
- Method: POST
- URL: http://127.0.0.1:8000/api/admin/trigger-test
- Headers: Authorization: Bearer {token}
- Kết quả: Nhận chuỗi báo thành công. Mọi phép thuật đang diễn ra dưới DB.
2. Truy vết Activity Logs (Bằng chứng không thể chối cãi)
- Method: GET
- URL: http://127.0.0.1:8000/api/admin/logs
- Kết quả:
{
"data": [
{
"id": 2,
"user_id": 1,
"action": "updated",
"loggable_type": "App\\Models\\Order",
"loggable_id": 1,
"old_payload": {
"status": "pending"
},
"new_payload": {
"status": "cancelled"
},
"ip_address": "127.0.0.1",
"created_at": "2026-05-07T08:30:01.000000Z"
},
{
"id": 1,
"user_id": 1,
"action": "created",
"loggable_type": "App\\Models\\Order",
"loggable_id": 1,
"old_payload": null,
"new_payload": {
"code": "ORD-4829",
"total_amount": 50000000,
"status": "pending",
"id": 1
},
"ip_address": "127.0.0.1",
"created_at": "2026-05-07T08:30:00.000000Z"
}
]
}
Nhìn vào log id: 2, Super Admin biết chính xác User ID 1 vừa đổi trạng thái đơn hàng từ pending sang cancelled! Không một kẽ hở.
3. Kiểm tra Hộp thư Thông báo
- Method:
GET - URL:
http://127.0.0.1:8000/api/admin/notifications - Kết quả:
{
"data": [
{
"id": "abc123xx-yyyy-zzzz...",
"type": "App\\Notifications\\SystemAlertNotification",
"notifiable_type": "App\\Models\\User",
"notifiable_id": 1,
"data": {
"title": "Cảnh báo hủy đơn!",
"message": "Đơn hàng ORD-4829 trị giá 50 triệu vừa bị hủy.",
"level": "critical",
"timestamp": "2026-05-07 15:30:01"
},
"read_at": null,
"created_at": "2026-05-07T08:30:01.000000Z"
}
]
}
Sau khi gọi API này, Notification sẽ tự động được đánh dấu đã đọc (read_at cập nhật time), lần gọi GET tiếp theo nó sẽ không hiện ra nữa (vì dùng hàm unreadNotifications).
All rights reserved