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:
- Add
created_byandupdated_bycolumns to every table - Manually set these values in every controller or service
- Remember to include them in
$fillablearrays - Repeat this process for every new model
This approach is:
- ❌ Error-prone: Easy to forget in some places
- ❌ Inconsistent: Different implementations across models
- ❌ Tedious: Lots of boilerplate code
- ❌ Hard to maintain: Changes require updates everywhere
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
bootBlameable(): This method is automatically called by Laravel when the model bootscreatingevent: Fires before a new record is insertedupdatingevent: Fires before an existing record is updatedAuth::check(): Ensures we only set values when a user is logged inAuth::id(): Gets the ID of the currently authenticated user
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
initializeHasDateTimeCasts(): Called automatically when a model instance is createdarray_merge(): Combines existing casts with our timestamp casts'datetime': Ensures timestamps are always returned as DateTime objects
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
- Blameable Trait: Automatically sets
created_byandupdated_by - HasDateTimeCasts Trait: Automatically casts timestamps
- Integration: Clean models with minimal boilerplate
Key Takeaways
- ✅ Use traits to keep models clean
- ✅ Never put audit fields in
$fillable - ✅ Let Laravel’s boot system handle the heavy lifting
- ✅ Test your traits thoroughly
- ✅ Document your audit strategy for your team
When to Use
- Any application needing audit trails
- Multi-user systems
- Applications requiring accountability
- Projects where data provenance matters
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