DEV Community

Hoang Manh Cam
Hoang Manh Cam

Posted on

Building a Multilingual Activity Logging System in Laravel

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
Enter fullscreen mode Exit fullscreen mode

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');
    });
}
Enter fullscreen mode Exit fullscreen mode

Run the migration:

php artisan migrate

Enter fullscreen mode Exit fullscreen mode

Step 2: Create the ActivityLog Model

Create a model that handles activity logs:

php artisan make:model ActivityLog
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 => '',
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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',
],
Enter fullscreen mode Exit fullscreen mode

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
    ]
];
Enter fullscreen mode Exit fullscreen mode

Please update similarly for other languages.

Step 8: Create the Controller

Create a controller to display activity logs:

php artisan make:controller Admin/ActivityLogController
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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.');
}
Enter fullscreen mode Exit fullscreen mode

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.');
}
Enter fullscreen mode Exit fullscreen mode

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.');
}
Enter fullscreen mode Exit fullscreen mode

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)