rulr/laravel-authored
A small Laravel package for recording and querying authorship on your models, so you can see who created or changed a record. Simple traits and helpers keep attribution consistent across your app without extra tables or boilerplate.
rulr/laravel-authored is a small, single-purpose package: it stamps created_by and updated_by on Eloquent models from the currently authenticated user. There is no audit history — it stores the last writer, not a log.
Minimal setup:
composer require rulr/laravel-authored
1. Add the columns in your migration using the schema macro the package registers (typically via its service provider — grep src/ for Blueprint::macro to confirm the exact name; it is usually authored()):
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
$table->authored(); // adds nullable created_by / updated_by
});
For an existing table, use a separate migration with Schema::table(...).
2. Apply the trait to the model:
use Rulr\Authored\Concerns\Authored; // confirm namespace in src/
class Post extends Model
{
use Authored;
}
3. Migrate and verify:
php artisan migrate
First use case: retrofit authorship onto a handful of business models (Post, Invoice, Ticket) so support staff can see who last touched a record, without building a full activity-log system.
Where to look first: the service provider (to see the macro name, config key, and whether the user resolver is bindable), the trait itself (to see which Eloquent events it hooks — creating/updating vs. saving), and whether a config file is publishable (php artisan vendor:publish --provider="Rulr\...").
Attach the relationship for display. The trait only writes IDs; you still declare the relations yourself:
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
Eager-load them in index endpoints (Post::with('creator:id,name')) to avoid N+1s, since the trait does nothing for reads.
Bulk-create still works. Because the trait hooks model events, Post::create([...]), firstOrCreate, and updateOrCreate all get stamped correctly as long as the model goes through Eloquent. Seeding and factories are fine — they'll simply write null when no one is authenticated.
Non-HTTP contexts need an explicit user. In queued jobs, artisan commands, and scheduled tasks there is no auth session, so the columns stay null. If attribution matters there (e.g., "imported by system"), either set the resolver explicitly before the write or assign the column on the model instance yourself:
Auth::loginUsingId($importOwnerId); // crude but effective for a batch
// ... or, if the package exposes a resolver hook:
Authored::resolveUserUsing(fn () => $systemUser);
Check the trait for a static override point before reaching for Auth::loginUsingId.
Impersonation / multi-guard. If your admin panel uses a non-default guard, configure the package (config file or resolver) to read from that guard; otherwise auth()->id() returns null under the admin guard.
Testing. $this->actingAs($user) is all you need; assert on the columns directly:
$this->actingAs($user)->post('/posts', ['title' => 'Hi']);
$this->assertDatabaseHas('posts', ['created_by' => $user->id, 'updated_by' => $user->id]);
Add a second test asserting null when unauthenticated, since that's the behavior most likely to surprise you in production.
Backfilling. For existing rows, write a one-off migration or command that sets created_by from whatever proxy you have (user_id, tenant owner). Keep the columns nullable during rollout, backfill, then optionally tighten.
Mass updates bypass the trait entirely. Post::where(...)->update([...]) and DB::table('posts')->update(...) never fire model events, so updated_by goes stale. Same for ->increment(), ->delete() on a builder, and Model::query()->update(). If those paths exist in your codebase, convert them to per-model save() or accept the inconsistency and document it.
saveQuietly() / updateQuietly() skip it too — they mute events by design. Useful when you deliberately don't want to touch updated_by (e.g., a nightly scoring job), dangerous if used casually.
Nulls are the default, not an error. Console commands, queue workers, seeders, and unauthenticated API endpoints all produce null authorship. Make the columns nullable in the schema (the macro should do this) and handle nulls in your UI ("System" / "Unknown") rather than assuming a user is always present.
Guard/column configuration is the usual breakage point. Verify (a) which guard the package reads, (b) whether column names are configurable if your schema uses author_id conventions, and (c) whether a published config file is cached — run php artisan config:clear after changing it, and remember config:cache in deploy scripts.
Foreign keys on user deletion. If you add ->constrained('users') to created_by, deleting a user will either fail or cascade depending on your choice. ->nullOnDelete() is usually the right call for audit columns; the macro may not add constraints at all, in which case decide deliberately.
Guard against mass assignment. The trait should assign these attributes directly, but if the columns end up in $fillable, a malicious request body could spoof created_by. Confirm the trait uses forceFill/direct property assignment and keep the columns out of $fillable.
It is not an audit log. There's no history: update a record three times and you only know the last editor. If you need "who changed what, when, from what to what," reach for spatie/laravel-activitylog or owen-it/laravel-auditing and treat this package as a lightweight complement (fast "last touched by" queries), not a replacement.
Package maturity. At a handful of stars and zero dependents, audit the source before adopting: it's likely a few dozen lines — a trait hooking creating/updating plus a Blueprint macro. That's cheap to vendor into app/Support/ if you'd rather not depend on an unmaintained package, and vendoring lets you add the resolver override and custom column names you'll inevitably want. Pin the version in composer.json and check the changelog on upgrade, since pre-1.0 audit-column packages sometimes rename the macro.
Extension point to know: if the trait hardcodes Auth::id(), the cleanest customization is to override the trait's resolver method in your model or bind your own user resolver in a service provider. Look for a protected method like authoredUserId() in the trait — that's the seam.
How can I help you explore Laravel packages today?