Activity logging is essential for tracking user actions in web applications. This article will guide you through implementing a comprehensive activity logging system in Laravel with multilingual support, from database setup to displaying logs.
Step 1: Create the Database Structure
First, create a migration for the activity logs table:
php artisan make:migration create_activity_logs_table
In the migration file, define the structure:
public function up(): void
{
Schema::create('activity_logs', function (Blueprint $table) {
$table->bigInteger('id', true)->unsigned()->comment('ID');
$table->string('record_table')->comment('Target Record Model');
$table->bigInteger('record_id')->unsigned()->comment('Target Record ID');
$table->string('action')->comment('Method');
$table->string('ip_address')->comment('IP Address');
$table->string('user_agent')->comment('User Agent');
$table->string('user_table')->comment('User Model');
$table->bigInteger('user_id')->unsigned()->nullable()->comment('User ID');
$table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
$table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP'));
$table->timestamp('deleted_at')->nullable();
// Add indexes for better performance
$table->index(['user_table', 'user_id']);
$table->index(['record_table', 'record_id']);
$table->index('action');
});
}
Run the migration:
php artisan migrate
Step 2: Create the ActivityLog Model
Create a model that handles activity logs:
php artisan make:model ActivityLog
Implement the model with dynamic description generation:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ActivityLog extends Model
{
use SoftDeletes;
protected $fillable = [
'record_table',
'record_id',
'action',
'ip_address',
'user_agent',
'user_table',
'user_id',
];
protected $appends = ['description'];
public function getDescriptionAttribute(): string
{
// The user who performed the action
$user = $this->getRecord($this->user_table, $this->user_id);
// The record on which the action was performed
$record = $this->getRecord($this->record_table, $this->record_id);
if (!$user || !$record) {
return '';
}
$action = $this->action;
$user_table = __("label.tables.$this->user_table");
$record_table = __("label.tables.$this->record_table");
// Format the description based on the action type
return match ($action) {
default => __(
"activity_log.descriptions.$action",
[
'user_table' => $user_table,
'user_name' => $user->activity_log_name,
'record_table' => $record_table,
'record_name' => $record->activity_log_name,
'record_id' => $this->record_id,
]
),
'export' => __(
"activity_log.descriptions.export",
['user_table' => $user_table, 'user_name' => $user->activity_log_name, 'record_table' => $record_table]
),
'import' => __(
"activity_log.descriptions.import",
['user_table' => $user_table, 'user_name' => $user->activity_log_name, 'record_table' => $record_table]
),
// Other custom actions
};
}
protected function getRecord($table, $id)
{
// Get model name from config
$model = config("const.models.$table");
if (class_exists($model)) {
$query = $model::query();
// Handle deleted records gracefully
return $query->find($id) ?? new class {
public string $activity_log_name;
public function __construct()
{
$this->activity_log_name = __('label.labels.deleted');
}
};
}
return null;
}
}
Step 3: Create the ActivityLogService
Create a service to handle activity log creation:
<?php
namespace App\Services;
use App\Helpers\UserHelper;
use App\Models\ActivityLog;
class ActivityLogService
{
public function save(string $recordTable, string $recordId, string $action): ActivityLog
{
$activityLog = new ActivityLog();
$activityLog->record_table = $recordTable;
$activityLog->record_id = $recordId;
$activityLog->action = $action;
$activityLog->ip_address = request()->ip() ?? 'undefined';
$activityLog->user_agent = request()->userAgent() ?? 'undefined';
$activityLog->user_table = UserHelper::getAuthTable(auth()->user());
$activityLog->user_id = auth()->id() ?? null;
$activityLog->save();
return $activityLog;
}
}
Step 4: Create a Helper for Authentication Types
Create a helper to determine the authenticated user type:
<?php
namespace App\Helpers;
class UserHelper
{
public static function getAuthTable($user = null): string
{
if (!$user) {
return '';
}
return match (get_class($user)) {
'App\Models\Admin' => 'admins',
'App\Models\User' => 'users',
'App\Models\Manager' => 'managers',
default => '',
};
}
}
Step 5: Update Your Models
Add a method to each model that should be tracked in activity logs. For example User model:
public function getActivityLogNameAttribute(): string
{
if ($this->trashed()) {
return __('deleted');
}
$name = $this->full_name;
if (Route::is('admin.*')) {
return "<a href='" . route('admin.users.edit', $this->id) . "'>" . $name . "</a>";
}
return $name;
}
Step 6: Configure Model Mappings
In config/const.php, add model and icon mappings:
'models' => [
'admins' => 'App\Models\Admin',
'users' => 'App\Models\User',
'settings' => 'App\Models\Setting',
'activity_logs' => 'App\Models\ActivityLog',
'managers' => 'App\Models\Manager',
],
'icons' => [
'admins' => 'ph-user-gear',
'users' => 'ph-users',
'settings' => 'ph-gear',
'languages' => 'ph-translate',
'activity_logs' => 'ph-scroll',
'notifications' => 'ph-bell',
'managers' => 'ph-person-simple',
],
Step 7: Create Translation Files
Create language files for activity logs:
For English (lang/en/activity_log.php
):
<?php
return [
'actions' => [
'store' => 'Create',
'update' => 'Update',
'destroy' => 'Delete',
'import' => 'Import',
'export' => 'Export',
'update_password' => 'Update Password',
// other actions
],
'descriptions' => [
'store' => ':user_table :user_name created :record_table :record_name.',
'update' => ':user_table :user_name updated :record_table :record_name.',
'destroy' => ':user_table :user_name deleted :record_table ID::record_id.',
'import' => ':user_table :user_name imported :record_table.',
'export' => ':user_table :user_name exported :record_table data.',
// other descriptions
]
];
Please update similarly for other languages.
Step 8: Create the Controller
Create a controller to display activity logs:
php artisan make:controller Admin/ActivityLogController
Implement the controller:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Services\DataTableService;
use App\Services\ActivityLogService;
use Illuminate\Http\Request;
class ActivityLogController extends Controller
{
protected DataTableService $dataTableService;
protected ActivityLogService $activityLogService;
public function __construct(DataTableService $dataTableService, ActivityLogService $activityLogService)
{
$this->dataTableService = $dataTableService;
$this->activityLogService = $activityLogService;
}
public function index(Request $request)
{
if ($request->ajax()) {
$columns = [
'id', 'record_table', 'record_id', 'action', 'ip_address',
'user_agent', 'user_table', 'user_id', 'created_at',
];
$data = ActivityLog::select($columns);
$response = $this->dataTableService->processDataTable($data, $request, $columns);
return response()->json($response);
}
return view('admin.activity_logs.index');
}
}
Step 9: Create the View
Create a view to display activity logs using DataTables:
@extends('admin.layouts.master')
@section('title')
{{ __('label.tables.activity_logs') }}
@endsection
@section('center-scripts')
<script src="{{URL::asset('assets/admin/js/vendor/tables/datatables/datatables.min.js')}}"></script>
@endsection
@section('content')
<div class="content">
<!-- Single row selection -->
<div class="card">
<table class="table datatable-selection-single" id="activity-log-table">
<thead>
<tr>
<th width="5%">{{ __('label.columns.common.id') }}</th>
<th width="15%">{{ __('label.columns.activity_logs.record') }}</th>
<th width="15%">{{ __('label.columns.activity_logs.action') }}</th>
<th width="25%">{{ __('label.columns.activity_logs.description') }}</th>
<th width="15%">{{ __('label.columns.activity_logs.ip_address') }}</th>
<th width="25%">{{ __('label.columns.activity_logs.user_agent') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
@endsection
@section('scripts')
<script>
let dataTableLanguage = @json(trans('datatable'));
let icons = @json(config('const.icons'));
let models = @json(trans('label.tables'));
let action = @json(trans('activity_log.actions'));
$(document).ready(function() {
const config = {
tableId: 'activity-log-table',
ajaxUrl: "{{ route('admin.activity_logs.index') }}",
columns: [
{'data': 'id'},
{
'data': 'record_table',
'render': function(data, type, row) {
return icons.hasOwnProperty(data) ?
`<i class="${icons[data]}"></i> ${models[data]}` : data;
},
},
{
'data': 'action',
'render': function(data, type, row) {
return action.hasOwnProperty(data) ? action[data] : data;
},
},
{'data': 'description'},
{'data': 'ip_address'},
{'data': 'user_agent'},
],
language: dataTableLanguage,
};
initializeDataTable(config);
});
</script>
@endsection
For more details on implementing DataTables in Laravel, you can refer to our previous article: Implementing Dynamic DataTables in Laravel with Server-Side Processing.
Step 10: Register Routes
Add routes for activity logs:
Route::group(['middleware' => 'can:admin:activity_logs'], function () {
Route::get('/activity_logs', [ActivityLogController::class, 'index'])
->name('activity_logs.index');
});
Step 11: Using the Activity Log Service
Here's how to use the service in controllers like UserController:
For creating records:
public function store(Request $request)
{
// Create a new user
$user = User::create($request->validated());
// Log the activity
$this->activityLogService->save(
$user->getTable(), // Table name
(string)$user->id, // Record ID
__FUNCTION__ // Action (store)
);
return redirect()->route('admin.users.index')
->with('success', 'User created successfully.');
}
For updates:
public function update(User $user, UserRequest $request)
{
$user->fill($request->all());
$user->save();
$this->activityLogService->save(
$user->getTable(),
(string)$user->id,
__FUNCTION__
);
return redirect()->back()->with('success', 'User updated successfully.');
}
For custom actions like password updates:
public function updatePassword(User $user, UserPasswordRequest $request)
{
$user->fill($request->all());
$user->save();
$this->activityLogService->save(
$user->getTable(),
(string)$user->id,
"update_password" // Custom action name
);
return redirect()->back()->with('success', 'Password updated successfully.');
}
How It Works
Saving Activity Logs:
- Each controller action calls the ActivityLogService
- The service records details like table name, record ID, action type
- Context info (IP, user agent) is automatically captured
Dynamic Description Generation:
- The
getDescriptionAttribute()
method formats descriptions based on action type - It uses translation strings with placeholders (
:user_name, :record_name
) - Models provide their own
activity_log_name
methods for custom formatting
Multilingual Support:
- Descriptions and action names come from language files
- The system automatically uses the current locale
- Format strings differ between languages for natural phrasing
DataTable Integration:
- Server-side processing handles large log volumes efficiently
- Client-side rendering applies formatting to icons and action names
- All strings come from translation files for consistency
Benefits of This Approach
- Complete Audit Trail: Track who did what, when, and from where
- Multilingual Support: Automatically adapts to the user's language
- Contextual Formatting: Links to related records when appropriate
- Graceful Handling: Works even when related records are deleted
- Clean Architecture: Separation of concerns between models, service, and controller
This implementation provides a robust, multilingual activity logging system that can easily scale with your application's needs while maintaining excellent performance.
Top comments (0)