🎬 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
Example content:
Schema::table('orders', function (Blueprint $table) { $table->unsignedBigInteger('created_by')->default(0); $table->unsignedBigInteger('updated_by')->default(0); });
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'); }); } } }
Get your table names with:
SHOW TABLES;
Then run:
php artisan migrate
🧩 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'); } }
✅ Step 3: Use the Trait in Models
use App\Traits\AuditTrailUpdater; class Order extends Model { use AuditTrailUpdater; }
❓ 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()); } }
Run:
php add-audit-trail-updater.php
🧾 In Blade
{{ $order->updater->name ?? 'System' }}
🚀 Wrap-Up
✅ Saves time on audit tracking
✅ Works for any number of models
✅ Clean, DRY, reusable
Top comments (2)
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?
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.