🎬 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.