Search by

fluffydiscord / rapira-symfony-bundle

FluffyDiscord

Rapira runtime for Symfony

Package info

github.com/FluffyDiscord/rapira-symfony-bundle

Type:symfony-bundle

pkg:composer/fluffydiscord/rapira-symfony-bundle

Statistics

Installs: 378

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.3 2026-11-04 12:19 UTC

This package is auto-updated.

Last update: 2026-11-04 12:36:08 UTC


README

Symfony runtime for Rapira. HTTP only.

DDEV users: see DDEV add-on.

Features

Requirements

  • PHP >= 8.4
  • Rapira >= 0.9.2
  • Symfony ^7.4 || ^8

Installation

composer require fluffydiscord/rapira-symfony-bundle

config/bundles.php:

 return [
+    FluffyDiscord\RapiraBundle\FluffyDiscordRapiraBundle::class => ['all' => true],
 ];

Usage

  1. Set the runtime:
composer config extra.runtime.class 'FluffyDiscord\RapiraBundle\Runtime\Runtime'
composer dump-autoload
  1. Swap the kernel trait:
- use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
+ use FluffyDiscord\RapiraBundle\Kernel\RapiraMicroKernelTrait;

class Kernel extends BaseKernel
{
-    use MicroKernelTrait;
+    use RapiraMicroKernelTrait;
}
  1. rapira.toml — point entrypoint at your public/index.php:
[http]
listen = "0.0.0.0:8000"

[http.pool]
entrypoint = "public/index.php"
mode = "dispatcher"
processes = 4
rapira serve rapira.toml

Full sample: rapira.toml.

public/index.php and bin/console stay untouched. Console and classic mode run the stock Symfony runner.

Rather not touch composer.json? Set APP_RUNTIME on the Rapira process (Dockerfile ENV, compose environment). .env is read too late for it.

dev vs prod

mode Code changes
dev classic picked up on the next request
prod dispatcher restart Rapira

rapira.dev.toml:

[http]
listen = "0.0.0.0:8000"

[http.pool]
entrypoint = "public/index.php"
mode = "classic"
processes = 2
rapira serve rapira.dev.toml

Leave out [http.uploads] here. Rapira won't start with it outside dispatcher mode.

Configuration

config/packages/rapira.yaml

rapira:
    warmup:
        enabled: true
        learn: true
        learn_requests: 30
        manifest_path: ~
    doctrine:
        preconnect: false
    session:
        keep_connection: true
    profiling:
        xhprof:
            enabled: false
            output_dir: ~
    vips:
        enabled: auto
        max_operations: 50
        max_memory_mb: 50
        max_files: 20
Option Default Meaning
warmup.enabled true Master switch for the warmers and the recorder. See Worker warmup.
warmup.learn true Record which classes and cache files real responses load, replay them at every later worker boot.
warmup.learn_requests 30 Stop recording after this many responses per worker.
warmup.manifest_path null null = <kernel.cache_dir>/rapira/warmup.manifest.json. Point outside the cache dir to keep learning across deploys.
doctrine.preconnect false Open PostgreSQL/MySQL/MariaDB connections at worker boot, check them at every request start. Needs doctrine/dbal. See Database connections.
session.keep_connection true Keep the PdoSessionHandler connection open between requests; reconnect and retry when it dies.
profiling.xhprof.enabled false true = profile every request. auto = only with kernel.debug on. Both need ext-xhprof.
profiling.xhprof.output_dir null null = ini xhprof.output_dir, then <sys_get_temp_dir>/xhprof.
vips.enabled auto Cap libvips's process-wide cache at worker boot. auto = when jcupitt/vips is installed and FFI works in the worker (ffi.enable=1); otherwise no worker can load libvips, so nothing to cap. true = also log why it can't.
vips.max_* 50 / 50 / 20 libvips cache limits: operations, memory (MB), open files.

Worker warmup

The bundle warms during worker boot, before Rapira sends the first request. Zero config:

  1. Generic warmers — router, Doctrine metadata, event listeners, form types, Twig runtimes, container preload class list. Missing dependencies are skipped.
  2. Learned manifest — workers record what real traffic loads; every next worker replays it at boot. Invalidated when the container is rebuilt.

dispatcher mode only — the runtime sets APP_RUNTIME_MODE=web=1&worker=1, whatever .env says. classic mode keeps nothing warm, so warmup switches itself off.

Warmed classes live in each worker's opcache — budget opcache.memory_consumption × processes.

opcache.preload works too: point it at config/preload.php, from Symfony's recipe. The container preload warmer isn't needed then.

Running Rapira as root? Also set opcache.preload_user.

Warming your own services

