Skip to content

Framework integration ​

A framework application runs without changes in Classic mode. Configure Rapira to use the existing entry script. In Worker mode, the PHP process stays active between requests. The framework design determines which application state can stay in memory. This page describes the rules for all frameworks. The framework guides describe only framework-specific behavior.

Verified with

  • PHP 8.5.8, NTS, embed SAPI
  • Rapira 0.8.0
  • Symfony 7.4.15 and 8.1.2, Yii3 app template 1.4 (yii-runner-http 3.2.1)

Tests ran these applications on Linux with one worker process. Framework statements on this page come from those tests. The examples on this page use the v0.9 configuration format. See configuration for the Rapira settings.

Classic and Worker modes ​

Classic mode uses the existing entry script. It starts a new PHP request for each HTTP request. A framework that runs under php-fpm can also run in this mode. See Classic mode for more information. Only the static files, TLS, and OPcache sections below apply to Classic mode.

Worker mode keeps the process active. The script initializes the application and requests work in a loop. The application state stays between requests. See execution modes and Worker mode for more information.

One codebase can use both modes. Keep public/index.php. Add worker.php to the project root. http.pool.mode selects the execution mode, and http.pool.entrypoint selects the script. Classic mode remains available if a Worker mode migration fails.

Each worker uses the directory of its entry script as the working directory. Thus, relative file paths in worker.php resolve against the project root. Relative file paths in public/index.php resolve against public/. A chdir() call in PHP code stays in effect until the worker process exits.

Worker loop ​

Each framework uses the same basic worker script:

php
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';

$app = new App(); // The worker creates this object once and reuses it.

$handler = static function () use ($app): void {
    header('Content-Type: text/plain');
    http_response_code(200);
    echo $app->handle($_SERVER['REQUEST_URI']);
};

while (\Rapira\handle_request($handler)) {
    gc_collect_cycles();
}

The script contains these operations:

  • require .../vendor/autoload.php registers the autoloader. The autoloader and the loaded classes stay available until the worker script restarts.
  • $app = new App(); initializes the application before the loop. Symfony keeps a persistent kernel here. Yii3 can keep a persistent runner or create a runner inside the handler. Each guide shows the initialization and the request cleanup.
  • $handler = static function () use ($app): void defines a handler without arguments. The handler reads request data from the superglobals. It gets other dependencies through use.
  • header(), http_response_code(), and echo create the response as in a classic script. See HTTP for response transmission.
  • while (\Rapira\handle_request($handler)) waits for a request. handle_request() fills the superglobals, runs the handler, and completes the request. It returns true after a request and false when the worker stops. Call it only from the top-level script loop. Outside Worker mode, it throws Rapira\Exception\NotInWorkerModeError.
  • gc_collect_cycles(); collects reference cycles between requests. It does not correct memory leaks. See Memory and recycling.

Inside the handler, Rapira sets SCRIPT_NAME to /worker.php because worker.php is the entry script. DOCUMENT_ROOT contains the script directory. REQUEST_URI contains the client path. Symfony and Yii3 routed requests and generated URLs correctly with these values. The generated URLs did not contain worker.php. Before you integrate another framework, check whether it builds URLs from SCRIPT_NAME instead of REQUEST_URI.

Before the first request, $_SERVER contains the process environment. At that time, SCRIPT_NAME contains the absolute script path, and DOCUMENT_ROOT is empty. The $_SERVER of a request does not contain the process environment. Read environment variables before the loop, or use getenv() in the handler. Do not calculate a URL prefix from $_SERVER before the loop. See $_SERVER before the first request.

Per-request and resident state ​

Rapira rebuilds everything in the left column for every request. Ordinary PHP code can continue to read these values. Everything in the right column remains between requests. The worker script must manage this state.

New for every requestRemains between requests
$_GET, $_POST, $_SERVER, $_COOKIE: Rapira refills them with request data. $_SERVER does not contain the process environmentThe Composer autoloader and each class that it loaded
php://input: the raw request body, CONTENT_TYPE, and CONTENT_LENGTHstatic properties and variables, which keep values across requests
$_FILES and the uploaded temporary filesObjects created before the loop, such as the container, kernel, and application
Session data: session_start(), the request cookie, and the response Set-Cookie fieldOpen resources: database handles, cache clients, streams
Response state: status code, headers, setcookie(), and output buffersThe process: the same pid and one resident PHP interpreter for each worker
Shutdown functions registered inside the handler$_ENV values loaded before the loop
The max_execution_time clock, re-armed for each request

Rapira starts a new max_execution_time timer for each request. The time that a worker waits for a request does not count toward this limit.

The following three behaviors apply to a persistent worker.

A resident object keeps its state between requests

PHP does not call the destructor of a persistent object at the end of a request. It calls the destructor once when the worker cycle ends, or when code removes the last reference.

Do not use a destructor for per-request cleanup. Reset per-request state inside the handler.

An initialization shutdown function runs once at the end of the worker cycle

PHP runs each shutdown function that code registers outside the handler once at the end of the worker cycle. PHP runs each function that the handler registers at the end of that request.

