sailantis / azera-stokehold
Application-server adapter that lets Azera run on stokehold. Serves HTTP from persistent PHP workers that reuse the Azera AppContext DI container, Router, Dispatcher and middleware pipeline.
Requires
- php: >=8.2
- sailantis/azera-framework: dev-main
- sailantis/stokehold-worker: dev-main
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-11 14:40:00 UTC
README
Application-server adapter that lets Azera run on top of stokehold. stokehold (a Rust application server) runs persistent PHP workers, so your app boots once and stays resident, reusing database/cache/queue connections across requests — while Azera's Router, Dispatcher, DI container and middleware pipeline work exactly as they do under PHP-FPM.
This package serves HTTP. It sits on top of
sailantis/stokehold-worker, the PHP SDK for stokehold's
worker protocol; that package is the transport, this one is the framework glue.
Installation
composer require sailantis/azera-stokehold
Requires PHP >= 8.2, the
sailantis/azera-frameworkpackage, and astokeholdbinary on the host.
Quick start
Point a worker backend at the entry point in your stokehold configuration:
default_backend = "app" [server] http = "0.0.0.0:8001" document_root = "/srv/app/public" front_controller = "index.php" [backends.app] kind = "worker" command = "php vendor/sailantis/azera-stokehold/azera-worker.php" workers = 4
Then start the server. Your existing routes, controllers, middleware and templates run unchanged:
stokehold serve
Stokehold serves static files from document_root itself and sends everything else to the worker, so the
layout is the same as under nginx plus PHP-FPM. No php -d flags are needed: see Sessions.
How it works
Stokehold spawns worker processes and speaks its own framed protocol to each one over stdin/stdout. The worker boots the application once, then serves every request from the same process:
stokehold (Rust)
└─ http listener ──► framed protocol over stdin/stdout ──► azera-worker.php
├─ WorkerBootstrap::boot() (once)
└─ StokeholdHttpWorker::run() (per request)
├─ WorkerSession::begin()
├─ StokeholdToAzeraRequest
├─ Router → Dispatcher → Controller
└─ clearRequestScope() (after each)
The per-request pipeline is the same one the framework runs under FPM. What an application server has to add is what a SAPI does for free, and this package does all of it:
- The body. Under a worker
php://inputis not the request body — stdin carries protocol frames — so the body is handed to Azera explicitly. This is not an optimisation: lettingbody()fall back tophp://inputwould read protocol bytes and desynchronise the stream permanently. $_POST. PHP parses a form-urlencoded body into$_POSTbefore the application runs. The worker reconstructs that from the body and the effective content type, soRequest::post()andinput()behave as they do under FPM.$_COOKIEand the session. A worker speaks neither HTTP nor CGI, so the session id is read from theCookieheader and theSet-Cookiefor a new session is written onto the response. See below.
Sessions
Sessions work with no special configuration, because PHP's own session cookie handling cannot be used in a
worker. header(), setcookie() and headers_list() are all no-ops under a worker's SAPI, so a
Set-Cookie PHP writes would never reach the client. WorkerSession (in sailera/azera-framework; every
adapter needs the same two halves) therefore:
- reads the session id from the request's
Cookieheader before the application runs, soSessionMiddlewarefinds it exactly as it would under FPM, and - builds the
Set-Cookiefor a session started during the request, for the adapter to put on the response.
It also relaxes session.use_cookies and session.cache_limiter at the start of each request, so PHP does
not waste effort on a cookie it cannot send — and stops it emitting Cache-Control headers, and a warning,
through the same dead channel. An id the client did send is still adopted, so a returning client keeps its
session.
The id is cleared between requests. session_id() outlives the request in a persistent process, so a client
presenting no cookie would otherwise be handed the session left behind by the previous client.
session.name and the session.cookie_* settings are honoured as usual, so a custom cookie name is set the
way it always was — session_name('_s') in the worker's customisation file.
Customising the worker
An application does not need to change the entry point. For a friendly 404, or any one-time setup that
needs the booted container, add a stokehold-worker.php to the project root that returns a callable:
<?php use Azera\AppContext; use Azera\Http\Response; return static function (AppContext $ctx): ?Closure { if (!DEBUG_MODE) { return static fn (string $path, string $method) => Response::redirect('/'); } return static fn (string $path, string $method) => Response::html( "<h1>404 Not Found</h1><p>No route {$method} {$path}.</p>", 404, ); };
The callable receives the booted AppContext and may return a not-found handler (string $path, string $method → Azera\Http\Response, or null for a plain 404). It runs before the first request, so it
is also the place for session settings — session_name('_s') — since PHP only refuses to change those once
a session is active.
Differences from azera-roadrunner
Same architecture, different transport — and a few things are deliberately better rather than merely different:
- No PSR-7 hop. RoadRunner's worker hands you PSR-7, so that adapter converts PSR-7 → Azera. Stokehold's
protocol already speaks CGI, so there is no intermediate message to build and no
nyholm/psr7dependency. - No reflection. Azera's
Requestnow has asetRawBody()method instead of the private-property poke the RoadRunner adapter previously needed. - Sessions work. Both adapters share
Azera\Http\WorkerSession, which carries the session id in from theCookieheader and theSet-Cookieback out, since a worker's SAPI has no header channel. dd()cannot kill the worker. In halt mode it becomes a 500 carrying the dump, as on RoadRunner.
Testing
composer test
The suite runs without PHP-FPM, a socket, or a stokehold binary: the worker is driven over in-memory streams carrying real protocol frames, so the loop, the translation, the response framing and the per-request reset are all asserted against the actual bytes a server would send and receive.
License
MIT — see LICENSE.