| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A pure-PHP SDK for Restate — durable execution for Services, Virtual Objects, and Workflows. It mirrors the Rust SDK surface with idiomatic PHP: attributes for service definitions, a typed context API, and a true bidirectional HTTP/2 streaming server.
The Restate service protocol (v5–v7) is implemented from scratch in pure PHP — framing, protobuf messages, the journal/replay state machine, suspension, and signals — so the SDK has no native-extension dependency: the default server (AmpStreamingServer) runs on pure-PHP amphp/http-server, and a request/response Swoole server, a PSR-15 adapter, and an AWS Lambda handler are available as alternative transports.
composer require qcodr/restate-sdk-php
composer require amphp/http-server # the default server transportDefine services with attributes; the first parameter is always the context.
use Qcodr\Restate\Sdk\Context\{Context, ObjectContext, SharedObjectContext};
use Qcodr\Restate\Sdk\Service\Attribute\{Service, VirtualObject, Handler, Shared};
#[Service]
final class Greeter
{
#[Handler]
public function greet(Context $ctx, string $name): string
{
return "Greetings {$name}";
}
}
#[VirtualObject]
final class Counter
{
#[Handler] // exclusive: may write state
public function add(ObjectContext $ctx, int $delta): int
{
$next = ($ctx->get('count') ?? 0) + $delta;
$ctx->set('count', $next);
return $next;
}
#[Shared] // read-only, concurrent
public function get(SharedObjectContext $ctx): int
{
return $ctx->get('count') ?? 0;
}
}Serve them over true bidirectional HTTP/2 streaming (the default server):
use Qcodr\Restate\Sdk\Endpoint\Endpoint;
use Qcodr\Restate\Sdk\Endpoint\ProtocolMode;
use Qcodr\Restate\Sdk\Server\AmpStreamingServer;
$endpoint = Endpoint::builder()
->bind(new Greeter())
->bind(new Counter())
->protocolMode(ProtocolMode::BidiStream)
->build();
(new AmpStreamingServer($endpoint))->listen('0.0.0.0', 9080);Register the deployment with a running Restate server, then invoke through the ingress:
restate deployments register http://localhost:9080
curl localhost:8080/Greeter/greet -H 'content-type: application/json' -d '"world"' # "Greetings world"
curl localhost:8080/Counter/acme/add -H 'content-type: application/json' -d '5' # 5Drop ->protocolMode(ProtocolMode::BidiStream) (and add --use-http1.1 when registering) to serve plain request/response over the same amphp host, or swap in SwooleServer / the PSR-15 / Lambda adapters — see Transports below.
use Qcodr\Restate\Sdk\Context\{WorkflowContext, SharedWorkflowContext};
use Qcodr\Restate\Sdk\Service\Attribute\{Workflow, Handler, Shared};
#[Workflow]
final class SignupWorkflow
{
#[Handler]
public function run(WorkflowContext $ctx, string $email): string
{
$ctx->set('email', $email);
$token = $ctx->promise('email-verified'); // suspends until resolved
return "verified:{$email}:{$token}";
}
#[Shared]
public function verify(SharedWorkflowContext $ctx, string $token): void
{
$ctx->resolvePromise('email-verified', $token);
}
}Service classes must be stateless. A bound service instance is shared across concurrent invocations within the server process — keep per-invocation data in local variables or Restate state ($ctx->set(...)), never in mutable instance properties.
| Capability | Methods |
|---|---|
| Side effects | run(name, fn, ?RunOptions) — with optional per-run RetryPolicy |
| Timers | sleep(seconds), timer(seconds) → DurableFuture |
| Calls (await) | serviceCall, objectCall, workflowCall (+ idempotencyKey, headers) |
| Calls (async) | serviceCallAsync, objectCallAsync, workflowCallAsync → DurableFuture |
| Calls (handle) | serviceCallHandle, … → CallHandle (result(), invocationId()) |
| One-way sends | serviceSend, objectSend, workflowSend (optional delay) |
| Cancellation | cancel(invocationId) — peer cancel; observed as CancelledException (409) |
| Combinators | select, awaitAll, awaitAny (any), awaitAllSucceeded (all-or-fail) |
| Tracing | traceContext() — W3C trace context (bridge to OpenTelemetry) |
| Awakeables | awakeable(), resolveAwakeable, rejectAwakeable |
| State (objects) | get, set, clear, clearAll, stateKeys |
| Promises (wf) | promise, peekPromise, resolvePromise, rejectPromise |
| Request meta | key(), invocationId(), requestHeaders(), requestIdempotencyKey() |
| Randomness | random()->uuidV4(), randomInt, randomFloat |
| Logging | logger() — a replay-aware PSR-3 logger |
Errors: throw TerminalException for a non-retryable failure returned to the caller; RetryableException (optionally pause: true or a retryDelayMillis) for a tuned transient failure; any other throwable is a plain transient error (retried).
ctx->logger() returns a PSR-3 logger that suppresses records emitted during replay, so each line is logged exactly once even though handlers re-run from the top on every slice. Provide the underlying logger (e.g. Monolog) when constructing the server: new AmpStreamingServer($endpoint, logger: $myLogger) (defaults to a null logger).
For distributed tracing, mind the propagation boundary:
The SDK stays dependency-free and emits no spans itself. Install open-telemetry/sdk and use the withIncomingTraceParent() bridge in examples/tracing.php to start spans under the incoming trace.
Discovery options. Configure per-service / per-handler behavior the runtime reads from the manifest (negotiated up to schema v4):
use Qcodr\Restate\Sdk\Service\{ServiceOptions, HandlerOptions, RetryPolicyOnMaxAttempts};
$endpoint = Endpoint::builder()
->bindWithOptions(new Counter(), (new ServiceOptions(
inactivityTimeoutMillis: 60_000,
idempotencyRetentionMillis: 86_400_000,
ingressPrivate: false,
metadata: ['team' => 'orders'],
))->withHandler('add', new HandlerOptions(
retryPolicyMaxAttempts: 5,
retryPolicyOnMaxAttempts: RetryPolicyOnMaxAttempts::Pause,
)))
->build();Request identity verification (opt-in; requires ext-sodium). Reject requests not signed by your Restate instance's key:
$endpoint = Endpoint::builder()
->bind(new Greeter())
->identityKey('publickeyv1_...') // unsigned/invalid requests → 401
->build();Transports. The default AmpStreamingServer (pure-PHP amphp) serves true bidirectional HTTP/2 streaming. One amphp process is a single event loop; pass a worker count to pre-fork N processes that share the port via SO_REUSEPORT (needs ext-pcntl) and scale across cores like a Swoole worker pool:
(new AmpStreamingServer($endpoint))->listen('0.0.0.0', 9080, workers: 8); // 0 = one per CPUIts connection, concurrency and idle ceilings default to values sized for the Restate runtime as the only peer — many long-lived, deliberately idle streams from one IP, which amphp's own defaults reject. That is not a general hardening posture: if the endpoint is reachable by anything other than your runtime (especially without identityKey()), tighten them:
new AmpStreamingServer($endpoint, limits: new ServerLimits(
connectionLimitPerIp: 200,
streamIdleTimeoutSeconds: 300,
));The same framework-agnostic core is also hostable request/response via the Swoole server (Qcodr\Restate\Sdk\Server\SwooleServer, needs ext-swoole), a PSR-15 adapter (Qcodr\Restate\Sdk\Server\Psr15Handler) in any Slim/Mezzio stack, on AWS Lambda (Qcodr\Restate\Sdk\Server\LambdaHandler — Function URL / API Gateway proxy), and directly via RequestProcessor (bytes in → bytes out).
Typed clients. bin/restate-codegen <ServiceClass> [outDir] [namespace] generates an IDE-autocompletable client so callers write GreeterClient::fromContext($ctx)->greet('world') instead of stringly-typed $ctx->serviceCall('Greeter','greet','world'). The discovery manifest also carries a JSON Schema for each handler's input/output, derived from the PHP types.
Serde. JSON is the default; BytesSerde provides raw octet-stream passthrough. Inject a custom Serde into the server/processor for other formats.
The examples/ directory ports the Rust SDK's examples to PHP. Each file is a self-contained, runnable endpoint.
| Example | Shows |
|---|---|
| greeter.php | the simplest stateless service |
| counter.php | Virtual Object state (get / add / increment / reset) |
| run.php | durable side effects (ctx->run) around an HTTP call |
| failures.php | terminal (no-retry) vs transient (retried) errors |
| fan_out.php | concurrent durable timers via timer() + select() |
| schema.php | structured JSON input/output + scalars |
| cron.php | a periodic task that re-schedules itself with delayed sends |
| services.php | the canonical Service + Virtual Object + Workflow trio |
| tracing.php | replay-aware PSR-3 logging (run standalone: php examples/tracing.php) |
Run a single example with the bundled server (amphp; the per-example endpoints are request/response, so --use-http1.1 is fine):
php bin/restate-serve examples/counter.php # serves on :9080
restate deployments register http://localhost:9080 --use-http1.1
curl localhost:8080/Counter/my-key/incrementOr bring all of them up live (Docker) over true bidi HTTP/2, against a real runtime:
make examples # builds + registers the example endpoint (bidi)
curl localhost:8080/FanOut/fanOut # -> "Completed in order: fast, medium, slow"Unit tests run anywhere (no extensions, no Docker):
composer install
composer test # vendor/bin/phpunit --testsuite unitEnd-to-end verification is the official cross-SDK conformance suite (restatedev/e2e) — the same battery every Restate SDK runs. It boots a real Restate runtime + a PHP image of the standard test-services and drives them (needs JDK ≥ 21 + an AVX2 host):
make conformance # downloads the suite, builds the images, runs `default`
make conformance TEST_SUITE=allThe default config passes 48 / 49 over the bidi (amphp) transport on a V7-enabled runtime — Cancellation 6/6, KillInvocation 1/1, Signals 2/2 included. It also runs in CI (conformance.yml); an AVX2-free offline fallback is documented in conformance/README.md.
To try the example services live by hand:
make examples # curl localhost:8080/FanOut/fanOut
make downThe project ships a strict static-analysis, coding-standard, and security gate — all of it runs fully offline (no cloud/SaaS):
make lint # php-cs-fixer (check) + phpstan (== composer lint)
make stan # phpstan only (== composer stan)
make cs # coding-standard check (no changes) (== composer cs)
make cs-fix # apply the coding standard (== composer cs:fix)
make sast # psalm taint analysis (SAST) (== composer sast)
make check # lint + sast + unit tests (pre-commit) (== composer check)The Compose file pins restatedev/restate:1.5.2 (the last AVX2-free image, so it runs on older hardware); it already serves the bidi examples. Override with RESTATE_IMAGE=... for a newer runtime — V7 cancellation/signals over bidi needs ≥ 1.7 (which needs AVX2), as covered in conformance/README.md.
src/ Protocol/ wire protocol: 64-bit framing, hand-rolled protobuf codec, messages Vm/ StateMachine — journal replay, completion table, suspension, eager state Discovery/ endpoint manifest builder + content-type negotiation Service/ attributes + reflection-based service/handler definitions Context/ typed context API (Service / Object / Workflow) over the VM Serde/ JSON (de)serialization Endpoint/ framework-agnostic RequestProcessor + transport DTOs Server/ transport adapters: AmpStreamingServer (default, bidi), Swoole, PSR-15, Lambda
The framework-agnostic RequestProcessor (bytes in → bytes out) is the testable core; each server is one swappable transport. The default AmpStreamingServer advertises BIDI_STREAM: the runtime keeps the invocation channel open in both directions, streaming the journal and late completions/signals, so a parked await is resumed on the next result instead of writing a suspension. The request/response transports (SwooleServer, PSR-15, Lambda) advertise REQUEST_RESPONSE instead: the SDK processes one slice and suspends when it awaits a result it does not yet have, and the runtime re-invokes with a longer journal so the handler replays from the top.
| Back | FazBrowse Home | New Git URL |