qcodr/restate-sdk-laravel
Composer 安装命令:
composer require qcodr/restate-sdk-laravel
包简介
Laravel integration for the Restate PHP SDK — durable execution (workflows, virtual objects, sagas, durable timers) wired into Laravel's container, routing, and Artisan.
README 文档
README
Laravel integration for the Restate PHP SDK — durable execution (workflows, virtual objects, sagas, durable timers, exactly-once side effects) wired into Laravel's container, routing, and Artisan.
Write Restate services as ordinary Laravel classes (with constructor DI), list them in
config, and serve them either from inside your app (a request/response route) or over true
bidirectional HTTP/2 (php artisan restate:serve).
Why
Restate adds guarantees Laravel's native tools don't give you for multi-step, long-running, or per-entity-stateful work:
| Laravel area | What Restate adds |
|---|---|
| Jobs / multi-step processes | Durable sagas — order → payment → inventory → shipping with automatic retries and compensation, exactly-once |
| Per-entity state (counters, balances, inventory, rate limits) | Virtual Objects — single-writer state per key, no lockForUpdate / row-lock races |
| Long-running / human-in-the-loop (approvals, email verify, reminders) | Durable promises + timers — wait days for an external event, surviving restarts/deploys |
| Webhooks / side effects | Exactly-once processing + the outbox pattern via ctx->run() |
| Scheduler | Self-rescheduling durable timers that survive restarts (vs stateless cron) |
| API orchestration / fan-out | Durable combinators (select/awaitAll) with retries |
Requirements
- PHP 8.2+
- Laravel 12 or 13
qcodr/restate-sdk-php(pulled in automatically)amphp/http-serveronly if you usephp artisan restate:serve(bidi)
Installation
composer require qcodr/restate-sdk-laravel
php artisan vendor:publish --tag=restate-config # publishes config/restate.php
The service provider is auto-discovered.
Quick start
Define a service as a normal Laravel class — constructor dependencies are injected from the container:
namespace App\Restate; use Qcodr\Restate\Sdk\Context\Context; use Qcodr\Restate\Sdk\Service\Attribute\{Service, Handler}; #[Service] final class GreeterService { public function __construct(private readonly \App\Services\Mailer $mailer) {} #[Handler] public function greet(Context $ctx, string $name): string { // Non-deterministic work (DB, HTTP, mail) goes inside ctx->run() so it runs // exactly once and replays from the journal on retries. $ctx->run('notify', fn () => $this->mailer->ping($name)); return "Hello {$name}"; } }
Register it in config/restate.php:
'services' => [ App\Restate\GreeterService::class, ],
Point a running Restate runtime at your app and invoke through the ingress:
restate deployments register http://localhost:8000/restate --use-http1.1 curl localhost:8080/GreeterService/greet -H 'content-type: application/json' -d '"world"' # "Hello world"
One rule for handlers: they must be deterministic and stateless. Keep every DB / HTTP / mail / random call inside
ctx->run()(a durable side effect); per-invocation data lives in local variables or Restate state, never in instance properties.
Serving
In-app route (default). The provider mounts a catch-all route at the configured prefix
(restate by default), served request/response by your normal Laravel stack (FPM, Octane).
Register the deployment at <app-url>/restate. Zero extra infrastructure.
Bidirectional streaming. For cancellation, signals, and fewer re-invokes on suspension-heavy handlers, run the amphp host instead:
composer require amphp/http-server php artisan restate:serve --port=9080 --workers=8
Set config path to null to disable the in-app route when serving this way.
Calling Restate from Laravel
The two sections above expose your handlers to the runtime. The reverse direction — starting
a Restate invocation from ordinary Laravel code (a controller, job, or listener) — goes through
the Restate ingress with the RestateClient:
use Qcodr\Restate\Laravel\Client\RestateClient; public function __construct(private readonly RestateClient $restate) {} // Call and await the result (request/response): $greeting = $this->restate->call('GreeterService', 'greet', 'Ada'); // Fire-and-forget — returns the invocation id immediately: $id = $this->restate->send('OrderWorkflow', 'run', ['orderId' => $orderId], key: $orderId); // Keyed object/workflow, idempotency, and a durable delayed send: $this->restate->send('OnboardingService', 'nudge', ['userId' => $id], key: $id, delayMs: 3_600_000);
Configure the ingress in config/restate.php (ingress.url, RESTATE_INGRESS_URL; optional
ingress.token, RESTATE_INGRESS_TOKEN → Authorization: Bearer). Full recipe — controllers,
jobs, listeners, idempotency, delayed send, error handling — in
docs/usecases/dispatch.md.
Configuration
config/restate.php:
| Key | Description |
|---|---|
services |
Service / virtual-object / workflow class names exposed by the deployment |
path |
Route prefix the runtime calls (null disables the in-app route) |
middleware |
Middleware group for the route (default ['api']) |
identity_key |
publickeyv1_... to verify request signatures (needs ext-sodium) |
ingress.{url,token} |
Restate ingress base URL + optional Bearer token for the RestateClient (the caller side) |
server.{host,port,workers} |
restate:serve bind settings (workers: 0 = one per CPU) |
Artisan
php artisan restate:discover # list the bound services php artisan restate:discover --json # print the raw discovery manifest php artisan restate:serve # serve over bidi HTTP/2 (amphp)
Use cases
Worked, tested examples live under tests/Examples/ (proven offline against the SDK —
discovery, business logic, and the durable flow over a fake context) with copy-pasteable
recipes:
- Saga — an order-processing workflow (reserve inventory →
charge → ship) with automatic compensation on failure: a thin
#[Workflow]over injected, idempotent services. The proof drives a failing payment and asserts inventory is released, shipping never runs, and aTerminalExceptionsurfaces. - Rate limiter — a per-key token bucket as a
#[VirtualObject]: single-writer state per key, nolockForUpdate/ Redis race. The proof drains one key while another stays full, showing per-key isolation. - Dispatch from Laravel — the caller side: start
invocations (call-and-await, fire-and-forget
send, keyed objects/workflows, idempotency, durable delayed send) from a controller, job, or listener via theRestateClientover the Restate ingress. - Queue connection — dispatch existing
ShouldQueuejobs on therestateconnection (->onConnection('restate')) to run them durably on Restate (exactly-once, durable retries) with noqueue:workworker. - Validation — validate a handler's decoded-array input
with Laravel's Validator at the boundary via the
ValidatesInputtrait, throwing a terminal 400 on bad input (the idiomatic answer to the array gotcha below). - Testing —
Restate::fake()+Restate::assertCalled(...)/assertSent(...), theBus::fake()equivalent for Restate dispatches. - Generators & discovery —
php artisan make:restate-service(-object,-workflow) plus directory auto-discovery (configdiscover). - Typed clients —
php artisan restate:codegengenerates an IDE-autocompletable client per service (GreeterClient::fromContext($ctx)->greet(...)). - Scheduler —
Schedule::restate('Svc','handler', $payload) ->dailyAt('03:00')fires durable Restate invocations from Laravel's scheduler. - Observability — handler logs flow into Laravel's logging stack (replay-aware), and an optional Telescope watcher tags ingress dispatches.
- Auth & tenant — re-establish the authenticated user / tenant
inside a handler from the headers the runtime forwards (
withAuth), and forward them on outbound dispatches.
Several surface a real SDK boundary:
JsonSerdehands handlers the decoded array, not a hydrated object, so a handler's input parameter isarray/scalar and the value object is built (and validated — see the Validation trait) inside the handler.
Code quality
Mirrors the SDK's strict gate, all offline:
make check # php-cs-fixer + PHPStan (max, with Larastan) + Psalm taint + PHPUnit
License
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 5
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: Apache-2.0
- 更新时间: 2026-06-27