webrek / saga
Orchestration-based sagas for PHP: run multi-step processes and roll them back with per-step compensations when one fails. Framework-agnostic core with an optional Laravel bridge.
Requires
- php: ^8.2
Requires (Dev)
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- infection/infection: ^0.29
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
Suggests
- illuminate/contracts: To use the Laravel bridge (service provider, Eloquent journal, facade).
README
Run multi-step processes and, if one fails midway, undo the previous ones through their compensation — in reverse order. The saga pattern, for when an operation touches several services and there's no database transaction that spans them all.
A framework-independent core (plain PHP) with an optional Laravel bridge (Eloquent journal, events, facade).
use Webrek\Saga\Laravel\Facades\Saga; use Webrek\Saga\SagaContext; $resultado = Saga::for('checkout', ['order_id' => 42]) ->step('cobrar', fn (SagaContext $c) => $c->set('cargo', $pago->charge($c['order_id'])), compensate: fn (SagaContext $c) => $pago->refund($c['cargo'])) ->step('apartar', fn (SagaContext $c) => $inventario->reserve($c['order_id']), compensate: fn (SagaContext $c) => $inventario->release($c['order_id'])) ->step('enviar', fn (SagaContext $c) => $envios->create($c['order_id'])) // if this fails… ->run(); // …the inventory is released and the charge is refunded, automatically. $resultado->isCompleted(); // false $resultado->status; // SagaStatus::Compensated
Completes webrek's resilience quartet: input (idempotency) · output (outbox) · dependencies (circuit-breaker) · and coordination (this one).
Installation
composer require webrek/saga
On Laravel, the service provider is auto-discovered. Publish the journal migration:
php artisan vendor:publish --tag=saga-migrations
php artisan migrate
php artisan vendor:publish --tag=saga-config # optional
How it runs
Steps execute in order. A SagaContext (a bag of data) is passed to each
step: what one step writes, the next — and the compensations — read.
If a step throws an exception, the already completed steps are compensated in reverse order and the saga ends with one of three statuses:
| Status | Meaning |
|---|---|
Completed |
All steps ran successfully. |
Compensated |
A step failed and everything before it was undone cleanly. |
CompensationFailed |
A step failed and a compensation did too — left half-done, requires manual intervention. |
The result never swallows the original exception ($resultado->failure), and the
CompensationFailed statuses are exactly the ones worth watching ($resultado->needsAttention()).
The journal (Laravel)
Every run is recorded in the sagas table: name, status, completed steps,
context, the step that failed and compensation errors. It's your audit trail and,
above all, the way to find the sagas whose compensation failed. Each run also
dispatches an event (SagaCompleted, SagaCompensated,
SagaCompensationFailed).
Disable it with 'journal' => false in the configuration.
Without Laravel
use Webrek\Saga\{Saga, SagaRunner, SagaContext}; $saga = new Saga(new SagaRunner); // no events, no journal $resultado = $saga->for('checkout', ['order_id' => 42]) ->step('cobrar', $accion, compensate: $compensacion) ->run();
For your own events or persistence, implement Webrek\Saga\Contracts\EventDispatcher
and Webrek\Saga\Contracts\SagaStore and pass them to the SagaRunner.
Scope
This is a synchronous, in-process orchestration saga: it runs inside your request or your job. It's not a two-phase commit, and it doesn't retry steps on its own — if you need retries, combine it with Laravel's queue or with webrek/laravel-circuit-breaker inside a step. Compensations must be idempotent.
Testing
composer test
The core suite (tests/Unit) runs without a framework; the Laravel suite
(tests/Feature) exercises the Eloquent journal.
Contributing
See CONTRIBUTING.md. Run make check before opening a pull
request.
Security
Report vulnerabilities through the security advisory form, not as public issues. See SECURITY.md.
License
The MIT License (MIT). See LICENSE.