testmonitor/laravel-accountable
Laravel package that makes Eloquent models accountable by tracking which user created, updated, or deleted each record. Adds configurable audit relations and helpers so you can see who did what, when, and query models by responsible user.
composer require the package, then publish its config and migration: php artisan vendor:publish --provider="...UserstampsServiceProvider" (check config/userstamps.php and database/migrations/*_add_userstamps_to_*.php before running php artisan migrate).config/userstamps.php first. It controls the user model, the column names (created_by, updated_by, deleted_by by default), and whether the migration adds real foreign keys. Decide this before migrating in any shared environment.use Illuminate\Database\Eloquent\SoftDeletes;
use Vendor\Userstamps\Traits\HasUserstamps;
class Post extends Model
{
use HasUserstamps, SoftDeletes;
}
Post::create($data). created_by and updated_by are populated from the current auth user with no extra code, and deleted_by is set on delete().SoftDeletes, the deleted_by column is dead weight — either skip it in the migration or accept the unused column.php artisan tinker is the new whileActingAs helper, since it lets you exercise stamping outside a real HTTP request:whileActingAs($user, fn () => Post::create(['title' => 'Seed post']));
Let the trait do the work; don't hand-assign stamps. The normal flow is create/update/delete/restore on models with the trait. Everywhere else you start writing $model->created_by = auth()->id() you are duplicating package logic and will drift.
Attribution for non-request contexts. Console commands, seeders, imports, and jobs have no auth()->user(), so stamps are null unless you act. Use the new callback helper to scope impersonation and keep it local:
whileActingAs($importer, function () use ($rows) {
foreach ($rows as $row) {
Article::create($row);
}
});
Because it is a callback, the acting user is reverted automatically — no try/finally in your own code and no leaked state into the next operation in the same process.
Queue jobs are the biggest real-world gap. A dispatched job runs in a worker with no auth context. Either pass the acting user's ID in the job payload and wrap handle() in whileActingAs(User::find($this->actorId), ...), or leave stamps null and accept that "system" writes are unattributed. Pick one policy per app and be consistent — mixed attribution is worse than none.
Data imports and backfills. This is where 10.1.0's "don't override an already-set userstamp" matters most. If you're re-importing rows that already carry created_by, or copying records with replicate(), the existing value now wins instead of being clobbered by the current user. That makes idempotent re-imports and record duplication behave correctly out of the box, but it also means a fresh create() where you deliberately set created_by will keep your value rather than the auth user's — which is what you want for migrations that preserve history.
Reading the data. Expose relations and eager load them:
$posts = Post::with('createdBy', 'updatedBy')->latest()->paginate();
Use ->withTrashed() on the deletedBy relation, otherwise deleted users render as blank "deleted by" cells in admin screens.
Testing. In feature tests, $this->actingAs($user) covers HTTP paths. For unit-level model tests, whileActingAs($user, fn () => ...) is cleaner than juggling Auth::login()/logout() around assertions, and assertDatabaseHas('posts', ['created_by' => $user->id]) reads well.
Audit-style queries. Stamp columns are just integer FKs, so Post::where('created_by', $user->id)->count() and group-bys for "activity by user" dashboards are trivial. Add indexes on created_by/updated_by if you build those screens.
nulls. No auth context means no stamps, with no warning. The usual culprits: queue workers, scheduled commands, model factories in seeders, and php artisan migrate-time data migrations. Audit these deliberately.Model::where(...)->update(), insert(), DB::table(), and upserts skip model events entirely — no stamps. Load and save models, or accept missing attribution.created_by/updated_by are in $fillable, a crafted request can forge attribution. Keep them out of $fillable and let the package write them; if you need explicit assignment for imports, use forceFill()/setAttribute() in trusted code only.whileActingAs.users. On a non-standard user table, rename it in config before migrating. Prefer nullOnDelete semantics so deleting a user doesn't cascade-delete or block on audit rows; the default restrict will bite you when someone deletes an old account.whileActingAs scope. It's a callback, so don't return early in a way that skips the closure's completion path, and verify nested usage if you wrap an importer that itself wraps calls — nesting should restore the outer actor, but confirm in a test rather than assuming. Also remember it does not change what else sees auth()->user() unless the package swaps the resolver; if you rely on auth()->id() in your own code inside the callback, test it.created_by and asserted it got replaced will now fail; existing "re-stamp everything on save" expectations need revisiting. This is almost always the better behavior, but it is a behavior change — scan for it during upgrade.whileActingAs, non-overriding stamps) with no deprecations listed, so it should be a low-risk composer update. After upgrading, add one test asserting that an explicitly-set created_by survives a save, and one asserting whileActingAs restores the previous actor — those two tests encode everything this release changed.How can I help you explore Laravel packages today?