orisintel/laravel-process-stamps
Laravel Process Stamps adds lightweight provenance to Eloquent models, automatically recording the command, job, request, or test that created or updated each record. Trace data changes, debug background workflows, and answer "where did this row come from?" with minimal setup.
Install the package
composer require orisintel/laravel-process-stamps
Publish config and migrations, then migrate
php artisan vendor:publish --provider="OrisIntel\ProcessStamps\ProcessStampsServiceProvider"
php artisan migrate
This creates the dedicated stamps table (the package deliberately keeps stamps in their own table so it does not alter your existing models' schema). Publish only the config if you don't want to touch migrations:
php artisan vendor:publish --tag=process-stamps-config
Opt in per model by adding the trait:
use OrisIntel\ProcessStamps\HasProcessStamps;
class Order extends Model
{
use HasProcessStamps;
}
The trait wires up created and updated listeners for you—no explicit calls in boot() or observers needed.
Where to look first
config/process-stamps.php — table name, column names, and any context/formatting options.src/HasProcessStamps.php — shows the relation name (typically processStamps()) and the exact events it hooks.First use case: prove it works, then surface it
Create or update a model from php artisan tinker and inspect the resulting stamp:
$order = Order::create([...]);
$order->processStamps()->latest()->first();
// type => "console", name => "tinker", id => null (or similar)
Then make it user-visible in an admin screen ("Last modified by nightly:reconcile") or in support tooling—that is the primary day-to-day payoff.
1. Per-record audit display
Read the latest stamp for any opted-in model and render it in an admin/inspector view:
$stamp = $order->processStamps()->latest('id')->first();
// $stamp->type, $stamp->name, $stamp->id, $stamp->created_at
Because it's a morphMany relation, $order->processStamps and eager loading (with('processStamps')) work as usual—so you can avoid N+1s in list views.
2. Querying by process (the "who wrote this" report)
Use it to answer operational questions without digging through logs:
// Every record stamped by a specific job
Order::whereHas('processStamps', fn ($q) => $q->where('name', 'ImportOrdersJob'))->get();
// Anything touched today by a scheduled task
ProcessStamp::where('type', 'schedule')
->whereDate('created_at', today())
->with('stampable')
->get();
This is the pattern that pays for the package: correlating a bad batch of writes back to the process that produced it.
3. Queue jobs and console commands
No integration code is required—the package resolves the current process context at write time. A job that updates 10,000 orders will stamp every row with that job's name, so a single query tells you which job is responsible for a corrupted import. Scheduled commands are captured distinctly, which is invaluable when a nightly task and a user action both mutate the same table.
4. Combining with optimistic/idempotent flows
Pair stamps with your own updateOrCreate / firstOrCreate paths. The package records the process, not user identity—so if you need "which user," keep your own updated_by column and let the package cover the process dimension (job vs. request vs. scheduler). The two are complementary, not redundant.
5. Customising process names
If you want more descriptive names than the raw class/command name (e.g. "checkout-retry" instead of the job FQCN), the config and/or trait expose a place to override the resolved name—check HasProcessStamps for the resolution hook before hand-rolling context setting.
Model::insert(), query-builder update(), and raw SQL do not fire Eloquent events, so no stamp is recorded. If a path is "important enough to trace," route it through Eloquent (save, updateOrCreate, firstOrCreate) so the created/updated events fire.->save() on an unchanged model does not fire updated, so you won't get a stamp row for it. That's usually what you want—but don't be surprised when a "save" button click produces no record.config/process-stamps.php in a cached config environment, run php artisan config:clear (and redeploy). Changed table/column names otherwise silently keep the old values.created/updated, factories and fixtures will produce them. Either assert against them intentionally or wrap setup in withoutEvents() / DB::transaction cleanup. Use RefreshDatabase so the stamps table doesn't accumulate across runs.Bus::batch, the stamp reflects the process executing the write—verify with a small test that the job name (not the dispatching request) is captured. Context can also be lost if you swallow exceptions or run code outside the queue lifecycle.created/updated via observers, model events, or other traits, double-check ordering—both will run, but one may assume the other hasn't mutated the row. Stamps are additive; they don't replace your own updated_by/created_by columns.boot method once—it's short and tells you exactly which events and relations power everything.How can I help you explore Laravel packages today?