A thin native extension that runs the official Temporal Rust Core
(temporalio/sdk-rust) in-process and
exposes it to PHP as an async transport. No gRPC extension, no RoadRunner: the
core's gRPC runs on its own Tokio threads, and completions are delivered back to
the TrueAsync reactor through a cross-thread trigger, so every call looks
synchronous while the coroutine yields underneath.
The high-level client API is the reused official Temporal PHP SDK (the
Temporal\* namespace), driven through a ServiceClientInterface adapter over
this transport — see the true-async/sdk-php
fork (branch true-async), which strips gRPC/RoadRunner from the dependencies.
A client starting a workflow — written flat: the first Core call launches the
TrueAsync scheduler and the script runs as the main coroutine, so no explicit
Async\spawn() wrapper is needed (the same way Async\await() does it).
use Temporal\Client\WorkflowClient;
use Temporal\Client\WorkflowOptions;
use Temporal\Client\GRPC\TrueAsyncServiceClient;
use TrueAsync\Temporal\Core\Connection;
$client = WorkflowClient::create(
TrueAsyncServiceClient::fromCore(new Connection('127.0.0.1:7233')),
);
$stub = $client->newUntypedWorkflowStub('OrderWorkflow',
(new WorkflowOptions())->withTaskQueue('orders')->withWorkflowId('order-42'));
$run = $client->start($stub, $orderId);
echo $run->getResult(); // parks the coroutine until the workflow completesMore — defining workflows/activities, running a worker, signals and queries — in Usage below.
Just the transport seam:
TrueAsync\Temporal\Core\Connection—connectplus an asyncrpcCall(service, method, requestBytes): responseBytes.TrueAsync\Temporal\Core\Worker— the worker transport: poll/complete for activity tasks and workflow activations, activity heartbeat recording, and the shutdown lifecycle.TrueAsync\Temporal\{TemporalException, ConnectionException, ServiceException}.
Everything user-facing (workflow client, options, data converter, the generated
Temporal\Api\* protobuf messages) comes from the reused SDK.
Workflow and activity code is the reused SDK's — the same attributes and
generator (yield) style as temporalio/sdk-php; only the transport differs.
use Temporal\Activity\ActivityInterface;
use Temporal\Activity\ActivityMethod;
use Temporal\Activity\ActivityOptions;
use Temporal\Workflow;
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
#[WorkflowInterface]
class OrderWorkflow
{
#[WorkflowMethod(name: 'OrderWorkflow')]
public function run(string $orderId): \Generator
{
$activities = Workflow::newActivityStub(
OrderActivities::class,
ActivityOptions::new()->withStartToCloseTimeout(10),
);
$charged = yield $activities->charge($orderId); // schedule + await the activity
return "order {$orderId}: {$charged}";
}
}
#[ActivityInterface(prefix: 'Order.')]
class OrderActivities
{
#[ActivityMethod]
public function charge(string $orderId): string
{
// real I/O is fine here — activities run in ordinary TrueAsync coroutines
return 'charged';
}
}use Temporal\Worker\TrueAsync\TemporalWorker;
use TrueAsync\Temporal\Core\Connection;
use TrueAsync\Temporal\Core\Worker as CoreWorker;
$core = new CoreWorker(new Connection('127.0.0.1:7233'), 'orders'); // 'orders' = task queue
(new TemporalWorker($core, 'orders'))
->registerWorkflowTypes(OrderWorkflow::class)
->registerActivityImplementations(new OrderActivities())
->run(); // long-polls workflows + activities until shutdown()run() blocks until the core shuts down; call $worker->shutdown() from a signal
handler or another coroutine to stop the loops, after which run() finalizes and
returns.
Signal and query handlers are plain methods on the workflow:
#[WorkflowInterface]
class SubscriptionWorkflow
{
private bool $cancelled = false;
#[WorkflowMethod(name: 'SubscriptionWorkflow')]
public function run(): \Generator
{
yield Workflow::await(fn() => $this->cancelled);
return 'cancelled';
}
#[Workflow\SignalMethod(name: 'cancel')]
public function cancel(): void
{
$this->cancelled = true;
}
#[Workflow\QueryMethod(name: 'isCancelled')]
public function isCancelled(): bool
{
return $this->cancelled;
}
}Drive it from a client through the stub:
$stub = $client->newUntypedWorkflowStub('SubscriptionWorkflow',
(new WorkflowOptions())->withTaskQueue('orders')->withWorkflowId('sub-1'));
$run = $client->start($stub);
$open = (bool) $stub->query('isCancelled')->getValue(0); // false — read state, no side effects
$stub->signal('cancel'); // deliver a signal
echo $run->getResult(); // "cancelled"0.1.0-dev — pre-release, the API is not yet stable. Working end to end against
a live server today:
- Transport (
Core\Connection,Core\Worker) — reviewed, ASAN-clean, covered by the test suite and CI. - Activity worker — run, heartbeat, cooperative cancellation.
- Workflow worker — the core lifecycle through the reused SDK engine: start/complete, timers, activities, signals, queries, cancellation (of the workflow, its timers, activities and child workflows), child workflows, and continue-as-new.
In progress: the long tail of workflow commands — signal/cancel external workflow, updates, side effects, versioning/patches, local activities, and search attributes. These raise an explicit error rather than failing silently.
The official temporalio/sdk-php runs
PHP workers under RoadRunner with its own event loop, beside TrueAsync rather
than on it. This project links the official Rust core directly and reuses the
SDK's client layer on top, in-process on the TrueAsync reactor.
- PHP 8.x built with ZTS and the TrueAsync runtime.
- A Rust toolchain (
cargo) to build the vendoredsdk-core-c-bridge. - The reused SDK package (
true-async/sdk-php, branchtrue-async), which pullsgoogle/protobufand the bundled Temporal protobuf messages.
git submodule update --init --recursive
cargo build --release -p temporalio-sdk-core-c-bridge \
--manifest-path third_party/sdk-rust/Cargo.toml # build the Rust core bridge (once)
phpize && ./configure --enable-temporal --with-php-config="$(command -v php-config)"
make -j"$(nproc)" # -> modules/temporal.so
php run-tests.php -q -p "$(command -v php)" \
-d extension="$(pwd)/modules/temporal.so" tests/ # live/ cases SKIP without a dev serverFull instructions, requirements and the design rationale: docs/installation.md and docs/DESIGN.md.
Apache 2.0. Temporal and the Temporal logo are trademarks of Temporal Technologies Inc.