use FluffyDiscord\RapiraBundle\Warmup\WorkerWarmerInterface;

class MyCacheWarmer implements WorkerWarmerInterface
{
    public function __construct(private readonly MyExpensiveService $service)
    {
    }

    public function warmup(): void
    {
        $this->service->buildInMemoryIndexes();
    }
}

Autoconfigured. Or listen to WorkerBootingEvent.

Response/file streaming

StreamedResponse and StreamedJsonResponse stream as they go and send X-Accel-Buffering: no. Callbacks must echo — a \Generator callback never runs:

 return new StreamedResponse(
-    function (): \Generator {
-        yield "data";
+    function (): void {
+        echo "data";
     }
 );

BinaryFileResponse inside the [http.sendfile] root → Rapira sends it straight from disk. Outside it, or with deleteFileAfterSend() → streamed through PHP.

Request data

Superglobals aren't per-request. Use the Request:

  • $_GET, $_POST, $_COOKIE, $_FILES are empty.
  • $_SERVER holds the process env, never the current request.
  • echo / header() outside a streamed callback are dropped — return a Response.
  • With framework.http_cache on, a Request you keep after the response is emptied. Copy what you need first.

Uploads

Rapira parses multipart bodies, you get regular UploadedFiles. move() works as usual.

[http.uploads]
dir = "/tmp"
max_file_size_mb = 16
max_files = 20

Over the limit → 413.

Error handling

Failure dev (kernel.debug) prod
exception in your code Symfony's exception page Symfony's error page
exception escaping Symfony HtmlErrorRenderer page bare 500, empty body
exception after headers were sent response cut off response cut off
  • Escaping exception → kernel reboots; the next request gets a clean container.
  • Details go to the Rapira log and Sentry if installed.
  • Over max_execution_time or memory_limit → 502; the next request works.

max_execution_time only counts CPU time. A stalled database or HTTP call keeps the worker busy, and requests queue behind it. Cap it:

rapira.toml

[http.pool]
request_terminate_timeout_secs = 60

Over the limit → Rapira kills and replaces the worker; that client's connection drops.

Uploads of a killed worker stay in rapira-spool-<pid> under [http.uploads] dir until Rapira restarts.

Not covered:

  • kernel boot failure — no error page, just a PHP fatal in the Rapira log
  • Rapira worker mode — stock Symfony runner, no worker loop; use dispatcher
  • a custom or decorated http_cache.store — its in-memory key map isn't cleared between requests
  • duplicate backend calls for an uncached URL with framework.http_cache — Symfony only locks stale entries, so each worker can call your app at once

Sentry

composer require sentry/sentry-symfony

Configure as usual.

Database connections

Each worker keeps one connection per Doctrine connection plus one for sessions if using PDO sessions, and replaces them when the server drops them. PostgreSQL, MySQL and MariaDB.

Stock Symfony This bundle
Doctrine connection opened on first query doctrine.preconnect: true → opened at worker boot
Doctrine, server dropped the connection first query of the next request fails doctrine.preconnect: true → SELECT 1 at request start, reconnect before your code runs
PdoSessionHandler new connection every request one persistent connection per worker
PdoSessionHandler, server dropped the connection session request fails reconnect, re-run the failed query, carry on
PdoSessionHandler::LOCK_ADVISORY, failed request lock dies with the connection lock released before the retry

Turn off DoctrineBundle's idle timeout. Otherwise it closes the connection 10 minutes after connecting:

config/packages/doctrine.yaml

 doctrine:
     dbal:
         url: '%env(resolve:DATABASE_URL)%'
+        idle_connection_ttl: 0

Sessions are covered when framework.session.handler_id is a DSN ('%env(DATABASE_URL)%') or your own PdoSessionHandler service built from a DSN.

Pass a DSN, not a \PDO instance. The bundle can't reopen a \PDO you created.

Not covered:

  • a connection that dies mid-request in Doctrine — DBAL reconnects on the next query, the failed query isn't retried

Events

Event Payload
WorkerBootingEvent —
WorkerRequestReceivedEvent Rapira\Http\Request
WorkerResponseSentEvent Symfony Request + Response
WorkerRequestFailedEvent Rapira\Http\Request + throwable

Developing with Symfony and Rapira

Same rules as RoadRunner — see Developing with Symfony and RoadRunner: stateless services, static form defaults, lean User session serialization.

DDEV add-on

ddev add-on get FluffyDiscord/ddev-rapira

See the add-on repository for configuration and usage.

Testing

tests/docker-qa.sh                # Mago analyzer + PHPUnit
tests/docker/run-integration.sh   # integration suite against the real Rapira binary