Repository navigation
Temporal PHP SDK v2.20.0
Warning
PHP 8.2+ is required. Support for Carbon 2 and Symfony 5.4 is dropped.
RoadRunner PHP packages moved from spiral/roadrunner-* to roadrunner/*, and RoadRunner v3 is recommended.
This release adds serialization context for data converters, RoadRunner v3 support, payload size warnings, and a guard that prevents read-only workflow callbacks from creating commands.
Features
Serialization Context
A payload converter can now know which Workflow or Activity it serializes data for — for example, to pick an encryption key or sign payloads per Workflow.
Implement SerializationContextAwareInterface on your converter: DataConverter passes the context down to every converter that implements it.
Two context types are provided:
WorkflowSerializationContext—namespace,workflowId;ActivitySerializationContext—namespace,activityType,taskQueue,workflowId,workflowType,isLocal.
Both implement HasWorkflowSerializationContext, so getNamespace() and getWorkflowId() work for either.
The example below signs each payload with the Workflow it belongs to, and rejects a payload decoded in a different context:
use Temporal\Api\Common\V1\Payload;
use Temporal\DataConverter\HasWorkflowSerializationContext;
use Temporal\DataConverter\PayloadConverterInterface;
use Temporal\DataConverter\SerializationContext;
use Temporal\DataConverter\SerializationContextAwareInterface;
use Temporal\DataConverter\Type;
final class SignedValue
{
public function __construct(public mixed $value) {}
}
final class SigningConverter implements PayloadConverterInterface, SerializationContextAwareInterface
{
private const ENCODING = 'binary/signed';
private const METADATA_KEY = 'ctx-signature';
private ?SerializationContext $context = null;
private string $signature = 'none';
public function getEncodingType(): string
{
return self::ENCODING;
}
public function getSerializationContext(): ?SerializationContext
{
return $this->context;
}
public function withSerializationContext(?SerializationContext $context): static
{
$clone = clone $this;
$clone->context = $context;
$clone->signature = $context instanceof HasWorkflowSerializationContext
? \sprintf('%s|%s', $context->getNamespace(), $context->getWorkflowId() ?? '')
: 'none';
return $clone;
}
public function toPayload($value): ?Payload
{
if (!$value instanceof SignedValue) {
return null;
}
return (new Payload())
->setData(\json_encode($value->value))
->setMetadata(['encoding' => self::ENCODING, self::METADATA_KEY => $this->signature]);
}
public function fromPayload(Payload $payload, Type $type): mixed
{
$signature = $payload->getMetadata()[self::METADATA_KEY] ?? '';
if ($signature !== $this->signature) {
throw new \RuntimeException("Payload signed for \"$signature\", decoded in \"{$this->signature}\"");
}
return new SignedValue(\json_decode($payload->getData(), false, 512, \JSON_THROW_ON_ERROR));
}
}Register the converter on both the client and the worker, before JsonConverter:
use Temporal\DataConverter\{BinaryConverter, DataConverter, JsonConverter, NullConverter, ProtoConverter, ProtoJsonConverter};
$converter = new DataConverter(
new SigningConverter(),
new NullConverter(),
new BinaryConverter(),
new ProtoJsonConverter(),
new ProtoConverter(),
new JsonConverter(),
);
$client = WorkflowClient::create($serviceClient, converter: $converter);
$factory = WorkerFactory::create(converter: $converter);The context is applied on both the client and the worker side: Workflow and Activity inputs and results, signals, queries, updates, memo, child Workflows, Continue-As-New, external signals and failures (FailureConverter).
ActivityCompletionClient applies no context by default; use withContext(ActivitySerializationContext $context) to set one.
The same behavior is covered by the cross-SDK conformance tests in temporalio/features.
Closes #587.
RoadRunner v3 Support
The SDK now works with RoadRunner v3. The PHP dependencies moved to the new roadrunner/* package names:
| Before | After |
|---|---|
spiral/roadrunner |
roadrunner/roadrunner |
spiral/roadrunner-cli |
roadrunner/cli |
spiral/roadrunner-kv |
roadrunner/kv |
spiral/roadrunner-worker |
roadrunner/worker |
roadrunner-php/roadrunner-api-dto |
roadrunner/api-dto |
roadrunner-php/version-checker |
roadrunner/version-checker |
If your project requires the old package names directly, update your composer.json.
The required RoadRunner version is now ^3.0, so rr get-binary downloads RoadRunner v3.
A RoadRunner 2025.x binary still runs the worker, but the worker logs a version warning at start, and the new worker options below have no effect on it.
Payload Size Warnings
The client logs a [TMPRL1103] warning when an outgoing request carries payloads above a limit: 512 KiB for payloads and 2 KiB for memo by default.
The limits are configurable (experimental):
use Temporal\Client\ClientOptions;
use Temporal\Common\PayloadLimitOptions;
$options = (new ClientOptions())->withPayloadLimits(
PayloadLimitOptions::new()->withPayloadSizeWarning(1024 * 1024),
);
// or turn the warnings off
$options = (new ClientOptions())->withPayloadLimits(PayloadLimitOptions::disabled());Closes #802.
Disable the Payload Error Limit on the Worker
By default, the worker fails the Workflow Task with PAYLOADS_TOO_LARGE when a payload exceeds the namespace limit. The new option turns this check off:
$worker = $factory->newWorker('task-queue', WorkerOptions::new()->withDisablePayloadErrorLimit());Closes #801.
Configurable Eager Activity Reservation Limit
The number of eager activities reserved per Workflow Task was hardcoded to 3. Now it is configurable (RoadRunner 3.0+):
WorkerOptions::new()->withMaxEagerActivityReservationsPerWorkflowTask(10);To turn eager execution off, use withDisableEagerActivities().
Closes #795.
AwaitOptions for Workflow::awaitWithTimeout()
Workflow::awaitWithTimeout() now accepts AwaitOptions as the first argument, so the underlying timer can get a summary. Concurrent awaits are now distinguishable in the UI.
use Temporal\Workflow\AwaitOptions;
use Temporal\Workflow\TimerOptions;
yield Workflow::awaitWithTimeout(
AwaitOptions::new(30)->withTimerOptions(TimerOptions::new()->withSummary('wait-for-resolution')),
fn(): bool => $this->resolution !== null,
);Closes #803.
Read-Only Workflow Callbacks
A query handler, an update validator, an await condition and a Workflow::sideEffect() callback could create commands (start a timer, an activity, etc.).
Such commands are not part of the workflow's sequence and break replay. Now such a call throws.
The guard is on by default. To keep the previous behavior:
\Temporal\Worker\FeatureFlags::$readOnlyWorkflowCallbacks = false;Priority Is Stable
Priority and its API are no longer experimental.
Others improvements and fixes
- #815 – chore: drop PHP 8.1 support (@xepozz)
- #817 – chore: drop support of old dependency branches: Carbon 2, Symfony 5.4 (@xepozz)
- #823 – fix(worker):
WorkerOptions::$deploymentOptionsdefaults tonullinstead of an uninitialized property (@xepozz) - #829 – perf: native defaults and time-only
DateIntervalwithout Carbon (@xepozz) - #828 – chore(bench): PHPBench microbenchmarks and a PR job that compares them with the base branch (@xepozz)
- #814 – chore: bump internal/dload to ^1.16.5 (@xepozz)
- #819 – ci: cache the DLoad version registry between workflow runs (@roxblnfk)
- #836 – test: accept null for empty search attributes on Temporal Server 1.31+ (@xepozz)