laravel-appkit/blameable
Laravel AppKit Blameable automatically tracks who created, updated, or deleted Eloquent models, filling blame columns without extra code. Lightweight and easy to drop into existing apps for simple audit trails.
laravel-appkit/blameable stamps Eloquent models with the authenticated user on create, update, and (optionally) soft delete. v1.2.0 is a maintenance release — the only substantive change is PHP 8.4 support plus a large dependency refresh — so nothing in the day-to-day workflow changes if you're already on v1.1.x.
Minimal setup:
composer require laravel-appkit/blameablephp artisan vendor:publish --tag=blameable-configcreated_by, updated_by, deleted_by by default — nullable, indexed, foreign-keyed to users if you want referential integrity):use LaravelAppkit\Blameable\Traits\Blameable;
class Post extends Model
{
use Blameable;
}
First use case: drop the trait on your Post/Order/Invoice models, add the columns via migration, and assert in a test that actingAs($user)->post(...) sets created_by. That single test proves wiring; everything else is configuration.
Where to look first: the published config/blameable.php (column names and the user model class), then the trait source to confirm which Eloquent events it hooks. Check composer.lock after upgrading — the release bumped PHPUnit, Testbench, and Guzzle, which matters only for your own test suite if you share those constraints.
Trait per model, not global. Apply Blameable only to models that need auditing. Blanket-applying it via a base model class causes every table to require the columns, which is a migration burden you'll regret on pivot and lookup tables.
Pair with soft deletes deliberately. deleted_by is only meaningful alongside SoftDeletes. If the trait writes deleted_by but the model doesn't use soft deletes, the column stays null forever — harmless but misleading. Decide per model.
Console and queue contexts. Blameable resolves the current user from auth(). In queued jobs and Artisan commands there is no authenticated user, so columns come through null (or whatever the package falls back to). If you need real attribution in jobs, set the actor explicitly before dispatching:
auth()->setUser($user); // inside the job's handle(), or
$post->created_by = $user->id; // explicit override
Prefer the explicit override — mutating the auth guard inside a worker is a well-known source of cross-job leakage.
Backfilling existing rows. Ship a one-off command that sets created_by from a heuristic (owner column, earliest audit log entry, or a known admin) rather than leaving thousands of nulls. Run it before you start surfacing "Created by" in the UI.
Reporting layer. Because the columns are plain foreign keys, joins are trivial:
Post::with('creator:id,name')->latest()->paginate();
Add a creator() belongsTo relation on the model — the package typically ships a helper for this, but defining it yourself is a two-line change and keeps the query explicit.
API resources. Expose created_by as an ID plus a nested creator object rather than the raw column, so the frontend doesn't need a second round trip.
Testing. Use actingAs($user) for stamped paths and a separate test with no authenticated user to pin down the null behaviour you actually want. Testbench-based suites upgraded alongside this release, so if you were on a pinned Testbench 6.x, expect a routine composer update with no code changes.
Mass assignment will silently defeat you. If created_by is in $fillable, a malicious request can forge attribution. Keep the blame columns out of $fillable and let the trait write them; if you need to set them manually (backfills, imports), use forceFill() or direct property assignment inside a trusted context.
Updating vs. saving. The trait hooks creating/updating events. A model saved with no dirty attributes doesn't fire updating, so updated_by won't move on a no-op save. That's usually correct, but it surprises people writing "last touched by" reports.
Bulk updates bypass everything. Post::where(...)->update([...]) never instantiates models, so no events fire and no stamps are written. If you rely on updated_by for compliance, convert those to chunked model saves or add the column to the update payload by hand.
Config caching. After changing config/blameable.php, run php artisan config:clear in local and remember config:cache in deploy — a stale cached config pointing at the wrong user model is the most common "it stopped working" cause.
Custom user model / guards. If your admin panel authenticates against a different guard than your API, verify which one the package consults. The config usually lets you point at a user model class; the guard resolution is the part to test explicitly.
Foreign key ordering in tests. RefreshDatabase with foreign keys to users means factories must create the user before the stamped model. Use actingAs() first, or a for($user) relation, to avoid constraint violations that look like trait failures.
Nullable columns are the sane default. Making created_by non-nullable breaks seeders, imports, and every console-created record. Add a database-level index instead and enforce presence in the UI layer.
Extension points. If the package's default resolution doesn't fit (multi-tenant, impersonation, service accounts), subclass or wrap rather than editing the vendor trait — override the resolution in a small base model or bind your own resolver if the package exposes one. Check the trait source before upgrading across minors so you know what's safe to override.
On v1.2.0 specifically: no breaking changes, no deprecations, no new public API. Upgrade freely. The PHP 8.4 support in PR #128 is the only functional change — if you run CI on 8.4, this is the release that unblocks you. The rest of the changelog is Dependabot noise; don't read it as a signal of feature direction.
How can I help you explore Laravel packages today?