| title | Packages |
|---|---|
| weight | 5 |
Durable consists of a core library, an optional framework integration, and a choice of backend. The backend is where an execution's journal lives and what schedules its work; the journal is the append-only record of every step an execution took (see the glossary). Install what you need: the library alone is enough to write and unit-test a workflow, and the packages above it change only where the execution is recorded, never the workflow code.
| Package | Brings | Needs |
|---|---|---|
gplanchat/durable |
workflows, activities, timers, event journal, in-memory backend | psr/cache |
gplanchat/durable-bundle |
Symfony wiring, Messenger transports, profiler panel | the library and Symfony Messenger |
gplanchat/durable-bridge-temporal |
the Temporal driver, over gRPC | the library, ext-grpc, a Temporal cluster |
gplanchat/durable-bridge-dbal |
durable execution on one SQL database | the library, Doctrine DBAL 3 or 4, symfony/lock |
gplanchat/durable-bridge-illuminate |
the same, through Laravel's database layer | the library, illuminate/database 11, 12 or 13 |
gplanchat/durable-laravel |
the Laravel wiring: ports bound from config, work on the application's queue | the library, illuminate/support; the Illuminate or Temporal bridge for the backend you select |
gplanchat/durable-magento |
a Magento 2.4 / Mage-OS module: declaration, workers, admin screen | the library; Temporal for anything that must outlive a process |
gplanchat/durable-plugin |
a Sylius admin dashboard for workflow runs | the bundle, knplabs/knp-menu; Sylius 2.x to appear in its menu |
gplanchat/durable-filament |
a Filament panel dashboard for workflow runs | the Laravel integration, Filament 3 or 4 |
gplanchat/durable-phpstan |
static analysis of stub calls against their contract | the library, phpstan/phpstan |
gplanchat/durable-rector |
automated migration off the Temporal PHP SDK | the library, rector/rector |
A workflow is the PHP class that describes an execution's steps, and an activity is a unit of side effect that a workflow calls, such as an HTTP call or a database write.
The three bridges are alternatives and do not stack: you install Temporal, DBAL or Illuminate, never two of them.
The last two packages are development-time tools and belong in require-dev:
gplanchat/durable-phpstanresolvesactivityStub()andchildWorkflowStub()calls against the contract interface. A mistyped activity or a wrong argument then shows up as an analysis error instead of a serialization failure at runtime. It also checks that an#[Activities]parameter and its@param ActivityStub<Contract>docblock name the same contract.gplanchat/durable-rectormigrates a project off the official Temporal PHP SDK. It rewrites the attributes and changes the execution model, and it keeps the workflow and activity type names that a running server already has on record. It leaves a comment on each construct it cannot convert, so you see them before you start. See the comparison page.
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durableThe library contains the engine and the whole domain: WorkflowEnvironment, activities, timers,
side effects, signals, queries, updates, child workflows, the event journal, and the value objects
that describe scheduling options.
It has one runtime dependency, psr/cache, and the cache pool itself is optional: it memoises
activity contract resolution, and ActivityContractResolver works without one. The library needs
no framework. You can drive it from a plain PHP script, a Laminas application, a console tool,
or a test.
It includes an in-memory backend that runs everything in one process. Your unit tests use it, and it needs nothing else installed.
Note
The in-memory backend keeps no state between processes. Use it for tests and local exploration; a workflow that has to survive a deploy needs another backend. See Backends.
gplanchat/durable-bundle, the Symfony integration {#gplanchatdurable-bundle--the-symfony-integration}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bundleThe bundle does the following, which you would otherwise write by hand:
- Autoconfiguration. The bundle registers every class carrying
#[AsWorkflow]or#[AsActivityHandler]. You neither list these classes in a container file nor tag them.#[AsActivity]names the contract and registers nothing. - Messenger wiring. Workflow resumes and activity dispatches go to the transports you name in
durable.yaml, so a workflow that suspends resumes through your existing queues. - One console command.
durable:execution:diagnose <executionId>prints what the engine holds for one run: its workflow metadata, its parent/child links and its event journal. The bundle adds no worker command; the worker is Messenger's ownmessenger:consumeon the transports above. - Profiler panel. In the Symfony toolbar, the panel shows each execution, its journal, and the timeline of its activities, including which attempt failed and why.
All configuration lives in one file, documented key by key in the configuration reference.
gplanchat/durable-bridge-temporal, the Temporal driver {#gplanchatdurable-bridge-temporal--the-temporal-driver}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bridge-temporalThe bridge talks to a Temporal cluster directly over gRPC. The dependency tree contains neither the official Temporal PHP SDK nor RoadRunner: the protobuf definitions are vendored and the workers are plain PHP processes.
Compared with the in-memory backend, it adds:
- executions that survive process restarts, deploys and crashes;
- server-side retry policies, so a failing activity is retried even if the worker is gone;
- cron schedules, search attributes, and cross-process visibility in the Temporal UI;
- a read-through event store, so the profiler shows a real execution's history.
It requires ext-grpc and a reachable cluster. For local work, one command starts a development
server:
temporal server start-dev --namespace durable-test --port 7233Note
Cron schedules and search attributes are Temporal capabilities with no in-process equivalent. The
journal backends do not run them. A child workflow's cronSchedule fails with
UnsupportedByBackendException there, and so do its namespace and taskQueue. Its search
attributes are written into the journal and nothing acts on them, and the start options of a root
workflow exist only on the Temporal client.
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bridge-dbalThe bridge provides durable execution on one SQL database, without an orchestration cluster or
ext-grpc. DUR030
records the decision behind it.
The bridge leaves the replay interpreter, the workflow ports and the command buffer unchanged.
Replay is how an execution resumes: the workflow code runs again from its first line, and each
recorded step returns its result from the journal. The bridge only makes four process-local stores
persistent: the event journal, the workflow metadata, the parent links between child workflows, and
the run catalog, the list of executions a dashboard reads. Two classes share the run catalog's
table: DbalWorkflowRunProjection writes it and DbalWorkflowRunCatalog reads it. Workflow and
activity code is byte-for-byte what runs on Temporal or in memory.
| Kept | Given up, compared with Temporal |
|---|---|
Workflow classes, activities, WorkflowEnvironment |
Distributed task queues; resumes go through Symfony Messenger |
| Signals, queries, updates | Server-side scheduling; timers use Messenger's DelayStamp |
| Cancellation and compensation semantics | Server-side task serialisation, replaced by an application lock |
| Replay determinism and the event journal | History retention, visibility API, the Temporal UI |
Choose it when you need durability without running a cluster. It takes one database you already
back up and no extension to compile. The bridge ships no migration: DurableSchema creates the
tables on the first write, and bin/console durable:setup creates them up front (see
DBAL backend).
gplanchat/durable-bridge-illuminate, the Laravel backend {#gplanchatdurable-bridge-illuminate--the-laravel-backend}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable gplanchat/durable-bridge-illuminate
php artisan migrateThis bridge provides the same four stores as the DBAL bridge, with the same trade-offs against
Temporal: the table above applies here word for word. One class, IlluminateWorkflowRunCatalog,
both writes and reads the run catalog. The connection differs. These stores use
Illuminate\Database\Connection and its query builder, without Eloquent.
Give the stores their own connection in config/database.php, separate from the application's
default one (DUR054). On a shared connection, Durable's own transactions nest inside the
application's: a business rollback erases journal events, and a claim stays invisible to the other
workers until the business code commits. To handle an activity that writes and then dies, make the
activity idempotent. Never share a transaction with business code for that purpose.
The five tables ship as migrations loaded straight from the package, so migrate is enough. Each
store has its table, and the event journal also writes durable_execution_heads, a counter per
execution that stops a superseded resume from writing (DUR053). To edit them, publish them with
vendor:publish --tag=durable-migrations. The command copies the whole Migrations/ directory of
the package, five files, into database/migrations; from then on, you maintain the published
copies. Keep the published files' names. Laravel keys migrations by basename and gives
precedence to database/migrations when two names match, which makes your copy the one that runs.
If you rename a file, Laravel runs both the package's file and your copy. Only
0001_01_01_000000_create_durable_tables.php fails on the second run: its Schema::create calls
have no guard and stop on a table that already exists. The four other migrations check hasColumn,
hasIndex or hasTable first and change nothing the second time.
ResumeLock covers what no choice of storage supplies. It lives in gplanchat/durable-laravel, which uses it on every backend. When two workers resume the same
execution, both replay it, both treat the commands it produces as new, and those commands go out
twice. The journal does not prevent this, because it records whatever it receives, duplicates
included. ResumeLock takes a closure, so a queued job, an artisan command or a hand-written worker
can all use it.
Note
This bridge provides the storage only. It binds no ports and ships no worker command and no
job; DurableIlluminateServiceProvider registers only the location of the migrations.
gplanchat/durable-laravel, described in
the next section, binds them. If you install the bridge alone, you wire the stores yourself, as a
framework-less application does.
gplanchat/durable-laravel, the Laravel integration {#gplanchatdurable-laravel--the-laravel-integration}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-laravel gplanchat/durable-bridge-illuminate
php artisan migrate
php artisan vendor:publish --tag=durable-configThe package suggests the two bridges and requires neither, so you install the one of the backend you
select: gplanchat/durable-bridge-illuminate for one SQL database, as above, or
gplanchat/durable-bridge-temporal for a Temporal cluster. In memory needs neither. With backend
set to illuminate, the default, and the bridge missing, registration fails with the command to run.
Package auto-discovery registers the provider. From one published config/durable.php, the
provider binds the four storage ports, the activity and resume jobs, and the per-execution lock.
One backend value binds every port. A journal on one backend with a run catalogue on another
is a fault, so backend takes a single value. A value this package does not serve fails at
registration with an error that names it and the three backends the package serves: illuminate, memory
and temporal.
You declare workflows in configuration. Laravel has no equivalent of Symfony's attribute
autoconfiguration, so the workflows key names the classes. Naming them costs 0.14 ms, measured,
and does not grow with the application. A reflection scan costs 15 ms at a thousand classes and
loads all of them into every process to find five. For the same reason, there is no
durable:cache: config:cache already caches the file it would duplicate.
Work runs on the queue the application already drains, with php artisan queue:work as the
only worker. Activities and resumes are jobs; a timer is a deferred timer-firing job that uses the
queue's own delay.
Comparison with durable-workflow/workflow {#it-is-not-a-durable-engine-for-laravel-and-that-square-is-taken}
durable-workflow/workflow, formerly
laravel-workflow/laravel-workflow, provides durable execution on Laravel queues, with its own
storage. It is explicitly inspired by Temporal and Azure Durable Functions and has more than a
thousand stars. Since 2.0 it writes workflows as straight-line methods on Fibers, and it runs
embedded in your application, on its own standalone server, or on its managed Cloud, with PHP,
Python and Rust SDKs. It ships a monitoring UI, Waterline. It does its job well; if you want a
Laravel-first engine, choose it.
gplanchat/durable-laravel offers a different backend choice. The same workflow code runs
against a Temporal cluster (Temporal Cloud and Nexus included, with a history the Temporal UI
reads) or against a SQL database, with no cluster to run. A mixed Symfony / Sylius / Laravel
estate also shares a single engine: a workflow class written for gplanchat/durable-bundle runs
here unmodified. durable-workflow/workflow offers neither.
WorkflowResumeDispatcher::dispatchNewWorkflowRun() starts a run on every backend:
- on
illuminate, it queues the first resume forqueue:work; - on
temporal, it starts the workflow on the cluster, which delivers everything after that; - on
memory, it queues the run in the caller's process. Drive it in the same process withapp(InProcessWorkflowResumeDispatcher::class)->drain()after the dispatch. In a console process (tests, commands, queue workers),Artisan::call('durable:drain')does the same; the command is not registered in an HTTP request. The drain returns once the run has completed, or once it waits on a signal or on something due later than the ten-second budget. This backend's journal lives in the process, so nothing outside the process can advance the run, and a separatephp artisan durable:drainstarts with an empty queue and drives nothing.
A Nexus operation is an operation served by another service, with its own contract, that a
workflow calls the way it calls an activity (see the glossary). Serving one means
answering a call that arrives from another namespace, and only the cluster routes those calls. The
nexus.handlers key names the handlers and the contracts they serve:
'nexus' => ['handlers' => [App\Nexus\BillingHandler::class => App\Contracts\BillingService::class]],A workflow fulfils the operations that no handler serves. It carries #[FulfilsNexusOperation],
and listing it in workflows above is enough. A contract splits into two interfaces because PHP
has no way to express a partial implementation; the registry puts the two halves back together.
A Nexus declaration under a backend that cannot route fails at registration, before the first call, with an error that names the backend. Calling a Nexus operation needs no declaration here: the workflow makes the call, and that is the common case.
php artisan durable:nexus-worker drains the operations the cluster routes to this application.
| Setting | Rejected | Reason |
|---|---|---|
lock.store: null |
always | it grants every lock, in every deployment |
lock.store: array |
under illuminate |
a resume runs in a worker separate from whatever dispatched it, so two array locks never see each other: 15 overlapping critical sections out of 20, measured |
the sync queue connection |
under illuminate |
it runs jobs inline, so a resume that dispatches another resume recurses until the stack ends |
array remains allowed under memory. It is Laravel's own testing default, and a test needs
mutual exclusion within one process only.
The sqlite driver cannot host more than one worker. With four workers popping the jobs
table, the queue returns SQLSTATE[HY000]: General error: 5 database is locked, and three of the
four die on their first job, with WAL enabled and a 60 s busy timeout. Use MySQL, PostgreSQL or
Redis for the queue as soon as you run a second worker.
A killed worker's job stays reserved until retry_after, 90 seconds by default. A worker
started with --stop-when-empty inside that window finds an empty queue and exits without doing
anything, which looks exactly like a failed resume. The resume has not received the job yet. A
supervised worker outlives the window, picks the job up, and the execution completes.
Temporal is supported. backend: 'temporal' puts the journal and the run catalogue in
the cluster, and two workers drain what the application's own queue cannot carry:
php artisan durable:temporal-worker drains the workflow tasks, and
php artisan durable:temporal-worker --role=activity the activity tasks.
gplanchat/durable-bridge-temporal is only suggested. It installs eight packages,
five of them Symfony components that a Laravel application never loads, for some 36 MB. An
application that does not select the backend never installs them, and one that does gets an error
naming the package to install. Splitting the bridge, whose Symfony-coupled part is eight files out
of 774, would remove that weight; it is a separate change.
No dashboard. gplanchat/durable-filament
requires this package. This package suggests it in composer.json and never requires or detects it.
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-pluginThe plugin renders the dashboard in the Sylius admin: an entry in the admin menu,
the run list on Tabler cards with cursor paging, and the run detail beside it. The panels, the
grouping and the wording come from gplanchat/durable itself, so a run reads the same here and on
the Magento screen. This package provides the Sylius chrome around them.
Labels show the human-readable ActivityType.name and fall back to technical IDs only when no
name is available.
The plugin reads runs and executes nothing. It requires gplanchat/durable-bundle, which wires
the run catalog it reads, so the command above is the whole install.
Note
Live data comes from whichever backend is installed. The plugin requires no bridge:
gplanchat/durable suggests the backend, once, for every integration. Without a backend, the
plugin still installs, the route and the menu entry still work, and the dashboard renders its
degraded state instead of live runs.
gplanchat/durable-filament, the Filament dashboard {#gplanchatdurable-filament--the-filament-dashboard}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-filament// app/Providers/Filament/AdminPanelProvider.php
use Gplanchat\Durable\Filament\DurableFilamentPlugin;
return $panel
// ...
->plugin(DurableFilamentPlugin::make());The plugin renders the dashboard in a Filament 3 or 4 panel: a Durable runs entry in the panel's navigation, the run list with cursor paging and the name and execution-id filters that the backend can apply, and a page per run with its status, what it waits on, its Nexus operations and its history. It is available in English and French.
The plugin reads runs and executes nothing. It requires gplanchat/durable-laravel and reads
the run catalog that package binds for its backend: in-memory, Illuminate or Temporal. Nothing in
the plugin names a backend, and nothing in gplanchat/durable-laravel names Filament.
Note
The run page lists a run's Nexus operations when the catalog reports them, and only the Temporal catalog does: a journal cannot hold a Nexus operation, so on in-memory and Illuminate the section never appears.
gplanchat/durable-magento, the Magento integration {#gplanchatdurable-magento--the-magento-integration}
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-magentoA Magento 2.4 / Mage-OS module, listed as Gplanchat_DurableModule in bin/magento module:status.
It declares workflow and activity classes to the runtime, assembles the engine for a Magento
process, ships the workers as bin/magento commands, and adds a read-only admin screen under
System > Durable processes > Process history.
The screen uses Magento's chrome: a standard grid (paging, bookmarks, column controls, export, and a multi-select status filter whose options come from the status enum itself), with the state of the backend and the outcome counters above it. The content of the screen comes from neither Magento nor this package; see the dashboard, which every host renders in its own chrome.
Magento's container has no equivalent of Symfony's tag autoconfiguration, so you declare classes
explicitly, in two arrays in di.xml:
<type name="Gplanchat\DurableModule\Runtime\RuntimeFactory">
<arguments>
<argument name="workflowClasses" xsi:type="array">
<item name="place_order" xsi:type="string">Acme\Shop\Workflow\PlaceOrder</item>
</argument>
<argument name="activityHandlers" xsi:type="array">
<item name="order" xsi:type="object">Acme\Shop\Activity\OrderActivities</item>
</argument>
</arguments>
</type>You do not declare the contract. The factory reads each handler's interfaces and keeps those
carrying #[AsActivityMethod], which leaves one declaration fewer to get wrong and keeps the
activity names those of the attributes.
Three more arguments of the same factory bound a run, and di.xml is the only place to set them:
<argument name="maxActivityRetries" xsi:type="number">3</argument>
<argument name="budgetSeconds" xsi:type="number">30</argument>
<argument name="maxContinuations" xsi:type="number">10</argument>maxActivityRetriesis the retry ceiling of the activities thatMagentoRuntime::run()runs in the calling process when no DSN is set, the equivalent of the Symfony bundle'smax_activity_retries. The default,0, sets no cap. Temporal workers never read it: there, the cluster retries from the activity's ownRetryLimit.budgetSecondsboundsMagentoRuntime::run(). Without a DSN, the call runs a workflow to its end inside the calling process; with one, it waits that long for the cluster's result. Past the budget, the call throwsWorkflowStuckExceptioninstead of waiting longer. The default is10. In process, the budget exists because of the retry ceiling: with no ceiling, an activity that keeps failing would keep that process busy forever.maxContinuationsis, without a DSN, how many continue-as-new the in-process run follows (an execution that closes its journal and hands over to a fresh one; see the glossary) before it throwsContinuationCapReachedException. The default is10. With a DSN, the cluster runs the chain.
Workers and workflowClient() read none of them.
Magento supports three backends, and Composer enforces it. Magento reaches in-memory,
Temporal and its own database backend, and the module declares a conflict on both SQL bridges,
because Magento\Framework\App\ResourceConnection is neither a Doctrine DBAL connection nor
Illuminate's. app/etc/env.php selects the backend: resource/durable for the database backend
(see Backends), a DSN for Temporal, neither for memory; declaring
both fails with BackendSelectionException. A DSN selects Temporal:
'durable' => [
'temporal' => ['dsn' => 'temporal://temporal:7233?namespace=default&tls=0'],
],Without the DSN, the journal lives in the process that writes it and is lost when that process ends. That is acceptable for a console command and unsuitable for anything else.
MagentoRuntime::run() follows the same choice. Without a DSN, it runs the workflow in the calling
process. With a DSN, it starts the workflow on the cluster and waits for its result, which the
workers below produce. The wait lasts about budgetSeconds and ends with WorkflowStuckException.
With a DSN, a workflow that fails reaches the caller with the class the in-memory backend raises.
The workflow's own exception is rebuilt as new $class($message, $code): it carries the recorded
message and code, with no previous exception and no other property. That requires its class to load
in the calling process and its constructor to accept (message, code); otherwise the caller gets
WorkflowFailedException, whose message starts with Workflow "<execution id>" failed:. An
activity failure comes back as DurableWorkflowAlgorithmFailureException, with the activity's
exception as previous. Only a DSN adds two outcomes: WorkflowTimedOutException when a timeout set on the cluster elapses, and
WorkflowTerminatedException when someone terminates the run. A workflow that waits on a signal
waits the whole budget instead of failing at once.
The result comes back decoded from JSON: an object the workflow returns arrives as an array.
To start a workflow from an observer, call RuntimeFactory::resumeDispatcher()->dispatchNewWorkflowRun().
With a DSN, it starts the workflow on the cluster and returns at once. Without one, it runs the
workflow in the calling process, within budgetSeconds, and the request waits for it: it blocks
like run(). A workflow that fails does not throw from the call, as on the cluster; the failure goes
to the logger the factory holds. An undeclared workflow does throw. To start on the cluster only,
call workflowClient()->startAsync().
Workers are bin/magento commands, not queue consumers. Supervise them like any other
long-running process:
bin/magento durable:worker --role=journal --time-limit=3600
bin/magento durable:worker --role=activity --time-limit=3600Each process serves one role on one queue. The journal and activity roles use two distinct Temporal
queues, and you tune their concurrency separately. Nothing goes through Magento's own MessageQueue: on Temporal,
an activity is a Temporal command and a resume is a workflow task, so a Magento topic would only
add a second queue for an operator to supervise.
With resource/durable in env.php, the same command drains the database queues instead of
polling Temporal. Without --role, one process serves the resume, timer and activity queues;
--role=journal serves resumes and timers, --role=activity serves activities. There is no
--role=nexus on this backend, and the command refuses it. A message is acknowledged once it is
handled. When another worker holds the execution, when a resume arrives before its activity's
result, or when MySQL answers with a lock wait timeout or a deadlock, the message stays in the
queue and is delivered again a moment later. SIGTERM and SIGINT stop the worker between two
messages.
A shop that serves Nexus operations lists its handlers in
one more array of the same factory, nexusHandlers: one object per handler, whose
#[AsNexusServiceHandler] names the contract it serves. An operation the handler has no method for
comes from a workflowClasses entry carrying #[FulfilsNexusOperation]. A third process,
bin/magento durable:worker --role=nexus, serves them on the DSN's nexus_task_queue, which
defaults to the workflow task queue. Without a DSN, that worker fails with
A Nexus worker needs a cluster.
A missing worker shows differently depending on its role. Without --role=journal, nothing
advances: executions start, their history fills, and no process answers their workflow tasks.
Without --role=activity, the execution appears to work, which makes it harder to notice: it
advances up to its first activity and stops there, with the order charged and the stock
untouched, and you find out from the customer.
The --time-limit and --max-tasks bounds serve the supervisor: they end the process so that the
supervisor can restart it. Retries belong to the cluster, which schedules an activity's attempts
whether or not a worker is listening. A run whose activity "failed after 3 attempts" within seconds
points to a missing worker.
Warning
Magento's own queue settings do not apply to Durable. retry_inprogress_after, the
messagequeue_* cron jobs and queue_lock carry nothing of Durable's, because nothing of
Durable's goes through MessageQueue. Tune them for your own consumers.
Note
Start executions on the cluster, outside the request that triggers them. With a DSN, an
observer on sales_order_place_after hands the execution to Temporal and returns. A start can
still throw (an undeclared workflow), so catch it: an exception that leaves the observer aborts the
shop's own flow.
public function execute(Observer $observer): void
{
$incrementId = $observer->getEvent()->getData('order')->getIncrementId();
try {
$this->runtimeFactory->resumeDispatcher()->dispatchNewWorkflowRun(
ExecutionId::fromString('order-' . $incrementId),
PlaceOrder::class,
['orderId' => $incrementId],
);
} catch (\Throwable $exception) {
$this->logger->error('The workflow did not start: ' . $exception->getMessage());
}
}The same observer runs without a cluster, with a difference: the workflow runs inside the request,
which waits for it, for up to budgetSeconds (10 by default). A workflow that waits on a signal or
a long timer holds the request for the whole budget. The in-memory journal ends with the request,
so a run that is not finished by then is lost. That is acceptable in development; in production,
configure the DSN.
Each command below is the one the chooser on the home page gives you, written out in full.
The chooser reads its state from the URL, so a link can open the page with a situation already selected, for example in an issue, a README or a support answer:
https://durable.rocks/?fw=magento&be=temporal#install
fw is the framework (none, symfony, laravel, sylius, apiplatform, magento), be is
where the state lives (memory, temporal, dbal, illuminate), and dist is the base
underneath a distribution (none, symfony, laravel). Each axis is optional. The chooser
ignores a value it cannot apply (a framework that has not shipped, a backend that the pairing
forbids) instead of forcing it, so an old link falls back to the default instead of showing a
combination that does not exist. Choosing in the page rewrites the address bar, so the link to
share is the one already in your address bar.
Each command in the table assumes that the project allows the beta line first:
composer config minimum-stability beta
composer config prefer-stable true| Your situation | Command |
|---|---|
| Learning, or unit tests only | composer require gplanchat/durable |
| No framework, one SQL database | composer require gplanchat/durable gplanchat/durable-bridge-dbal |
| No framework, Temporal cluster | composer require gplanchat/durable gplanchat/durable-bridge-temporal |
| Symfony, tests only | composer require gplanchat/durable-bundle |
| Symfony, one SQL database | composer require gplanchat/durable-bundle gplanchat/durable-bridge-dbal |
| Symfony, Temporal cluster | composer require gplanchat/durable-bundle gplanchat/durable-bridge-temporal |
| Sylius, tests only | composer require gplanchat/durable-plugin |
| Sylius, one SQL database | composer require gplanchat/durable-plugin gplanchat/durable-bridge-dbal |
| Sylius, Temporal cluster | composer require gplanchat/durable-plugin gplanchat/durable-bridge-temporal |
| Laravel, one SQL database | composer require gplanchat/durable-laravel gplanchat/durable-bridge-illuminate |
| Laravel, Temporal cluster | composer require gplanchat/durable-laravel gplanchat/durable-bridge-temporal |
| Magento, Temporal cluster | composer require gplanchat/durable-magento gplanchat/durable-bridge-temporal |
Each line names only the integration: the bundle pulls in the library, and the plugin pulls in the bundle. Without a framework, you name the library yourself, and you also wire the workers yourself.
The Laravel lines name the integration and the bridge of the backend, because gplanchat/durable-laravel
requires neither bridge. It is a service provider that binds the four storage ports, with workflows
declared in config/durable.php and work on the queue the application already drains; the section
above lists what it does for you. To wire the stores yourself, require the library and the bridge
without the integration.
Every backend runs the same fiber driver and the same activity execution path. A workflow you tested in memory behaves the same way against DBAL or Temporal, including retry counting, failure classification, cancellation and compensation.
Some capabilities have no equivalent on a backend. Nexus fails with an explicit message there; a
child workflow's namespace, taskQueue and cronSchedule fail with
UnsupportedByBackendException, and its search attributes are recorded and not acted on.
Backends lists the differences.
Durable is developed in a single repository, gplanchat/durable-dev. A split publishes each
package to its own read-only repository, so composer require pulls a small package instead of
the whole tree.