Chandan Shakya's Blog

How to Handle created_by / updated_by with Laravel Traits

In any production Laravel application, tracking who created or modified records is essential for audit trails, accountability, and debugging. While Laravel automatically handles created_at and updated_at timestamps, manually managing created_by and updated_by fields across all models is tedious and error-prone.

This guide shows you how to automate audit field management using reusable traits, keeping your models clean and your code DRY.


The Problem: Manual Audit Management

Without proper automation, you’d need to:

  1. Add created_by and updated_by columns to every table
  2. Manually set these values in every controller or service
  3. Remember to include them in $fillable arrays
  4. Repeat this process for every new model

This approach is:


The Solution: Bootable Traits

Laravel’s Eloquent models automatically boot traits that follow the boot[TraitName] pattern. This allows us to create reusable audit functionality that works across all models.


Step 1: Create the Blameable Trait

This trait automatically sets created_by and updated_by fields using the authenticated user.

app/Models/Traits/Blameable.php

<?php

namespace App\Models\Traits;

use Illuminate\Support\Facades\Auth;

trait Blameable
{
    protected static function bootBlameable()
    {
        // Set created_by and updated_by on create
        static::creating(function ($model) {
            if (Auth::check()) {
                $model->created_by = Auth::id();
                $model->updated_by = Auth::id();
            }
        });

        // Set updated_by on update
        static::updating(function ($model) {
            if (Auth::check()) {
                $model->updated_by = Auth::id();
            }
        });
    }
}

How It Works


Step 2: Create a DateTime Casts Trait

To avoid repeating timestamp casts in every model, create a trait that automatically adds them.

app/Models/Traits/HasDateTimeCasts.php

<?php

namespace App\Models\Traits;

trait HasDateTimeCasts
{
    protected function initializeHasDateTimeCasts()
    {
        $this->casts = array_merge($this->casts ?? [], [
            'created_at' => 'datetime',
            'updated_at' => 'datetime',
        ]);
    }
}

How It Works


Step 3: Update Your Model

Now apply these traits to any model that needs audit tracking.

Before (Manual Approach):

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // ❌ Problem: Must remember to include audit fields
    protected $fillable = [
        'title',
        'content',
        'category_id',
        'status',
        'created_by',  // Manual addition
        'updated_by',  // Manual addition
    ];

    // ❌ Problem: Must manually set these in controllers
    // ❌ Problem: Must manually cast timestamps
    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    // ❌ Problem: Every controller needs this logic
    public function store(Request $request)
    {
        $post = Post::create(array_merge($request->all(), [
            'created_by' => auth()->id(),
            'updated_by' => auth()->id(),
        ]));
        
        return redirect()->route('posts.index');
    }
}

After (Trait Approach):

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\Traits\Blameable;
use App\Models\Traits\HasDateTimeCasts;

class Post extends Model
{
    use Blameable, HasDateTimeCasts;

    // ✅ Clean: Only business fields in fillable
    protected $fillable = [
        'title',
        'content',
        'category_id',
        'status',
    ];

    // ✅ Automatic: Timestamps are handled by trait
    // ✅ Clean: No need to define casts for created_at/updated_at

    // Relationships remain the same
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function author()
    {
        return $this->belongsTo(User::class, 'created_by');
    }

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

Step 4: Usage in Controllers/Services

With traits in place, your business logic becomes much cleaner:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Services\PostService;

class PostController extends Controller
{
    public function store(Request $request)
    {
        // ✅ Clean: No manual audit field handling needed
        $post = Post::create($request->all());
        
        return redirect()->route('posts.index');
    }

