bernskiold/laravel-blame
Laravel Blame adds automatic user attribution to Eloquent models, tracking who created, updated, or deleted each record through simple traits and relationships. Ideal for audits and accountability with minimal setup.
1.0.0 is the first stable release of bernskiold/laravel-blame, so there is no upgrade path to worry about — treat this as a greenfield install and pin "bernskiold/laravel-blame": "^1.0".
composer require bernskiold/laravel-blame
php artisan vendor:publish --tag="blame-config" # if the package ships config
php artisan vendor:publish --tag="blame-migrations"
php artisan migrate
Then opt a model in:
use Bernskiold\LaravelBlame\Traits\Blameable;
class Post extends Model
{
use Blameable;
}
Where to look first
config/blame.php — column names, guard resolution, and whether blame is enabled globally. Read this before touching a model, because the defaults decide your schema.Blameable trait itself (vendor/bernskiold/laravel-blame/src) — at a 1.0.0 the source is the documentation; the README is unlikely to cover every edge.First use case: attributing authorship on a record and surfacing it in the UI.
$post->creator; // BelongsTo User
$post->updater; // BelongsTo User
$post->deleter; // if soft deletes are tracked
// In a controller / resource
PostResource::make($post->load('creator'));
Use it for "Created by Erik · 2 hours ago" in an admin table, and for "who touched this invoice?" support tickets. It is a lightweight attribution layer, not a full audit trail — if you need field-level before/after diffs, pair it with owen-it/laravel-auditing or spatie/laravel-activitylog rather than replacing them.
Trait-per-model, not global. Add Blameable only to models where attribution is meaningful. Applying it at a base-model level drags extra belongsTo relations and two extra writes into every table in the app.
Resolve the actor explicitly in non-HTTP contexts. The most common real-world pattern is a job or command that must attribute work to a user who is not the authenticated one:
// In a queued job
Blame::use(fn () => $this->actingUser); // or the package's resolver API
Post::create([...]); // attributed to $this->actingUser
Wrap this in try/finally (or the package's withoutBlame() helper) so a long-lived queue worker never leaks one job's user into the next.
Backfill existing rows. For a live table, publish the migration but add the columns as nullable and backfill out-of-band:
DB::table('posts')->whereNull('created_by')
->update(['created_by' => $systemUserId]);
Do the backfill before you make the columns non-nullable in a follow-up migration. This keeps the deploy non-locking and reversible.
Seeders and tests. Factories will attribute to whoever is authenticated — usually nobody, which is fine if the columns are nullable. When you need deterministic data:
$this->actingAs($admin);
$post = Post::factory()->create();
// or explicitly avoid attribution noise
Post::withoutBlame(fn () => Post::factory()->count(50)->create());
Eager-load in list endpoints. Every blamed model adds one or two relations. Post::with('creator:id,name')->paginate() prevents the N+1 that appears the moment you render an author column in a table.
Reporting. Because attribution lands in real columns, ordinary SQL works — "records created per admin this month", "stale records nobody has updated":
Post::where('updated_by', $user->id)
->where('updated_at', '>', now()->subDays(30))
->count();
Consistent JSON shape. Expose a small, stable representation rather than the full user model:
'created_by' => $this->whenLoaded('creator', fn () => [
'id' => $this->creator->id,
'name' => $this->creator->name,
]),
Post::where(...)->update([...]) and DB::table('posts')->update() fire no model events, so updated_by stays stale. Bulk operations must set the column manually — this is the single most common source of wrong attribution data.$model->save() vs $model->update(). Both go through Eloquent and are covered, but increment(), touch(), and raw update() calls on the query builder are not.system user ID is usually the better default.web and admin guards, verify which one the package resolves by default and override it in config. Silently attributing admin actions to a logged-in front-end user is easy to miss in testing.NOT NULL column with an FK to a large table will take a lock and can time out mid-deploy.restrictOnDelete() will block deleting a user who ever created a record — usually not what you want. Use nullOnDelete() and render "Deleted user" in the UI.forceDelete. If deleting is tracked, confirm which path sets it and that forceDelete() behaves as expected; the audit value of deleted_by disappears if rows are hard-deleted.php artisan config:clear — a cached config is a frequent reason "the setting has no effect" in production.created_by / updated_by / deleted_by up front. Renaming later means a migration plus a data update plus touching every query that referenced them.withoutBlame() for scoped suppression, and — if the package allows it — a custom user model reference so attribution points at your own User and not the package's default.^1.0 is reasonable, but read the changelog (the release notes link only to it) before jumping minors, since a 1.x may still adjust trait method names or config keys.How can I help you explore Laravel packages today?