Demonstrates gplanchat/durable with Messenger, Temporal (gRPC workers), and class-based workflows (App\Durable\Workflow\). Durable persistence without DBAL in this sample: Temporal journal or in-memory depending on environment.
- PHP 8.2+
- Composer — dependencies live under
symfony/vendor/. Runcomposer installfrom thissymfony/directory. - Packages
gplanchat/durableandgplanchat/durable-bundleresolve via path from../src/Durableand../src/DurableBundleat the monorepo root. For an app outside the monorepo:composer require gplanchat/durable-bundlefrom Packagist.
After changing the vendor layout, clear cache: rm -rf var/cache/* then php bin/console cache:clear.
cd symfony
composer installWorkflowClient is the entry point for workflows from application code (controllers, commands, services):
// Start a workflow (fire-and-forget)
$executionId = $client->start('MyWorkflow', ['key' => 'value']);
// Start and wait until completion (synchronous)
$result = $client->startSync('MyWorkflow', ['key' => 'value']);
// Send a signal
$client->signal($executionId, 'approve', ['approved' => true]);
// Query workflow state
$status = $client->query($executionId, 'getStatus');
// Send an update (signal with a return value)
$result = $client->update($executionId, 'increment', ['amount' => 1]);WorkflowClient (and WorkflowResumeDispatcher) dispatches a ResumeWorkflowMessage containing only the executionId. Metadata (workflow type, initial payload) is stored in WorkflowMetadataStore before dispatch. ResumeWorkflowHandler loads that metadata to resume or start execution.
For the native Temporal backend, WorkflowTaskRunner:
- Receives a
PollWorkflowTaskQueueResponsewith Temporal history - Builds a
TemporalExecutionHistorywith O(1) event lookups - Runs the workflow handler in a standard PHP
Fiber - Replays history: resolved awaitables resume immediately
- Stops on the first unresolved awaitable (new command) and returns Temporal commands
No pcntl_fork(), no Swoole, no RoadRunner — plain PHP CLI.
TemporalHistoryCursor lazily pages Temporal history via next_page_token without loading the full history into memory at once.
See .env.dev: DURABLE_DSN (temporal://…), durable.temporal.dsn enables the Temporal bridge.
Typical workers (via symfony serve or manual runs):
# Journal worker (polls Temporal for workflow tasks)
php bin/console messenger:consume durable_temporal_journal
# Activity worker (polls Temporal for activity tasks)
php bin/console messenger:consume durable_temporal_activity
# In-memory workflow worker (Messenger backend)
php bin/console messenger:consume durable_workflowsSee .symfony.local.yaml for full worker configuration when using symfony serve.
cd symfony
composer test
# or: php bin/phpunitTests cover durable:sample example workflows and, when configured, real Temporal integration.
Requires a Temporal frontend reachable from the machine running PHPUnit (often docker compose up -d). The host port may differ from 7233 if TEMPORAL_FRONTEND_PORT is set — check with docker compose port temporal 7233.
- Prerequisite: ext-grpc
- Variable:
DURABLE_DSN, e.g.temporal://127.0.0.1:7233?namespace=default&journal_task_queue=durable-journal&activity_task_queue=durable-activities&tls=0(adjust the port) - Command:
composer test:temporal-integrationorphp bin/phpunit --group temporal-integration
Without a DSN or a reachable server, tests in group temporal-integration are skipped (the default composer test suite stays green).
durable:
temporal:
dsn: '%env(DURABLE_DSN)%'
# Option interpreter_mirror_activities was removed in the refactor.
# WorkflowTaskRunner handles native replay via Fiber (DUR027).The interpreter_mirror_activities key was removed. The Temporal bridge now uses WorkflowTaskRunner + TemporalHistoryCursor for native replay (see DUR027).