    public function update(Request $request, Post $post)
    {
        // ✅ Clean: updated_by is handled automatically
        $post->update($request->all());
        
        return redirect()->route('posts.index');
    }
}

Step 5: Advanced Customization

Option 1: Handle Guest Users

If your app allows guest actions, modify the Blameable trait:

<?php

namespace App\Models\Traits;

use Illuminate\Support\Facades\Auth;

trait Blameable
{
    protected static function bootBlameable()
    {
        static::creating(function ($model) {
            // Set to NULL if no authenticated user
            $userId = Auth::check() ? Auth::id() : null;
            $model->created_by = $userId;
            $model->updated_by = $userId;
        });

        static::updating(function ($model) {
            $model->updated_by = Auth::check() ? Auth::id() : null;
        });
    }
}

Option 2: Add Additional Audit Fields

Extend the trait to track more information:

<?php

namespace App\Models\Traits;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;

trait Blameable
{
    protected static function bootBlameable()
    {
        static::creating(function ($model) {
            if (Auth::check()) {
                $model->created_by = Auth::id();
                $model->updated_by = Auth::id();
            }
            
            // Track creation IP
            $model->created_from_ip = Request::ip();
        });

        static::updating(function ($model) {
            if (Auth::check()) {
                $model->updated_by = Auth::id();
            }
            
            // Track last update IP
            $model->last_updated_from_ip = Request::ip();
        });
    }
}

Option 3: Conditional Auditing

Only audit specific models or under certain conditions:

<?php

namespace App\Models\Traits;

use Illuminate\Support\Facades\Auth;

trait Blameable
{
    protected static function bootBlameable()
    {
        static::creating(function ($model) {
            // Only audit if model has the trait property enabled
            if (property_exists($model, 'auditEnabled') && $model->auditEnabled === false) {
                return;
            }
            
            if (Auth::check()) {
                $model->created_by = Auth::id();
                $model->updated_by = Auth::id();
            }
        });

        static::updating(function ($model) {
            if (property_exists($model, 'auditEnabled') && $model->auditEnabled === false) {
                return;
            }
            
            if (Auth::check()) {
                $model->updated_by = Auth::id();
            }
        });
    }
}

Step 6: Database Schema

Ensure your migrations include the necessary columns:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content')->nullable();
            $table->foreignId('category_id')->constrained();
            $table->string('status')->default('draft');
            
            // Audit fields
            $table->foreignId('created_by')->nullable()->constrained('users');
            $table->foreignId('updated_by')->nullable()->constrained('users');
            
            $table->timestamps();
        });
    }
}

Step 7: Retrieving Audit Information

Get Creator and Modifier

// In your controller or view
$post = Post::with(['author', 'modifier'])->find(1);

echo $post->author->name;      // John Doe
echo $post->modifier->name;    // Jane Smith

Display in Views

<div class="post-meta">
    <span>Created by: </span>
    <span>Last updated by: </span>
    <span>Created at: </span>
    <span>Updated at: </span>
</div>

Query by Audit Information

// Find posts created by a specific user
$posts = Post::where('created_by', $userId)->get();

// Find posts last modified by a specific user
$posts = Post::where('updated_by', $userId)->get();

// Find posts modified today
$posts = Post::whereDate('updated_at', today())->get();

Benefits of This Approach

✅ DRY Principle

Write the logic once, use it everywhere. No repetition.

✅ Consistency

All models behave the same way. No surprises.

✅ Maintainability

Need to change how auditing works? Update one file, apply everywhere.

✅ Clean Models

Models only contain business logic, not boilerplate.

✅ Testability

Easy to test the trait in isolation.

✅ Scalability

Works with any number of models without additional code.


Testing Your Traits

Here’s how to test the Blameable trait:

<?php

namespace Tests\Unit\Traits;

use Tests\TestCase;
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Auth;

class BlameableTest extends TestCase
{
    public function test_sets_created_by_on_create()
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $post = Post::create([
            'title' => 'Test Post',
            'content' => 'Test content',
        ]);

        $this->assertEquals($user->id, $post->created_by);
        $this->assertEquals($user->id, $post->updated_by);
    }

    public function test_sets_updated_by_on_update()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $this->actingAs($user1);
        $post = Post::create(['title' => 'Test']);

        $this->actingAs($user2);
        $post->update(['title' => 'Updated']);

        $this->assertEquals($user1->id, $post->created_by);
        $this->assertEquals($user2->id, $post->updated_by);
    }

    public function test_handles_guest_users()
    {
        // No user logged in
        $post = Post::create(['title' => 'Guest Post']);

        $this->assertNull($post->created_by);
        $this->assertNull($post->updated_by);
    }
}

Common Pitfalls