Register request shutdown functions inside the handler. Examples include metric output, fatal error processing, and request resource cleanup.

$_ENV remains between requests

Rapira does not rebuild $_ENV for each request. Values that code writes to $_ENV before the loop stay until the worker script restarts. Load environment configuration before the loop. Do not store request data in $_ENV.

A change to $_ENV does not change the process environment. Use putenv() when getenv() or child processes must see a value. In production, set environment variables in the service unit, container, or orchestrator.

Error handling ​

Tests confirmed three failure types with one worker:

  • exit or die inside the handler sends the current status and output. The process does not stop, and the worker continues to accept requests. For example, a framework can use exit for a maintenance response.
  • An uncaught exception returns 500 if PHP sent no output before the exception. After output, the status that PHP sent stays. A framework exception handler can return its own error page. Without such a handler and with display_errors off, the body is empty. The worker continues to accept requests.
  • An uncaught Error has the same result. For both types, PHP writes an Uncaught log record.

The worker errors counter increases when no exception handler catches the exception or Error. An exit request increases only handled. In all three cases, recycles stays zero.

A bailout-class fatal error ends the persistent script. The worker then starts the script again and initializes the application. This restart increases recycles. The process model status output shows these counters.

Static files ​

Rapira serves static assets with the static file middleware. Set [http.static].root to the framework public/ directory. Add the middleware to [http]:

toml
[http]
middleware = ["static"]

[http.static]
root = "public"

The middleware returns a response only when a path matches a file under the root. Its default forbid list blocks .php files, so it does not serve the entry script as a file. Other URLs run the entry script. Directory URLs also run the entry script, because the middleware does not serve index files. $_SERVER['REQUEST_URI'] contains the client path.

A CDN or reverse proxy can serve the assets instead. See Running in production for reverse proxy configuration.

TLS and proxies ​

Rapira accepts only plain HTTP and has no TLS settings. Terminate TLS at a proxy. Connect the proxy through loopback or a Unix socket. $_SERVER['HTTPS'] is always empty, and $_SERVER['REQUEST_SCHEME'] is always http. Configure the trusted proxies of the framework so that it reads X-Forwarded-Proto. Without this configuration, the framework generates http:// URLs.

Use hyphens, not underscores, in forwarded field names, because both characters can map to the same $_SERVER key. See HTTP and Running in production.

Memory and recycling ​

A worker can create the application inside the handler. The application then stays in memory for one request. The worker keeps less state than with a persistent Symfony kernel, but more than in Classic mode. Move the initialization out of the handler only after you identify the persistent state.

In this design, each request creates an object graph. Reference cycles can keep old graphs until the cycle collector runs. Memory use then increases for several requests and decreases when PHP releases many graphs. This pattern is not necessarily a memory leak. However, peak memory can be much larger than the memory for one request.

Tests found that gc_collect_cycles() in the loop or handler did not prevent this pattern. Later initialization can keep references to old graphs. The collector cannot release a graph while another object references it. Set memory_limit above the measured peak. Also set a worker replacement limit:

toml
[http.pool]
max_requests = 100

Each worker stops after it serves more than max_requests requests, and the master starts a replacement. Each worker uses its own limit, from max_requests + 1 to max_requests plus half of that value. Thus, workers do not stop at the same time. Tests sent hundreds of requests during several replacements. Memory returned to its initial level, and each request returned 200. This setting limits the memory peak of this pattern.

Persistent Symfony and Yii3 applications had stable memory use during the same tests. Keep worker replacement enabled to limit unexpected memory growth. See configuration and process model for more information.

OPcache and changed code ​

Rapira starts PHP once in the master before it creates workers. OPcache creates one shared memory segment, and each worker inherits the same mapping. Compiled scripts stay cached across requests and workers in all modes.

On PHP 8.4, OPcache is a separate opcache.so file that needs a zend_extension line in php.ini. See php.ini.

In production, opcache.validate_timestamps = 0 removes the file check from each request. This setting prevents automatic cache invalidation. The OPcache segment belongs to the master and remains during worker replacement. Thus, a deployment requires a complete restart. See running in production for the sequence.

During development, a persistent application does not read its initialization code again. This behavior does not depend on OPcache. After changes to the worker script or initialized services, press Ctrl-C. Then run rapira serve rapira.toml again.

Framework guides ​

  • Symfony: The kernel initializes once and stays in memory. services_resetter resets stateful services between requests. One worker file supports Symfony 7.4 and 8.1.
  • Laravel: Classic mode runs the standard public/index.php without changes. Worker mode is under development: Rapira does not yet provide the required Octane driver.
  • Yii3: StateResetter resets a persistent container after each request. Alternatively, the worker can create a new runner for each request.

Other frameworks can use the same basic worker script. Use Worker mode only if the application can process several requests in one process. First, create the application inside the handler. This design does not require framework support for persistent processes.

Validate the application in this design. Then keep the application in memory. Reset its request state after each request. Use Classic mode if neither Worker design operates correctly.