phasync / swerve
S(w)erve your long-running PHP applications from the CLI fast and efficiently.
Requires
- php: ^8.2
- ext-pcntl: *
- ext-posix: *
- ext-sockets: *
- phasync/net: ^2.0.2
- phasync/phasync: ^2.0.7
- psr/container: ^1.1 || ^2.0
- psr/http-message: ^1.0 || ^2.0
- psr/log: ^1.0 || ^2.0 || ^3.0
- psr/simple-cache: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: dev-master
- pestphp/pest: ^3.0 || ^4.0
- phasync/serializor: ^2.1.2
Suggests
- phasync/serializor: Swerve::upgrade() with a bootstrap file moves a closure to the WebSocket worker
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 0.2.0-RC3
- 0.2.0-RC2
- 0.2.0-RC1
- 0.1.0
- 0.1.0-beta7
- 0.1.0-beta6
- 0.1.0-beta5
- 0.1.0-beta4
- 0.1.0-beta3
- 0.1.0-beta2
- 0.1.0-beta1
- 0.1.0-alpha26
- 0.1.0-alpha25
- 0.1.0-alpha24
- 0.1.0-alpha23
- 0.1.0-alpha22
- 0.1.0-alpha21
- 0.1.0-alpha20
- 0.1.0-alpha19
- 0.1.0-alpha18
- 0.1.0-alpha17
- 0.1.0-alpha16
- 0.1.0-alpha15
- 0.1.0-alpha14
- 0.1.0-alpha13
- 0.1.0-alpha12
- 0.1.0-alpha11
- 0.1.0-alpha10
- 0.1.0-alpha9
- 0.1.0-alpha8
- 0.1.0-alpha7
- 0.1.0-alpha6
- 0.1.0-alpha5
- 0.1.0-alpha4
- 0.1.0-alpha3
- 0.1.0-alpha2
- 0.1.0-alpha1
- 0.1.0-alpha0
- dev-modes
- dev-classic
- dev-classic-fpm
- dev-plug
- dev-duplex
This package is auto-updated.
Last update: 2026-11-07 18:21:07 UTC
README
A PHP application server for connections that stay open.
<?php // swerve.php while (Swerve\Swerve::accept('public/', function () { echo "Hello, World\n"; })) { }
Swerve is a PHP application server, built on phasync
coroutines. A master process accepts the connections and hands each request to a worker; the
worker runs your swerve.php, which takes requests with Swerve::accept(). A request is PHP
as you know it: $_GET, $_POST, $_SESSION, header(), echo, php://input, each its own,
while the worker serves other requests at once. Your swerve.php chooses how: every request as it
comes, one at a time with the application kept loaded, or one per run, starting over clean as
under php-fpm.
Swerve is at 0.x: options and APIs may still change before 1.0.
Documentation · Examples: a chat room over Server-Sent Events and one over WebSockets.
Why swerve
- Connections that stay open. Server-Sent Events, WebSockets and long polling from an ordinary
request, raw connections after a
101, and publish/subscribe between workers, so live features need no separate Node or Go service. - Waiting is free. While one request waits for the database or an API, the same worker serves others. Your code stays ordinary sequential PHP: no promises, no callbacks. ext-phasync makes the blocking calls of libraries you did not write (PDO, mysqli, curl, file and DNS functions) wait as a coroutine.
- Isolated requests. Each request runs in a
Scopeof its own, with its own superglobals, headers and output, which the coroutines it starts share. - Boot once, or start clean. A
swerve.phpthat loops keeps the framework, routes and connection pools between requests; one that accepts once per run gives each request a fresh process state, as php-fpm does, and still shares opcache and APCu with every worker. - Supervised. A worker that dies is replaced. A reload starts new workers before the old ones stop, and the master's listener stays open throughout: no connection is refused.
- Yours to own. MIT, with no third-party dependencies beyond phasync and the PSR interfaces. See the Ennerd philosophy.
Getting started
Requirements: PHP 8.2 or later on Linux, with the pcntl, posix and sockets extensions, and apcu for
the shared cache, publish/subscribe and the container.
ext-phasync ships inside phasync, and swerve loads it by itself.
composer require phasync/swerve:^0.2@RC # 0.2 is a release candidate; without @RC Composer installs 0.1 vendor/bin/swerve -S public # serve a directory as php -S does: WordPress, Laravel's public/ vendor/bin/swerve # run ./swerve.php vendor/bin/swerve --watch # during development: reload when a PHP file changes
swerve.php decides how many requests a worker serves at once:
// every request as it comes, concurrently, with the application kept loaded while (Swerve::accept('public/', $app->handle(...))) { } // one at a time, keeping the application between requests while ($request = Swerve::accept('public/', $app->handle(...))) { phasync::await($request); } // one request per run of swerve.php, which starts over clean for the next (as under php-fpm) phasync::await(Swerve::accept('wordpress/', 'wordpress/index.php'));
The first argument is a directory of static files, answered without the application. The
handler is a script's path (run as a fresh main script, its PHP files too), a closure without
parameters (superglobals in, header() and echo out), or a closure taking a PSR-7
ServerRequestInterface and returning a ResponseInterface. Getting started
goes through it step by step.
What a request can do
- Stay open.
Swerve::upgrade()takes the connection off the request for a handler that keeps it as long as it likes, in the request: aSwerve\WebSocket, aSwerve\ServerSentEventsstream, or the rawSwerve\ClientRequest. Aswerve.phpthat loops serves other requests meanwhile; given a bootstrap file, a WebSocket moves to WebSocket workers that loaded it. A PSR-7 handler's101response stays open too. See Realtime. - Finish early. The response is complete at its
Content-Length, or atSwerve::finishRequest(), whichfastcgi_finish_request()calls: the connection takes its next request while the request's code runs on. - Wait cheaply.
phasync::sleep(),phasync::go()and every phasync primitive work in a request; the worker serves others while it waits.
Between workers
Publish and subscribe
Swerve::publish('chat', $message); foreach (Swerve::subscribe('chat') as $message) { // every message published to 'chat', in any worker, from the moment subscribe() returned }
Every message of every publisher has its place in one order, in a log the workers share in APCu.
Delivery is at most once, to the subscriptions that exist when it is published; a subscriber too
far behind gets a SubscriberLagException. Publish and subscribe
has the rest.
Shared cache
Swerve::cache() is a PSR-16 cache every worker shares, held in APCu.
$cache = Swerve::cache(); $user = $cache->get("user.$id") ?? load_user($id); $cache->set("user.$id", $user, 60);
A get, set or delete is one APCu call: a write is seen by every worker at once. Values last as
long as the master; a reload keeps them. It shares apc.shm_size (256M by default, taking memory
only as it fills) with publish/subscribe, and when that is full APCu empties itself.
Limiting a resource across workers
Swerve::container() is a PSR-11 container that caps how many instances of a service exist at
once in all workers together: MySQL accepts 151 connections by default, however many workers
there are.
// swerve.php, before accept(): every worker adds the same services Swerve::container()->add(\PDO::class, fn () => new \PDO($dsn, $user, $password), limit: 151); $db = Swerve::container()->get(\PDO::class); // this request's; waits while 151 are out $db = Swerve::container()->tryGet(\PDO::class, $ok); // null, $ok false, instead of waiting
A Lifetime::Scoped instance (the default) is shared by a request and its coroutines and given
back when the request has ended; Transient is one per get(); Singleton one per worker. An
instance holds its slot until it is garbage, and a worker that dies gives its slots back.
One worker at a time
A job that must not run twice takes a lock with flock() on a file of its own; the kernel
releases it when the handle closes or the worker dies.
$lock = fopen(sys_get_temp_dir() . '/myapp-nightly-report.lock', 'c'); if (flock($lock, LOCK_EX | LOCK_NB)) { work(); }
Per-request process state, for framework authors
Superglobals, headers, output and the session are each request's own. Process-wide PHP state is
not: setlocale(), a static of your own. A phasync::onScopeChange() hook keeps such state as
each request's, so application code can go on using the plain functions:
$locales = new WeakMap(); // per Scope phasync::onScopeChange(function (Scope $new, ?Scope $old) use ($locales) { if (null !== $old) { $locales[$old] = setlocale(LC_ALL, 0); } setlocale(LC_ALL, $locales[$new] ?? 'C'); });
Register it once, before accept(). It runs before a coroutine of another scope than the last one
runs: never between a request's own coroutines. It runs in the event loop and must not wait.
Running it
| Signal to the master | |
|---|---|
SIGTERM, SIGINT (Ctrl+C) |
Stop: the listener closes, the workers finish the requests in flight, then their WebSockets and event streams get a WorkerStoppingException (a WebSocket closes with 1001). What still runs after --grace (10 s) is killed. A second signal kills at once. |
SIGHUP |
Reload: each slot starts a worker on the code as it is now, and its old worker stops as above once the new one asks for its first request (a WebSocket closes with 1012: reconnect). A new worker that fails to start leaves the old one serving. |
- The master never loads your application. Workers are forked from it, so they share its opcache and APCu memory.
- swerve runs PHP as a long-running server:
memory_limitis a quarter of the machine's memory (at most 4 GiB) per worker,max_execution_timeis off, and opcache never checks timestamps: changed code arrives with a reload.php -d name=value vendor/bin/swervesets anything else. - It runs in the foreground, as systemd, Docker and supervisord expect, and logs to stderr (or
--log=<file>): notices and up, such as reloads and workers that exit.
Command line has every option, and Production sizing, systemd, Docker, nginx and TLS.
A server in your own program
A program that swerve does not start, such as a CLI tool with a terminal UI, serves HTTP itself
with Swerve\Server, in its own process, with the handlers Swerve::accept() takes:
phasync::run(function () use ($app, $tui) { $web = new Swerve\Server('127.0.0.1:8080', $app->handle(...), __DIR__ . '/public'); $tui->run(); $web->close(); });
One process, no workers: see A server in your own program.
