Download the PHP package sients/compensator without Composer

On this page you can find all versions of the php package sients/compensator. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package compensator

Compensator

Latest version Tests Downloads

Lightweight, synchronous Saga pattern orchestrator for Laravel. Chain steps with execute()/compensate(), and on failure Compensator automatically rolls back every completed step in reverse order — no queues, no database, no migrations.

If you don't need durable/replayable workflows (Temporal-style, surviving worker restarts), and just need to safely unwind a sequence of side effects that happened within a single request — payment charged, partner API called, external record created — this is for that.

Requirements

PHP 8.2+ and Laravel 12 or 13.

Laravel 10 and 11 are past their security-fix end of life, so they are not supported — recent Composer versions refuse to install them at all.

Installation

Laravel auto-discovers the service provider. No config file, no vendor:publish, no migrations to run.

Usage

Name your steps

$stepName is what tells you which side effect is stranded, so it is the one thing worth getting right. A step class is named after itself, which is usually enough:

Inline steps all share one class, so name them:

Without a name an inline step falls back to its position — closure step #0 — which keeps the report unambiguous but tells you nothing about what it did.

CompensatorResult carries:

Member Meaning
$successful Every step completed.
$context The context the chain ran with — populated even on failure.
$stepResults What each completed step's execute() returned, in order.
$failureCause The exception that broke the chain, or null on success.
$compensationFailures CompensationFailure objects ($step, $stepName, $stepIndex, $exception, $attempts).
needsManualCleanup() The one to alert on — failed chain and failed rollback.
fullyCompensated() Failed chain, clean rollback. Note this is false for a successful run too, since nothing was rolled back.

One chain, one run