❌ Pitfall 1: Forgetting to Add Columns

Problem: Trait expects columns that don’t exist.

Solution: Always add the columns in migrations:

$table->foreignId('created_by')->nullable()->constrained('users');
$table->foreignId('updated_by')->nullable()->constrained('users');

❌ Pitfall 2: Including Audit Fields in Fillable

Problem: Fields are in $fillable, allowing manual override.

Solution: Keep audit fields OUT of $fillable:

// ✅ CORRECT
protected $fillable = ['title', 'content', 'category_id'];

// ❌ WRONG
protected $fillable = ['title', 'content', 'created_by', 'updated_by'];

❌ Pitfall 3: Not Using Auth Facade

Problem: Trying to use $this->user in static context.

Solution: Always use Auth::check() and Auth::id() in boot methods.

❌ Pitfall 4: Forgetting About Mass Assignment

Problem: Using create() with user input that includes audit fields.

Solution: The trait handles this automatically, but keep audit fields out of fillable as a safety net.


Alternative: Using Existing Packages

If you prefer not to write your own traits, consider these packages:

1. Laravel Auditing

composer require owen-it/laravel-auditing

Full audit trail with changes tracking.

2. Laravel Userstamps

composer require mattiverse/laravel-userstamps

Similar to our Blameable trait.

3. Laravel Created By

composer require jeffersongoncalves/laravel-created-by

Handles created_by, updated_by, deleted_by, restored_by.


When NOT to Use This Approach

❌ High-Performance Applications

If you’re building a system with millions of writes per minute, the overhead of trait booting might be significant.

❌ Multi-Tenant Systems

If you need to track different users for different tenants, you’ll need more complex logic.

❌ External API Integrations

If data comes from external sources, you might want to track the source instead of a user.


Best Practices

✅ 1. Always Use Traits from the Start

Add traits to models as you create them, not as an afterthought.

✅ 2. Document Your Audit Strategy

Make sure your team knows these traits exist and how they work.

✅ 3. Consider Soft Deletes

If using soft deletes, extend the trait to track deleted_by:

static::deleting(function ($model) {
    if (Auth::check()) {
        $model->deleted_by = Auth::id();
    }
});

✅ 4. Use Database Constraints

Add foreign key constraints for data integrity:

$table->foreignId('created_by')->nullable()->constrained('users');
$table->foreignId('updated_by')->nullable()->constrained('users');

✅ 5. Index Audit Fields

For better query performance:

$table->index('created_by');
$table->index('updated_by');

Complete Example: Full Audit Trait

Here’s a comprehensive trait that handles all audit scenarios:

<?php

namespace App\Models\Traits;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;

trait FullAuditable
{
    protected static function bootFullAuditable()
    {
        // Creating
        static::creating(function ($model) {
            $userId = Auth::check() ? Auth::id() : null;
            $model->created_by = $userId;
            $model->updated_by = $userId;
            
            if (Request::has('ip')) {
                $model->created_from_ip = Request::ip();
            }
        });

        // Updating
        static::updating(function ($model) {
            $model->updated_by = Auth::check() ? Auth::id() : null;
            
            if (Request::has('ip')) {
                $model->last_updated_from_ip = Request::ip();
            }
        });

        // Deleting
        static::deleting(function ($model) {
            if (Auth::check()) {
                $model->deleted_by = Auth::id();
            }
        });

        // Restoring (for soft deletes)
        static::restoring(function ($model) {
            $model->deleted_by = null;
            $model->updated_by = Auth::check() ? Auth::id() : null;
        });
    }

    // Helper methods
    public function getCreator()
    {
        return $this->belongsTo(User::class, 'created_by');
    }

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

    public function getDeleter()
    {
        return $this->belongsTo(User::class, 'deleted_by');
    }
}

Summary

What We Built

  1. Blameable Trait: Automatically sets created_by and updated_by
  2. HasDateTimeCasts Trait: Automatically casts timestamps
  3. Integration: Clean models with minimal boilerplate

Key Takeaways

When to Use

This approach keeps your models focused on business logic while ensuring consistent, reliable audit tracking across your entire application.


Document Version: 2.0.0
Last Updated: 2026-01-13