v2.18
Temporal PHP SDK v2.18
This release focuses on extensibility, broader client coverage, and a stronger testing story, along with a set of correctness fixes and dependency updates.
Features
Plugin System
A new plugin architecture lets you hook into the SDK lifecycle — connection, client, schedule client, worker and worker factory. Each stage has its own interface with a no-op trait, so a plugin implements only the methods it cares about by extending AbstractPlugin.
use Temporal\Plugin\AbstractPlugin;
use Temporal\Plugin\WorkerPluginContext;
final class MetricsPlugin extends AbstractPlugin
{
public function __construct()
{
parent::__construct(name: 'metrics');
}
// override only the hooks you need; the rest are no-ops.
// each hook is middleware — call $next to continue the chain.
public function configureWorker(WorkerPluginContext $context, callable $next): void
{
$context->addInterceptor(new MyInterceptor());
$next($context);
}
}Plugins are registered by name in a PluginRegistry (duplicate names raise a \RuntimeException); WorkflowClient::create() accepts a registry and worker plugins registered on the client are auto-propagated to workers.
Operator / Service Client
A repo-canonical OperatorClient / OperatorClientInterface exposes Temporal operator RPCs — including DeleteNamespace — through the SDK's own client abstraction. The shared gRPC plumbing (connection handling, metadata, interceptors, retries, call context) was generalized so workflow and operator clients behave identically.
use Temporal\Api\Operatorservice\V1\DeleteNamespaceRequest;
use Temporal\Client\GRPC\OperatorClient;
$operatorClient = OperatorClient::create('127.0.0.1:7233');
$response = $operatorClient->DeleteNamespace(
(new DeleteNamespaceRequest())->setNamespace('example-namespace'),
);Activity Reset Support
Activity::heartbeat() now throws a new ActivityResetException when the server reports the attempt was reset (alongside cancelled/paused), and the reset state is exposed via ActivityCancellationDetails::$reset.
use Temporal\Activity;
use Temporal\Exception\Client\ActivityResetException;
try {
Activity::heartbeat($progress);
} catch (ActivityResetException) {
// attempt was reset by the user
}
// or inspect without throwing:
if (Activity::getCancellationDetails()?->reset) {
// ...
}Expose Side Effect Summary
Workflow::sideEffect() now accepts an optional SideEffectOptions carrying a single-line summary (surfaced in the Web UI / CLI).
use Temporal\Common\SideEffectOptions;
$id = yield Workflow::sideEffect(
fn() => Uuid::v4(),
SideEffectOptions::new()->withSummary('generate correlation id'),
);The
summaryfield is currently marked experimental.
Expose Summary Metadata for Local Activities
Local activities gain the same summary-metadata support as regular activities via LocalActivityOptions::withSummary().
use Temporal\Activity\LocalActivityOptions;
$stub = Workflow::newActivityStub(
Billing::class,
LocalActivityOptions::new()
->withStartToCloseTimeout(5)
->withSummary('charge customer'),
);Typed Results & firstExecutionRunId for Update-with-Start
On the updateWithStart caller path, the resultType and firstExecutionRunId from UpdateOptions are now propagated to the server request (previously hardcoded), so the update result can be typed instead of stdClass.
use Temporal\Client\Update\UpdateOptions;
use Temporal\Client\Update\LifecycleStage;
$options = UpdateOptions::new('echo', LifecycleStage::StageCompleted)
->withResultType(UpdateResult::class);
$handle = $client->updateWithStart($stub, $options, ['hello']);
$result = $handle->getResult(); // instance of UpdateResultTesting Framework
A dedicated set of changes brings the PHP testing framework close to Go/Java/TS: outbound calls can be stubbed, time-driven workflows run under time-skipping, and history can be asserted after a run.
Mock Activities & Child Workflows
Child workflows were the last outbound call with no way to stub them, and activity mocks couldn't vary the result per call.
// Stub a child workflow — no real child execution is started
$this->workflowMocks->expectCompletion('GreetChild', 'Hello, Antony');
$this->workflowMocks->expectFailure('GreetChild', new \RuntimeException('boom'));
$this->workflowMocks->expectCompletionWhen('GreetChild', ['Antony'], 'Hi');
// Per-call activity results inside a loop
$this->activityMocks->expectConsecutiveCompletions('Order.charge', [true, false, true]);
// Arg-matched activity result
$this->activityMocks->expectCompletionWhen('Order.charge', [100], true);
// getVersion / sideEffect mocks
$this->workflowMocks->expectVersion('migrate-db', 2);
$this->workflowMocks->expectSideEffect('deterministic-value');History-based assertions over a finished run via WorkflowInteractions ($this->interactions($run)):
$this->interactions($run)->activity('Order.charge')->withInput(100)->assertCalledOnce();
$this->interactions($run)->childWorkflow('GreetChild')->assertStartedTimes(3);
$this->interactions($run)->timer()->assertStarted('PT30M');
$this->interactions($run)->signal('approve')->assertNeverSent();
$this->interactions($run)->assertNoOtherActivities();Closes [#524](#524), [#302](#302).
Time-Skipping Support & Delayed-Callback Scheduler
Timer-driven workflows are now testable under time-skipping (composer test:func-timeskip), and a client-side scheduler fires signals/queries/cancels at simulated-time offsets — so a 30-minute-timer workflow completes in seconds of wall-clock.
final class TimerTest extends TimeSkippingWorkflowTestCase
{
public function testDelayedSignals(): void
{
$stub = $this->workflowClient->newUntypedWorkflowStub('SignalCollectorWorkflow');
$this->delayedCallbacks
->signalAfter(60, 'add', 'a')
->signalAfter(120, 'add', 'b')
->start($stub);
$this->assertSame(['a', 'b'], $stub->getResult());
}
}TimeLockingInterceptor+TimeSkippingWorkflowTestCase— timer-only workflows andActivityMockernow run under time-skipping.- Lock-counter ownership guard in
WorkflowTestCase::tearDownso a leaked lock no longer poisons the suite.
Closes [#743](#743), [#745](#745), [#529](#529), [#744](#744).
Mock Typed Search-Attribute Upsert
Workflows that upsert typed search attributes can now run against the time-skipping test server (which doesn't implement that RPC), and the upserts are assertable.
$this->searchAttributeMocks->assertUpserted('customKeywordField');
$this->searchAttributeMocks->assertUpsertedValue('customIntField', 42);
$this->searchAttributeMocks->assertUnset('obsoleteField');PHPUnit Filtering in the Test Worker
Acceptance bootstrapping moved into a PHPUnit Extension that collects the test classes PHPUnit actually selected and passes them to the RoadRunner worker (test-class= arg), so only the needed task queues are created instead of one per test. A filtered run no longer pays for every feature's queue:
composer test:accept -- --filter 'UpdateWorkflowTestCase'Transcript Debug Module
A Temporal\Testing\Transcript module records inbound/outbound wire frames, logs, raised errors and workflow-history-on-failure to per-run files while debugging. It ships as a worker plugin (TranscriptPlugin, built on the new plugin system) and adds composer scripts to view/merge/clean output; location is controlled by TEMPORAL_TRANSCRIPT_* env vars.
TEMPORAL_TRANSCRIPT_DUMP_ON_FAIL=1 composer test:accept
composer transcripts:mergeImprovements & Fixes
Handle CanceledException as TimeoutException
RPC cancellation during update/result waits is now surfaced consistently as a timeout-or-canceled condition, aligned with the Go, Java and TypeScript SDKs.
Propagate Cancellation to Scopes & Awaits Registered After Cancel
Cancellation now propagates to scopes and awaits that are registered after a cancel has already fired (closes [#769](#769)). Gated behind a feature flag (default off):
\Temporal\Worker\FeatureFlags::$propagateCancellationToNewScopes = true;Generate Update ID Client-Side When Not Provided
When the caller supplies no update ID, the client now generates a UUID v4 (Common\Uuid::v4()) instead of sending an empty string — fixing updates that never resolved on the time-skipping test server, and matching Go/Java/TS.
Merge Method-Level #[MethodRetry] into Retry Options
A method-level #[MethodRetry] is now merged with (rather than overridden by) options passed at call time — options set explicitly at the call site win, and the rest fall back to the attribute.
#[ActivityMethod]
#[MethodRetry(maximumAttempts: 5, initialInterval: '1s')]
public function charge(int $amount): void;
// call-site options merge on top of the attribute:
// maximumAttempts stays 5, initialInterval overridden to 2s
$activity = Workflow::newActivityStub(
Billing::class,
ActivityOptions::new()
->withRetryOptions(RetryOptions::new()->withInitialInterval('2s')),
);Others improvements and fixes
- [#684](#684) - include PHP 8.5 into CI (@roxblnfk)
- [#731](#731) - update dependency constraint for google/protobuf to support newer version (@xepozz)
- [#756](#756) - bump symfony/http-client dependency to ^5.4.53 (@xepozz)
- [#733](#733) - upgrade internal/dload to ^1.10.0 (@xepozz)
- [#728](#728) - improve static analysis, part 1 (@xepozz)
- [#717](#717) - correct static analysis types inference (@xepozz)
- [#738](#738) - improved static analysis for proxied workflows and activities (@mtorromeo)
- [#762](#762) - add
TickInfo::applyToand adopt it in command routers (@xepozz) - [#736](#736) - rework request ID generation logic and visibility (@roxblnfk)
- [#734](#734) - remove experimental markers from versioning APIs (@xepozz)
- [#780](#780) - remove experimental markers from user-metadata fields (@laniehei)
- [#730](#730) - remove unused
TemporalStarterdependency causing server reboots (@xepozz) - [#732](#732) - improve schedule test readability and robustness (@xepozz)
- [#753](#753) - simplify test processes run (@xepozz)
- [#755](#755) - stabilize paused-activity test coverage (@xepozz)
- [#749](#749) - collapse acceptance tests onto a shared
defaulttask queue (@xepozz) - [#742](#742) - update and pin all GitHub Actions (@mjameswh)
- [#791](#791) - pin
spiral/gh-actionscs-fix workflow to commit SHA (@xepozz) - [#750](#750) - add Dependabot config with a 14-day dependency-release cooldown, VLN-1347 (@picatz)
- [#771](#771) - bump actions/cache from 5.0.4 to 6.1.0 (@dependabot)
- [#767](#767) - bump shivammathur/setup-php from 2.37.0 to 2.37.2 (@dependabot)
- [#772](#772) - bump actions/checkout from 6.0.2 to 7.0.0 (@dependabot)
- [#787](#787) - bump actions/checkout from 7.0.0 to 7.0.1 (@dependabot)
- [#782](#782) - bump actions/setup-node from 6.3.0 to 7.0.0 (@dependabot)
- [#747](#747) - add CONTRIBUTING.md (@dplyukhin)
- [#751](#751) - add banner like other SDKs have (@Sushisource)
New Contributors
- @dplyukhin — [#747](#747)
- @mtorromeo — [#738](#738)
- @picatz — [#750](#750)
- @dependabot — [#771](#771)
- @laniehei — [#780](#780)