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.