tereta / runtime
Runtime with strategies for different server and request types
Requires
- php: >=8.2
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/log: ^3.0
Requires (Dev)
- http-interop/http-factory-tests: ^2.2
- php-http/psr7-integration-tests: ^1.4
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.10
This package is not auto-updated.
Last update: 2026-08-09 11:32:28 UTC
README
π English | Π ΡΡΡΠΊΠΈΠΉ | Π£ΠΊΡΠ°ΡΠ½ΡΡΠΊΠ°
A runtime with strategies for different server and request types: a single kernel implementing PSR-15. Handlers on top of PHP-FPM/CGI and Swoole, without changing the application code.
Table of contents
Requirements
PHP 8.2+, implementations of the PSR-7/PSR-15/PSR-17 interfaces (shipped with the package),
the swoole extension is needed only for the swoole strategy.
Installation
composer require tereta/runtime
Quick start
The application is described once - as a PSR-15 handler, and the way it is launched is set by the kernel strategy.
The very same handler works both under PHP-FPM/CGI/ApacheModule with the Request strategy and under Swoole: only the strategy() line changes.
For testing there is the Tereta\Runtime\Handlers\Closure decorator, which wraps a closure into a PSR-15 handler.
A minimal handler that responds to the user with the request method and URI as plain text:
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tereta\Runtime\Factories\Response as ResponseFactory;
use Tereta\Runtime\Factories\Stream as StreamFactory;
use Tereta\Runtime\Handlers\Closure as ClosureHandler;
$handler = new ClosureHandler(function (ServerRequestInterface $request): ResponseInterface {
return (new ResponseFactory())
->createResponse(200)
->withHeader('Content-Type', 'text/plain')
->withBody((new StreamFactory())->createStream(
sprintf("%s %s\n", $request->getMethod(), $request->getUri())
));
});
Tereta\Runtime\Handlers\Closure is a decorator for a quick start: it adapts a closure to Psr\Http\Server\RequestHandlerInterface (PSR-7).
In a real application you would normally implement Psr\Http\Server\RequestHandlerInterface directly in your own class.
For example (from the tereta/application package):
use Tereta\Route\Router;
use Tereta\Runtime\Kernel as RuntimeKernel;
$route = new Router(new RouterContext(new ResponseFactory, new StreamFactory));
$route->addHandler(HelpHandler::class)
->addRoute(RouteAttribute::class)->addRoute(RouteDatabase::class);
$route->context->container->set('orm', $orm);
(new RuntimeKernel)
->configure('development', true)
->run($route);
Where Router is an implementation of the Psr\Http\Server\RequestHandlerInterface interface.
A PSR-7 handler (controller) can also be implemented and processed through the Tereta\Runtime\Kernel->run method directly
Running under PHP-FPM
Tereta\Runtime\Strategies\Request is the strategy for the classic "one process - one request" model: PHP-FPM, CGI, Apache Module, the built-in php -S server.
The request strategy builds Psr\Http\Message\ServerRequestInterface from the superglobals ($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES) and the php://input body, and emits the response through header() and output to stdout.
Entry point public/index.php:
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use Tereta\Runtime\Kernel as RuntimeKernel;
(new RuntimeKernel())->run($handler);
No extra configuration is required - the strategy takes the data from the request environment. You can run it through FPM/CGI/Apache, or with the built-in PHP server for quick debugging:
php -S 127.0.0.1:8080 -t public
Running under Swoole
Tereta\Runtime\Strategies\Swoole - a long-living process - is the main goal of the package: running a PSR-15 handler in a resident Swoole HTTP server.
The swoole strategy brings up a resident HTTP server: the process lives between requests, the handler is created once, and Psr\Http\Message\ServerRequestInterface is built from the Swoole request object.
The swoole extension is required; without it the strategy throws a Tereta\Runtime\Exceptions\Runtime exception on startup.
Entry point runtime.php:
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Tereta\Runtime\Kernel as RuntimeKernel;
/**
* Global scope: the code of this file runs once when the server starts,
* and everything created here lives until it is stopped - application configuration and context, DI container, logger,
* PSR-17 factories, database connections and pools, cache, compiled routes.
*/
/**
* Request scope:
* the handler itself is created here once, but its handle() is invoked anew for every client request - everything
* that belongs to a particular request is created inside the method and does not outlive the response.
*
* @var Psr\Http\Server\RequestHandlerInterface $handler your PSR-15 handler
*/
(new RuntimeKernel())
->strategy('swoole')
->configure('host', $_ENV['HOST'] ?? '0.0.0.0')
->configure('port', (int) ($_ENV['PORT'] ?? 80))
->run($handler);
It is launched as an ordinary CLI process that does not exit while the server is running:
php runtime.php
By default host = 0.0.0.0 and port = 80 are used; fine-grained server parameters are passed via the settings key β see Configuration.
What is created before run() and what inside the handler
Swoole is a long-living process: it starts once and then serves all requests until it is stopped.
So the file's code runs once, at startup, while the handler is invoked on every request.
That means everything shared is prepared before run(), and everything that belongs to a particular HTTP client is created inside the handler.
- Before
run()- this is the scope needed by all requests. The database connection or connection pool, DI container, configuration, logger, PSR-17 factories, cache, routes. It is created once and then reused - which is, in fact, the whole point of Swoole. - Inside the handler - what the client request needs. Who has just arrived, their session, form data, an open transaction, temporary objects. All of that belongs to a single request and must be cleaned up after the response.
Why this matters exactly here. Under PHP-FPM the process dies right after the response, so a forgotten variable bothers nobody. In Swoole the process keeps living and serves the next client. If the data of one request is stored in a static property, in a container singleton or in a global variable, the next person on the site will see it. The same goes for resources: if a transaction was not closed or a connection was not returned to the pool, they will hang around until the server is restarted.
A separate note about database connections. Inside run() Swoole starts the server and spawns several worker processes - copies of the current one.
A connection opened one line above ends up in each of them, and all of them start writing into the same socket: responses get mixed up, queries fail.
The connection must be opened inside the worker process, and the simplest way is to defer opening it until the first use.
License and author
Tereta Alexander tereta.alexander@gmail.com Web: https://tereta.dev Copyright Β©2008-2026 Tereta Alexander License https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
www.ββββββββββββββββββββββββ βββββββββββββββββ ββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββ
βββ ββββββ ββββββββββββββ βββ ββββββββ
βββ ββββββ ββββββββββββββ βββ ββββββββ
βββ βββββββββββ βββββββββββ βββ βββ βββ
βββ βββββββββββ βββββββββββ βββ βββ βββ
.dev