logic-replay / laravel
Record a Laravel request as a sanitized logic trace, mark where the expected result diverged, and dry-replay it locally.
Requires
- php: ^8.1
- ext-json: *
- ext-mbstring: *
- illuminate/console: ^10.48 || ^11.0 || ^12.0
- illuminate/contracts: ^10.48 || ^11.0 || ^12.0
- illuminate/database: ^10.48 || ^11.0 || ^12.0
- illuminate/events: ^10.48 || ^11.0 || ^12.0
- illuminate/http: ^10.48 || ^11.0 || ^12.0
- illuminate/queue: ^10.48 || ^11.0 || ^12.0
- illuminate/routing: ^10.48 || ^11.0 || ^12.0
- illuminate/support: ^10.48 || ^11.0 || ^12.0
- illuminate/view: ^10.48 || ^11.0 || ^12.0
Requires (Dev)
- orchestra/testbench: ^8.22 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.5 || ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-01 07:45:43 UTC
README
Record one HTTP request as a sanitized trace: route middleware, service spans, queries, jobs, and checkpoints where the expected business result did not match. Dry-replay that same request locally inside a database transaction that is rolled back.
Recording is limited to local and testing unless you change record_environments. The viewer is limited the same way. Replay never commits database writes, and it fakes HTTP, mail, notifications, and queues.
Install
composer require logic-replay/laravel
From a path checkout:
"repositories": [ { "type": "path", "url": "../laravel-logic-reply" } ]
Publish the config when you want to change the defaults:
php artisan vendor:publish --tag=logic-replay-config
The file driver writes JSON under storage/logic-replay. For the database driver, set LOGIC_REPLAY_DRIVER=database and publish the migration:
php artisan vendor:publish --tag=logic-replay-migrations php artisan migrate
The package prepends its trace middleware automatically.
Checkpoints
use LogicReplay\Facades\LogicReplay; $total = LogicReplay::span('pricing.calculate', fn () => $pricing->calculate($cart)); LogicReplay::expect('cart.grand_total', expected: 1500, actual: $total->amount);
expect() returns false when the values differ. The HTTP response can still be 200. The trace status becomes failed.
Wrap a service call that is not already inside span():
LogicReplay::call($pricing, 'calculate', [$cart]);
Inspect
GET /logic-replaylists traces.php artisan logic-replay:listphp artisan logic-replay:show {id}php artisan logic-replay:export {id} trace.jsonphp artisan logic-replay:import trace.json
Passwords, tokens, cookies, and similar keys are stored as [redacted]. Other fields, including email addresses, are kept so a local replay still has the lookup values. Add those keys to redact_keys when they should be removed. Do not commit trace files.
Dry replay
php artisan logic-replay:run {id} --dry
The command freezes time to the original request, restores a user only when LogicReplay::resolveUserUsing() is set, fakes external calls from recorded HTTP responses, and rolls the database transaction back.
LogicReplay::resolveUserUsing(fn (int|string $id) => \App\Models\User::find($id)); LogicReplay::resolveTenantUsing(fn () => auth()->user()?->tenant_id);
During replay the request has header X-Logic-Replay-Tenant and attribute logic_replay.tenant_id.
Verdicts are same_failure, fixed, new_failure, unchanged, or incomplete. Queued jobs from the original trace are not executed.
Replay runs only in replay_environments (local, testing by default).