Search by

phasync / swerve-yii

frodeborli

Run Yii 3 applications on swerve: booted once per worker, WebSockets and Server-Sent Events, no rewrite.

Package info

github.com/phasync/swerve-yii

pkg:composer/phasync/swerve-yii

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

0.1.1 2026-10-19 19:29 UTC

This package is auto-updated.

Last update: 2026-10-19 22:01:30 UTC


README

CI Packagist PHP License

Your Yii 3 application, booted once and kept warm. swerve is a PHP application server: long-running workers that serve HTTP/1.1 themselves, stream request and response bodies, and hold WebSockets and Server-Sent Events. This package lets it run a Yii 3 application (yiisoft/app) unchanged. Yii 2 is not covered.

One request at a time per worker. Yii's container keeps a request's state in shared services, and PHP's session is one process-wide $_SESSION, so the adapter runs requests one at a time per worker; a request that waits for a database or an HTTP API holds its worker meanwhile. Run more workers for more concurrent requests. Why, and what to do about it.

The framework running correctly on swerve does not mean your application does: state your own code keeps in statics, globals or shared services, and connections shared between requests, need the same care. See Making your application safe for concurrent requests. phasync/doctor lists the candidates in your code: composer require --dev phasync/doctor, then vendor/bin/phasync-doctor.

composer require phasync/swerve-yii
vendor/bin/swerve --adapter=yii --http=0.0.0.0:8080 --public=public

That's the whole setup: the yii adapter runs the skeleton's src/bootstrap.php (the autoloader and .env) and serves the application. --adapter=yii picks it over swerve-psr15's adapter, which comes with it (or put "extra": {"swerve": {"adapter": "yii"}} in your composer.json). public/index.php stays as it is, so the same application still runs under PHP-FPM. APP_ENV and APP_DEBUG are read from the environment, as the skeleton reads them; a swerve.php run with --adapter=swerve sets them instead:

<?php // swerve.php, next to composer.json

require __DIR__ . '/src/bootstrap.php'; // yiisoft/app's: the autoloader and .env

return Swerve\Psr15\Bridge::wrap(new Swerve\Yii\Handler(__DIR__, debug: false, environment: 'prod'));

The handler is a PSR-15 request handler, served by swerve-psr15's Bridge.

WebSockets

A Yii action returns swerve-psr15's WebSocketResponse::from(); the callback gets swerve's WebSocket and runs on the connection after the 101. An ordinary GET to the same route is answered 426.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Swerve\Psr15\WebSocketResponse;
use Swerve\WebSocket;
use Swerve\Swerve;
use Yiisoft\User\CurrentUser;

final readonly class NewsAction   // Route::get('/news')->action(NewsAction::class)
{
    public function __invoke(ServerRequestInterface $request, CurrentUser $user): ResponseInterface
    {
        $userId = $user->getId();   // now: the callback runs after the request's services are reset

        return WebSocketResponse::from($request, static function (WebSocket $ws) use ($userId) {
            foreach (Swerve::subscribe("user:$userId") as $message) {
                $ws->send($message);
            }
        });
    }
}

Any other action, on any worker, pushes to it:

Swerve::publish("user:$userId", json_encode(['text' => 'Your report is ready']));
  • Both ways: foreach ($ws as $message) receives until the client closes; send(), sendBinary() and isBinary() do the rest. See swerve's realtime guide.
  • The worker's turn: the action returns the 101 and its turn ends; the callback runs beside the requests that follow. The tests hold 250 WebSockets open on one worker and its pages are still answered promptly.
  • The user, the session, route arguments: read them in the action, before WebSocketResponse::from(), and pass the values in. CurrentUser, SessionInterface, CurrentRoute and the like belong to whichever request the worker serves at that moment: read inside the callback, CurrentUser gives the guest, or the user of another visitor's request in progress (a test shows it).
  • Leaving and stopping: when the client leaves, with or without a close frame, the callback ends, also one that only forwards a subscription. On a shutdown or reload, open sockets are closed with 1001 (going away) and the worker exits cleanly.

How it runs

PHP-FPM builds Yii's configuration and container for every request; swerve builds them once per worker.

  • Once per worker: the configuration and the container are built, the container's ErrorHandler is registered, the bootstrap-web group runs and Application::start() dispatches ApplicationStartup, as yiisoft/yii-runner-roadrunner does, with the configuration groups of public/index.php's runner.
  • Per request: Application::handle() runs the middleware stack; an exception escaping it goes to ErrorCatcher. Then AfterEmit is dispatched, StateResetter runs the reset callbacks of the container's definitions (CurrentRoute, RequestProvider, Session, CurrentUser, WebView, ...), and PHP's session_id() and $_SESSION are cleared. yiisoft/session's Flash has no reset and keeps the last session id it saw, so its messages would not expire (yiisoft/session#2); the adapter clears that id too.
  • Concurrency: one request at a time per worker (phasync\Util\Synchronized). Yii keeps a request's state in shared services of the container, and its session is PHP's native session, one per process: a container per request in flight would still share it. Streamed bodies and WebSockets go on after the request's turn, so they don't hold up the worker. Why, and what to do about it.
  • Sessions: yiisoft/session through PHP's session module, with the save handler the application configures (files, Redis, a database).

Before you deploy

  • A request that waits (a database, an HTTP API) keeps its worker's other requests waiting, also with ext-phasync. Run at least as many workers as you would run PHP-FPM children.
  • A service of your own that keeps request state needs a reset callback in its container definition, as Yii's own services have: without one, the next request sees that state.
  • A streamed body, an SSE producer or a WebSocket callback runs after the reset: take what it needs (the user id, route arguments, session values) before returning the response. Read inside it, those services show another request's state, another visitor's user included.
  • AfterEmit is dispatched when the application returns the response, before swerve sends it, not after as under PHP-FPM.
  • The application's ErrorHandler is registered for the whole worker: a PHP warning becomes an ErrorException (a 500 page in a request, as under PHP-FPM), also in a coroutine that outlives its request, such as a stream's producer.
  • exit, die and dd() end the worker, and the requests it is serving with it.
  • A stop ends the requests in flight. On a shutdown or reload swerve stops a worker at once: a request still waiting then ends, answered 503 by swerve when nothing was sent yet, not the ErrorCatcher's page (config/events-web.php of this package listens for it); clients retry.

Compatibility

Yii PHP ext-phasync
3 (yiisoft/app 1.x) 8.2 – 8.5 optional; tested with and without

License

MIT. See the Ennerd philosophy for why this stack is built to be owned.