linkfast / exengine-roadrunner
RoadRunner application server support for the ExEngine microframework: long-running HTTP workers.
Package info
gitlab.com/linkfast/oss/exengine-extensions/exengine-roadrunner
Type:exengine-extension
pkg:composer/linkfast/exengine-roadrunner
Requires
- php: >=8.2
- ext-sockets: *
- linkfast/exengine: ^1.3.0-a1
- nyholm/psr7: ^1.8
- spiral/roadrunner-http: ^3.6
- spiral/roadrunner-worker: ^3.6
Requires (Dev)
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-12 11:07:08 UTC
README
Runs ExEngine applications on the RoadRunner application server: long-running PHP workers that boot ExEngine once and answer many requests.
© LinkFast S.A. (https://linkfast.io) · MIT License
Requirements
- PHP 8.2 or higher with
ext-sockets(bundled with PHP; enableextension=socketsinphp.ini) - ExEngine 1.3.0-a1 or higher
- A RoadRunner server binary (
rr), version 3 (tested with 3.0.0)
Getting started with exen
The exen CLI sets everything up:
exen extension:add linkfast/exengine-roadrunner # this package
exen rr:install # downloads the rr binary for your system
exen rr:init # writes worker.php and .rr.yaml
exen serve --rr # RoadRunner with reload on code changes
Manual setup
composer require linkfast/exengine-roadrunner:^1.0.0-a1
worker.php, next to composer.json:
<?php
require __DIR__ . '/vendor/autoload.php';
const EXENGINE_LOG = true;
exit(ExEngine\RoadRunner\Worker::run(new App\AppConfig(__DIR__ . '/public')));
.rr.yaml:
version: '3'
rpc:
listen: tcp://127.0.0.1:6001
server:
command: "php worker.php"
http:
address: 0.0.0.0:8080
middleware: ["static"]
static:
dir: "public"
forbid: [".php", ".htaccess"]
pool:
num_workers: 0 # 0 = one per CPU core
max_jobs: 500 # restart each worker after 500 requests
Then run rr serve.
How it works
Worker::run() boots ExEngine with ExEngine::create() and, for each request RoadRunner sends to the process,
converts it to an ExEngine\Http\Request, calls ExEngine::handle(), sends the response and calls
ExEngine::terminate(). When RoadRunner stops the worker, ExEngine::shutdown() runs.
Because the process stays alive, code changes need a worker reset (rr reset, which exen serve --rr does for
you), and state kept in static properties or singleton components is shared by the requests a worker answers.
URLs work with and without the launcher: /api.php/Hello/world and /Hello/world both reach Hello::world(),
and links keep the form the client used.
Services
By default ExEngine creates a component registered as a class name on every injection, so in a worker each
request builds it again. Mark the components that can be shared, such as database connections, HTTP clients or
caches, with #[Service]. A service is created once per worker and reused by every request it answers:
use ExEngine\Component;
use ExEngine\RoadRunner\Resettable;
use ExEngine\RoadRunner\Service;
#[Service]
final class Database extends Component implements Resettable
{
private PDO $pdo;
public function __construct(DatabaseProperties $properties)
{
$this->pdo = new PDO($properties->dsn, $properties->user, $properties->password); // once per worker
}
public function reset(): void
{
// Called after every request: discard per-request state (e.g. roll back an open transaction).
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
}
}
Register it as usual ($this->component(Database::class)). Components can also be made services without the
attribute, by name or class in roadrunner.services. Rules:
- A service must not keep data of one request for the next: implement
Resettableto clear it after each request. - A service cannot depend on
Http\Request, directly or through other components (ExEngine refuses to create it); callExEngine::currentRequest()when the request is needed. - Other components a service depends on are created once with it.
- Without RoadRunner (for example with
exen serveon PHP's built-in server) services are still shared within one ExEngine instance, which there means one request.
Configuration
| Key | Default | Description |
|---|---|---|
roadrunner.script-name | /api.php | Launcher path accepted as URL prefix; empty to route only URLs without it |
roadrunner.workers | 0 | Number of PHP workers; 0 starts one per CPU core |
roadrunner.max-jobs | 0 | Requests a worker answers before it is replaced (frees leaked memory); 0 for no limit |
roadrunner.max-memory | 0 | Memory limit of a worker in MB, replaced when over it; 0 for no limit |
roadrunner.services | [] | Components kept for the life of the worker (names or classes), besides #[Service] |
workers, max-jobs and max-memory size RoadRunner's pool: exen serve passes them to RoadRunner and
exen rr:init writes them to .rr.yaml, so they can differ per profile (application-prod.yml). Other server
settings (address, static files, timeouts) belong to .rr.yaml; see the
RoadRunner documentation.
Development
Tests need PHP 8.4+ (PHPUnit 13) with ext-sockets. The end-to-end test runs when RR_BINARY points to an rr
binary:
composer install
RR_BINARY=/path/to/rr php vendor/bin/phpunit
License
MIT, see LICENSE.