DEV Community

Talemul Islam
Talemul Islam

Posted on

🧠 Automatically Track created_by and updated_by in All Models

🎬 Background: Real Problem from a Real Project

A few days ago, a client from Upwork assigned me a task:
“I need to see who created or last updated specific tasks or orders in the system.”
So I dove into the Laravel project... and found that:
• No tables had created_by or updated_by columns.

• No model had logic to store the user ID on create/update.

• No Blade template had access to the creator or updater names.


That Raised Some Questions:

• Should I manually add these columns and save logic in every controller?

• Should I add listeners to each model manually?

• Should I create global logic in AppServiceProvider?

That’s when I stepped back and designed a better, reusable, scalable solution using traits and a helper script — saving me hours of tedious work.


🔍 What I'll Build

• Add created_by and updated_by columns via migration

• Automatically populate these fields using traits

• Make relationships accessible in Blade (e.g., $order->creator->name)

• Auto-attach the tracking trait to all models via a PHP script


🛠 Step 1: Add Audit Columns via Migration

Create the migration file:

php artisan make:migration add_audit_columns_by_to_all_tables
Enter fullscreen mode Exit fullscreen mode

Example content:

Schema::table('orders', function (Blueprint $table) {
    $table->unsignedBigInteger('created_by')->default(0);
    $table->unsignedBigInteger('updated_by')->default(0);
});
Enter fullscreen mode Exit fullscreen mode

For multiple tables, use this:

private array $tables = [
    'customers',
    'orders',
    'order_details',
];

public function up(): void
{
    foreach ($this->tables as $table) {
        if (!Schema::hasColumn($table, 'updated_by')) {
            Schema::table($table, function (Blueprint $tableBlueprint) {
                $tableBlueprint->unsignedBigInteger('updated_by')->default(0);
            });
        }
    }
    foreach ($this->tables as $table) {
        if (!Schema::hasColumn($table, 'created_by')) {
            Schema::table($table, function (Blueprint $tableBlueprint) {
                $tableBlueprint->unsignedBigInteger('created_by')->default(0);
            });
        }
    }
}

public function down(): void
{
    foreach ($this->tables as $table) {
        if (Schema::hasColumn($table, 'updated_by')) {
            Schema::table($table, function (Blueprint $tableBlueprint) {
                $tableBlueprint->dropColumn('updated_by');
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Get your table names with:

SHOW TABLES;
Enter fullscreen mode Exit fullscreen mode

Then run:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

🧩 Step 2: Create the Traits

App\Traits\AuditTrailUpdater.php

<?php
/**
 * Author: Talemul Islam
 * Website: https://talemul.com
 */

namespace App\Traits;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait AuditTrailUpdater
{
    public static function bootAuditTrailUpdater(): void
    {
        static::creating(function (Model $model) {
            try {
                if (Schema::hasColumn($model->getTable(), 'created_by')) {
                    $model->created_by = Auth::id() ?? 0;
                }
                if (Schema::hasColumn($model->getTable(), 'updated_by')) {
                    $model->updated_by = Auth::id() ?? 0;
                }
            } catch (\Exception $e) {
                Log::error('Error setting created_by for ' . get_class($model) . ': ' . $e->getMessage());
            }
        });

        static::updating(function (Model $model) {
            try {
                if (Schema::hasColumn($model->getTable(), 'updated_by')) {
                    $model->updated_by = Auth::id() ?? 0;
                }
            } catch (\Exception $e) {
                Log::error('Error setting updated_by for ' . get_class($model) . ': ' . $e->getMessage());
            }
        });
    }

    public function updater(): BelongsTo
    {
        return $this->belongsTo(User::class, 'updated_by');
    }

    public function creator(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by');
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Step 3: Use the Trait in Models

use App\Traits\AuditTrailUpdater;

class Order extends Model
{
    use AuditTrailUpdater;
}
Enter fullscreen mode Exit fullscreen mode

❓ Common Questions

Q: Will it break if a table doesn’t have the columns?

A: No, it checks with Schema::hasColumn() before setting the value.

Q: Will it handle both create and update?

A: Yes, creating and updating hooks are used.


But doing this for 50+ models manually? Painful. So…

⚙️ Step 4: Automate Trait Injection

add-audit-trail-updater.php:

<?php
/**
 * Author: Talemul Islam
 * Website: https://talemul.com
 */

$directory = __DIR__ . '/app/Models';

function addTraitToModel($filePath)
{
    $content = file_get_contents($filePath);

    if (strpos($content, 'use AuditTrailUpdater;') !== false) return;

    if (strpos($content, 'use Illuminate\Database\Eloquent\Model;') !== false) {
        $content = str_replace(
            'use Illuminate\Database\Eloquent\Model;',
            "use Illuminate\Database\Eloquent\Model;\nuse App\\Traits\\AuditTrailUpdater;",
            $content
        );
    }

    $content = preg_replace(
        '/class\s+\w+\s+extends\s+Model\s*{/',
        "$0\n    use AuditTrailUpdater;",
        $content
    );

    file_put_contents($filePath, $content);
    echo "Updated: $filePath\n";
}

$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));

foreach ($rii as $file) {
    if ($file->isFile() && $file->getExtension() === 'php') {
        addTraitToModel($file->getPathname());
    }
}
Enter fullscreen mode Exit fullscreen mode

Run:

php add-audit-trail-updater.php
Enter fullscreen mode Exit fullscreen mode

🧾 In Blade

{{ $order->updater->name ?? 'System' }}
Enter fullscreen mode Exit fullscreen mode

🚀 Wrap-Up

✅ Saves time on audit tracking

✅ Works for any number of models

✅ Clean, DRY, reusable

Top comments (2)

Collapse
 
dotallio profile image
Dotallio •

This kind of automation is a huge time-saver, especially when scaling up.
Did you run into any tricky edge cases while applying it across different models?

Collapse
 
talemul profile image
Talemul Islam •

Absolutely! Automating this saved me hours of repetitive work — especially with over 50+ models to update.

The main one was dealing with models that didn't have the created_by or updated_by columns, which could break mass operations. I handled that by wrapping the logic with Schema::hasColumn() checks to ensure safety.

Another subtle challenge was ensuring the trait's boot method didn’t conflict with other model events, but Laravel handles bootable traits nicely when structured properly.

Lastly, I had to sanitize the script to avoid injecting the trait multiple times if rerun — a simple string check (strpos) solved that.

Thanks for reading! Let me know if you've faced any similar issues or have suggestions for improvement.