Search by

linkfast / exengine-roadrunner

gchiappe

RoadRunner application server support for the ExEngine microframework: long-running HTTP workers.

Package info

gitlab.com/linkfast/oss/exengine-extensions/exengine-roadrunner

Issues

Type:exengine-extension

pkg:composer/linkfast/exengine-roadrunner

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

1.0.0-a1 2026-10-12 01:11 UTC

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; enable extension=sockets in php.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 Resettable to clear it after each request.
  • A service cannot depend on Http\Request, directly or through other components (ExEngine refuses to create it); call ExEngine::currentRequest() when the request is needed.
  • Other components a service depends on are created once with it.
  • Without RoadRunner (for example with exen serve on PHP's built-in server) services are still shared within one ExEngine instance, which there means one request.

Configuration

KeyDefaultDescription
roadrunner.script-name/api.phpLauncher path accepted as URL prefix; empty to route only URLs without it
roadrunner.workers0Number of PHP workers; 0 starts one per CPU core
roadrunner.max-jobs0Requests a worker answers before it is replaced (frees leaked memory); 0 for no limit
roadrunner.max-memory0Memory 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.