tenma / psr
Runs any PSR-15 app under Tenma: PSR-7 requests in, Tenma responses out, WebSockets and streams included.
Requires
- php: >=8.5
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
Requires (Dev)
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.3
- tenma/stubs: ^0.1.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Runs any PSR-15 app under Tenma: Slim, Mezzio, Waypoint, or a handler of your own.
Requests arrive as PSR-7 ServerRequestInterface, built with the PSR-17 factory you choose; the app's
ResponseInterface goes back as Tenma's response.
composer require tenma/psr nyholm/psr7
use Nyholm\Psr7\Factory\Psr17Factory; use Tenma\Psr\Psr; $app = /* any Psr\Http\Server\RequestHandlerInterface, e.g. a Slim app */; Tenma::serve(3000, Psr::handler($app, new Psr17Factory()));
tenma index.php
What arrives
- Method, URI (
http://+ Host header, or the address the connection arrived on), headers, cookies and query. - The raw body as a stream; for
application/x-www-form-urlencodedandmultipart/form-dataalso the fields as the parsed body and the uploads asUploadedFileInterface. JSON is left to the app, as PSR-7 says (Slim'sBodyParsingMiddleware, for instance). - Server params as under FPM:
REMOTE_ADDR,REMOTE_PORT,SERVER_ADDR,SERVER_PORT,REQUEST_URI,QUERY_STRING,REQUEST_TIME(_FLOAT).
TLS ends at a proxy in front of Tenma; the scheme is always http and X-Forwarded-* headers are not
trusted. Use a middleware that trusts your proxy if you need them.
What goes out
A body up to 1 MiB goes out in one piece; a bigger one, or one of unknown size, is read and sent in 64 KiB
chunks, so a file stream doesn't end up in memory. Several values of a header (Set-Cookie) stay separate.
WebSockets and streams
PSR-7 has no word for them. Upgrade is a response that carries one of Tenma's:
use Tenma\Http\{EventStream, Response, WebSocket}; use Tenma\Psr\Upgrade; $app->get('/ws', fn ($request, $response) => new Upgrade($response, Response::websocket(function (WebSocket $socket) { while (($message = $socket->receive()) !== null) { $socket->send("echo: $message"); } }))); $app->get('/events', fn ($request, $response) => new Upgrade($response, Response::events(function (EventStream $events) { $events->send('hello'); })));
Middlewares can change it like any response and it stays an Upgrade. A stream keeps the headers they add
(CORS, security headers, cookies); its own headers and status win. A WebSocket answers with its handshake
only. Another server than Tenma doesn't know Upgrade and sends the wrapped response.
Long-running and concurrent
Tenma keeps the app in memory and serves many requests at once, each in a Fiber of its own. That is fast, and it changes what an app may do:
- State in globals, statics or singletons is shared by every request in the process, at the same time. An
app that keeps the current user or request there needs
--workers Nand one request per worker, or a fix. $_GET,$_POST,$_SESSION,header(),echoandexitbelong to classic PHP; a PSR app uses the request and response objects instead.- Blocking calls (PDO, curl,
file_get_contents) hold up every request of the worker while they wait. Tenma's own APIs (Tenma::fetch(),Tenma\Db,Tenma\Fs) wait without blocking.
Tests
composer install composer test # starts tests/fixtures/app.php under tenma; skipped without it (TENMA_BIN) composer stan
License
MIT or Apache-2.0, at your option.