Runtimes (FPM and Swoole)
The same Winter code runs under PHP-FPM and under Swoole with no changes to controllers
or middleware. The runtime is chosen by which Boot method the entry point calls. The
difference reaches your code in exactly one place — shared state.
This section is being worked on
The page has not yet been checked against the current kernel code: some examples and
names may belong to earlier versions. Use it as a guide and verify exact signatures
against the sources or through call help.
The section will be rewritten, like the others that have already been reviewed.
What a runtime is and why
A runtime is the environment the application executes in: classic PHP-FPM (a process per request) or Swoole (long-lived coroutine workers).
The problem. FPM is simple and compatible, but it creates everything anew on every request. Swoole gives you coroutines and high throughput, but it lives a long time — and naive code with shared state breaks under it. You want to write one codebase and choose the runtime.
The solution. Winter hides the transport behind the HttpRequest/HttpResponse
contracts: the controllers are identical, and the runtime is set by the entry point.
That is what this page is about.
Entry points
Each runtime has its own Boot method:
final public static function web(): never; // FPM / dev
final public static function swoole(string $host = '0.0.0.0', int $port = 9501): never;
final public static function cli(array $argv = []): never; // the console
final public static function executor(array $argv = []): never; // a child thread// public/index.php (FPM)
require __DIR__ . '/../bootstrap.php';
Boot::web();
// server.php (Swoole — needs ext-swoole)
require __DIR__ . '/bootstrap.php';
Boot::swoole('0.0.0.0', 8080);The server is started with the run command (run = Swoole,
run dev = PHP’s built-in server).
The differences
FPM (web) |
Swoole (swoole) |
|
|---|---|---|
| Model | A process per request, no shared state | Long-lived workers, state in memory |
| Routes | Router::resolve (cached with DEBUG=false) |
Scanned once at startup, then from memory |
| Coroutines | none | SWOOLE_HOOK_ALL — PDO/cURL/files become coroutine-aware |
| Connection pool | a singleton per worker | a coroutine pool |
| Log context | ProcessContext |
CoroutineContext |
| Static files | Router::static() |
Router::static() |
Configuring Swoole
Override swooleConfig() in Boot — it is public static (not protected):
public static function swooleConfig(): array
{
return [
'worker_num' => swoole_cpu_num() * 2,
'max_request' => 5000, // restarting a worker fights leaks
'enable_coroutine' => true,
];
}run’s CLI options (--workers, --max_request…) override
these values.
The main pitfall — shared state
Do not keep request state in a singleton
Under Swoole a worker serves many requests, so a #[Singleton] is reused between
them. Never put the current request’s data (the current user, a request id, a loaded
entity) into a singleton’s properties — it will leak into somebody else’s request. Keep
singletons stateless and request state in the #[Request] scope (see Dependency
injection).
This is effectively the only difference that reaches application code. Everything else (the request/response adapters, the pool, the log context) the framework takes on itself.
Next
- CLI → run — starting the server and its options
- Dependency injection — scopes and shared state
- Logging — the context under FPM and Swoole