phasync
Frameworks / PSR-15

Frameworks

PSR-15

Run Slim, Mezzio or your own PSR-15 handler on Swerve with the swerve-psr15 adapter: one boot per worker, requests overlap.

Concurrency: concurrent. swerve.psr15.php returns your Psr\Http\Server\RequestHandlerInterface, and the adapter runs it once per request in a coroutine of its own, so requests that wait overlap in one worker. What your handler, your middleware and your container keep between requests is yours to watch — the first thing to check when two requests answer each other's data.

Install and run#

composer require phasync/swerve-psr15 slim/slim slim/psr7
<?php // swerve.psr15.php

use Slim\Factory\AppFactory;
use Swerve\Swerve;

$app = AppFactory::create();                                  // slim/psr7 installed: its factories build the responses
$app->addBodyParsingMiddleware();
$app->addErrorMiddleware(true, true, false, Swerve::log());   // error pages; errors in Swerve's log
$app->get('/hello/{name}', function ($request, $response, array $args) {
    $response->getBody()->write("Hello, {$args['name']}");

    return $response;
});

return $app;
vendor/bin/swerve --http=0.0.0.0:8080 --public=public

Installing phasync/swerve-psr15 adds an adapter: with it the only one installed, vendor/bin/swerve finds it from its own composer.json and calls its entry point instead of loading swerve.php. The entry requires the application's own Composer autoloader, then swerve.psr15.php, once per worker, and keeps whatever it returns for every request that worker serves. A file that is missing, or returns anything but a RequestHandlerInterface, stops the worker with exit code 2 and says so in the log — swerve.psr15.php not found, or returned stdClass; it must return a Psr\Http\Server\RequestHandlerInterface. A Mezzio Application is a request handler too — see the Laminas page.

What the adapter builds, and what it leaves to you#

Concurrency#

Every request runs in its own coroutine. A route that waits (phasync::sleep(), phasync's own HTTP client, a socket) lets the worker answer other requests meanwhile; with ext-phasync loaded, sleep(), PDO, mysqli and curl wait that way too, without the application's code changing at all.

$app->add(function ($request, $next) {
    return $next->handle($request->withAttribute('user', $request->getHeaderLine('X-User')));
});

$app->get('/me', function ($request, $response) {
    phasync::sleep(0.2);                                        // another request runs meanwhile
    $response->getBody()->write('hello ' . $request->getAttribute('user'));

    return $response;
});

Two requests to /me, one with X-User: ann and one with X-User: bob, both answer in about 0.2 s, each with its own user — because user travels on the request's attributes, not on a property.

Server-Sent Events#

A route returns Swerve\Psr15\EventStreamResponse::stream() instead of writing a body. The callback runs after the route returns, writes with swerve's ServerSentEvents, and ends the stream by returning; a client that left makes send() throw phasync\IOException — let it leave the loop.

use Swerve\Psr15\EventStreamResponse;
use Swerve\ServerSentEvents;
use Swerve\Swerve;

$app->get('/news', function ($request, $response) {
    return EventStreamResponse::stream(function (ServerSentEvents $sse) {
        foreach (Swerve::subscribe('news', heartbeat: 15) as $message) {
            null === $message ? $sse->comment('keep-alive') : $sse->send($message, event: 'news');
        }
    }, headers: ['access-control-allow-origin' => '*']);
});

It is an ordinary 200 response underneath: a CORS or logging middleware's with*() clones it and keeps the callback, so headers added on the way out (and the ones set in headers: above) reach the client with the head. content-type, cache-control and x-accel-buffering are swerve's own, and any value your response sets for those three is ignored. Last-Event-ID is on the request as a normal header, and also on ServerSentEvents::lastEventId(). A HEAD request gets the head and no body, and the callback never runs — there is nothing to stream to.

A PSR-15 route can upgrade to a WebSocket the same way, with Swerve\Psr15\WebSocketResponse::from(): the handler it runs is swerve's own WebSocket, so swerve's realtime docs apply unchanged. Take anything you need from the request (the user, a session) before returning — the handler runs after the route, over the request's body, and does not see the request object itself.

What stays per request, what doesn't#

Each request runs in a Scope of its own (Scope::getCurrent()), shared by the coroutines it starts and freed when the request ends. Request attributes live there; so does anything you key by Scope::getCurrent() yourself when attributes don't fit. Everything else your swerve.psr15.php builds — the Slim app, a container you attach with AppFactory::setContainer(), compiled routes, static properties, globals — belongs to the worker, not the request, and is shared by every request running in it at once, including concurrently. A container entry that is safe to share (a template engine, a route cache) is fine to build once there; one that isn't (anything holding the current user, a query builder with state pending between calls) needs to be built per request or kept off the container entirely, in favor of a request attribute.

date_default_timezone_set() and Locale::setDefault() follow the request's Scope, so a request that changes them affects only itself. setlocale() does not: it is process state in PHP itself. Use Locale::setDefault() and intl formatters instead; neither Slim nor this adapter calls setlocale().

Dangerous under concurrency#

Your application#

Slim, Mezzio and any other PSR-15 stack run requests concurrently in one worker, so your own code decides whether that is safe: middleware and handlers that keep request state in properties, and container definitions that hand every request the same instance (a PDO, a "current user" service), are what breaks. The framework running correctly on Swerve does not make your own code safe: see Making your application safe for concurrent requests for the checklist and a fix for each item.

Debugging#

See also#

  • Swerve::logThe log of this process: swerve's own PSR-3 logger, in its format (the time, this worker's slot).
  • phasync::onScopeChangeRegisters $fn to be called when the current Scope changes as the event loop switches coroutines.