A Compensator is a mutable, single-use builder: every method mutates and returns the same instance (like Laravel's own Http::withHeaders()), and run() may be called only once — a second call throws LogicException rather than silently repeating side effects that may already have been compensated. Build a fresh chain per run, or resolve one from the container:

Failure strategy

By default, if one compensate() call throws, Compensator keeps rolling back the rest of the chain instead of stopping — a partial rollback is usually worse than an attempted full one. Every failure is reported on $result->compensationFailures, never swallowed silently.

Retrying a rollback

A refund API that blips for a second is otherwise the difference between a recoverable failure and money stranded for good. Retries are off by default:

That is one attempt plus two retries per step being rolled back, with a fixed pause between them. $failure->attempts and the attempts property on CompensationSucceeded/CompensationFailed tell you how many it took.

Two things to keep in mind. The retries are synchronous, inside a request that is already failing — times × sleepMs is added to the response for every step that needs retrying, so keep both numbers small. And retrying means compensate() runs more than once, so it has to be idempotent (see below).

compensate() must be idempotent

Compensator can call the same compensate() more than once: retryCompensation() does it on failure, the shutdown guard resumes a rollback the dying process had started, and whoever handles a CompensationFailure runs the same undo by hand afterwards.

So check before acting rather than assuming the effect is still in place:

Logging the context

$context->all() returns the raw values, which may include Eloquent models or gateway response objects — dropping those into a log line is how a card number ends up in your log aggregator. snapshot() gives you a version that is safe to log: scalars survive, everything else becomes a short type description, and you can mask keys outright.

The simplest defence is still to keep only scalar identifiers in the context in the first place.

Rolling back the step that failed

By default a step whose own execute() throws is not compensated — only the steps before it are. That is the right default, because most compensate() implementations read a value that execute() writes to the context on its last line.

If a step can leave a side effect behind before throwing (the charge went through, then persisting the id failed), opt in:

Then that step's compensate() must tolerate a context execute() never finished filling in:

Working with database transactions

Compensator is not a replacement for DB::transaction(), and it never opens one for you. They cover different boundaries: a transaction gives you atomicity inside one database, Compensator unwinds effects that live outside it — a charged card, a partner API record, a file on S3.

If all of your work is in one database, you do not need this package. Use a transaction.

Do not wrap a whole chain in a transaction:

Two things go wrong. The connection is held open across every external HTTP call, so network latency turns into held locks and exhausted pool connections. And on failure, the rollback discards anything your compensate() methods wrote to that same database — the refund audit row you created while unwinding disappears along with everything else, leaving you with a real refund and no record of it.

Instead, keep each step's database work atomic inside the step, and let Compensator orchestrate across the boundaries:

The trade-off is explicit: between two steps there is a window where the first step's work is committed and the second has not run. That is the Saga bargain — eventual consistency in exchange for not holding a transaction across a network call.

Observability

No persistence layer means no built-in dashboard — but every transition dispatches a Laravel event you can subscribe to and route wherever you already send logs/metrics. Inside Laravel this works with a plain new Compensator() — it picks up the application's dispatcher automatically:

Available events:

Event Properties
StepSucceeded $step, $stepName, $stepIndex, $result (whatever execute() returned)
StepFailed $step, $stepName, $stepIndex, $exception
CompensationSucceeded $step, $stepName, $stepIndex, $attempts
CompensationFailed $step, $stepName, $stepIndex, $exception, $attempts

$stepIndex is the step's zero-based position in the chain, matching $result->stepResults.

Listeners are strictly observational: an exception thrown inside one is swallowed and never changes the outcome of the chain — a listener that throws must not roll back a step that actually succeeded or, worse, prevent a rollback from running at all.

Swallowed is not the same as lost, though. If your CompensationFailed listener is the only record of a stranded side effect and it throws, the incident would disappear, so the dropped exception is passed to the application's ExceptionHandler (or error_log() when there is no container). Call withoutEvents() to silence a single chain.

Surviving a dead process

This is the trade-off behind "no database". If PHP dies between steps — an out-of-memory fatal, an exceeded max_execution_time, a worker terminated mid-deploy — none of that is catchable, so the rollback never runs and nothing is written anywhere. You are left with a charged card and no record of it.

protectAgainstFatals() installs a shutdown handler that attempts the rollback anyway:

PHP still runs shutdown handlers after a fatal, so this recovers the two most common ways a request dies. It reserves a little memory up front and frees it on entry so a rollback is still possible after an out-of-memory kill. The outcome goes to your event listeners and to ExceptionHandler/error_log, since there is no longer anyone to hand a CompensatorResult to.

It is best effort, not a guarantee. Nothing survives SIGKILL, a segfault, or the machine losing power, and a rollback running after a timeout may itself be cut short. If a stranded side effect is genuinely unacceptable — real money, anything legally binding — do not rely on this alone: write an audit row from the StepSucceeded listener, or use a durable engine instead.

If the process dies part-way through a rollback, the guard resumes it rather than restarting it: a step whose rollback already finished is not undone a second time. A step that was in the middle of being compensated when the process died is retried, which is one of the reasons compensate() has to be idempotent.

Safe under Octane, RoadRunner and Swoole: the guard installs a single shutdown handler per process and retains nothing once a run finishes, so it does not accumulate across the requests a worker serves.

One more thing worth planning for: the rollback spends the user's request budget. A chain that fails on step five then makes four more outbound calls to undo, in a request that is already slow — and retryCompensation() multiplies that. It is exactly how you reach the timeout above.

What this deliberately does not do

If you need any of those, look at Durable Workflow (durable-workflow/workflow, formerly Laravel Workflow) or Saga Lara Flow (discovery-ukraine/saga-lara-flow) instead — they solve a different problem (durable, restart-surviving execution) at the cost of a queue + database dependency.

Testing

Style and static analysis run alongside the suite — this is what CI runs:

Contributing

See SECURITY.md.

Changelog

See CHANGELOG.md.

License

MIT.


All versions of compensator with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
illuminate/contracts Version ^12.0|^13.0
illuminate/support Version ^12.0|^13.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package sients/compensator contains the following files

Loading the files please wait ...