Laravel integration for the Restate PHP SDK — durable execution (workflows, virtual objects, sagas, durable timers) wired into Laravel's container, routing, and Artisan.
qcodr/restate-sdk-laravel is a Laravel package for laravel integration for the restate php sdk — durable execution (workflows, virtual objects, sagas, durable timers) wired into laravel's container, routing, and artisan..
It currently has 0 GitHub stars and 0 downloads on Packagist (latest version v0.2.0).
Install it with composer require qcodr/restate-sdk-laravel.
Discover more Laravel packages by qcodr
or browse all Laravel packages to compare alternatives.
Last updated
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).
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 |
qcodr/restate-sdk-php (pulled in automatically)amphp/http-server only if you use php artisan restate:serve (bidi)composer require qcodr/restate-sdk-laravel
php artisan vendor:publish --tag=restate-config # publishes config/restate.php
The service provider is auto-discovered.
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.
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.
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.
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) |
| server.limits | amphp connection / concurrency / idle ceilings for restate:serve; each key left unset keeps the SDK default, which assumes the runtime is the only peer — tighten them if the endpoint is reachable more widely |
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)
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:
#[Workflow] over
injected, idempotent services. The proof drives a failing payment and asserts inventory
is released, shipping never runs, and a TerminalException surfaces.#[VirtualObject]: single-writer state per key, no lockForUpdate / Redis race. The
proof drains one key while another stays full, showing per-key isolation.send, keyed objects/workflows, idempotency,
durable delayed send) from a controller, job, or listener via the RestateClient over the
Restate ingress.ShouldQueue jobs on
the restate connection (->onConnection('restate')) to run them durably on Restate
(exactly-once, durable retries) with no queue:work worker.ValidatesInput trait, throwing a
terminal 400 on bad input (the idiomatic answer to the array gotcha below).Restate::fake() + Restate::assertCalled(...)
/ assertSent(...), the Bus::fake() equivalent for Restate dispatches.php artisan make:restate-service
(-object, -workflow) plus directory auto-discovery (config discover).php artisan restate:codegen generates an
IDE-autocompletable client per service (GreeterClient::fromContext($ctx)->greet(...)).Schedule::restate('Svc','handler', $payload) ->dailyAt('03:00') fires durable Restate invocations from Laravel's scheduler.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.
Mirrors the SDK's strict gate, all offline:
make check # php-cs-fixer + PHPStan (max, with Larastan) + Psalm taint + PHPUnit