Advanced

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.

FPM Boot::web()Swoole Boot::swoole()Contracts HttpRequest / HttpResponse

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:

php
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
php
// 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):

bootstrap.php
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