prsw / amphp-bundle
Symfony bundle providing an AMPHP HTTP server runtime
Package info
github.com/praswicaksono/amphp-bundle
Type:symfony-bundle
pkg:composer/prsw/amphp-bundle
Requires
- php: >=8.4
- ext-pcntl: *
- ext-posix: *
- amphp/cluster: ^2.0
- amphp/file: ^3.2
- amphp/http-server: ^3.0
- amphp/http-server-form-parser: ^2.0
- amphp/http-server-router: ^2.0
- amphp/http-server-static-content: ^2.0
- amphp/socket: ^2.0
- amphp/websocket-server: ^4.0
- monolog/monolog: ^3.0
- psr/log: ^3.0
- symfony/dotenv: ^8.0
- symfony/http-foundation: ^8.0
- symfony/http-kernel: ^8.0
Requires (Dev)
- amphp/http-client: ^5.0
- carthage-software/mago: ^1.27.1
- doctrine/orm: ^3.0
- phpunit/phpunit: ^11.0
- symfony/framework-bundle: ^8.0
- symfony/yaml: ^8.0
Suggests
- amphp/http-client: Enables non-blocking HTTP client for Symfony Mailer (replaces CurlHttpClient)
- amphp/mysql: Enables async MySQL database driver for Doctrine DBAL
- amphp/postgres: Enables async PostgreSQL database driver for Doctrine DBAL
- amphp/redis: Enable async Redis driver for cache and session handler
This package is auto-updated.
Last update: 2026-05-16 17:29:19 UTC
README
Replaces PHP-FPM with a long-running async HTTP server for Symfony. All I/O is non-blocking and fiber-based. No more PHP-FPM process overhead.
Features
- Async HTTP server — built on amphp/http-server
- Async Doctrine — non-blocking MySQL/PostgreSQL driver with connection pooling
- WebSocket server — attribute-driven endpoints, no Symfony boot for handshake
- SSE streaming — server-sent events via
SseResponse - Generator-based streaming — async
StreamedResponsewithyield - Hot reload —
--dev --watchrestarts on file changes (requiresfswatch) - Cluster mode — multi-worker with process supervision and graceful restart
- Prometheus metrics —
/metricsendpoint withMetricCollectorInterface - Liveness / Readiness probes —
/healthz(no Symfony boot),/readyz(full stack) - Async Redis — cache, session handler, messenger transport
- Async Mailer — SMTP + HTTP via
amphp/http-client
Quick Start
1. Install
composer require prsw/amphp-bundle "@dev"
Register the bundle if you are not using symfony flex in config/bundles.php:
return [ PRSW\AmphpBundle\AmphpBundle::class => ['all' => true], ];
2. Start the Server
# Production — cluster mode with process supervision php bin/console amphp:start --workers=4 # Development — single worker, hot reload php bin/console amphp:start --dev --watch
Press Ctrl+C to stop.
3. Add a WebSocket Endpoint
Create a class with the #[WebsocketEndpoint] attribute — no YAML needed:
use Amp\Websocket\Server\WebsocketClientHandler; use Amp\Websocket\Server\WebsocketClientGateway; use Amp\Websocket\WebsocketClient; use PRSW\AmphpBundle\Websocket\Attribute\WebsocketEndpoint; #[WebsocketEndpoint(path: '/chat')] class ChatHandler implements WebsocketClientHandler { public function handleClient( WebsocketClient $client, \Amp\Http\Server\Request $request, \Amp\Http\Server\Response $response, ): void { $gateway = new WebsocketClientGateway(); $gateway->addClient($client); $gateway->broadcastText('User joined'); foreach ($client as $message) { $gateway->broadcastText($message->buffer()); } } }
The bundle auto-discovers #[WebsocketEndpoint] handlers at startup.
CLI Options
| Option | Short | Description | Default |
|---|---|---|---|
--workers |
-w |
Number of worker processes | CPU count |
--max-requests |
-r |
Requests per worker before restart | 1000 |
--host |
Bind address | 127.0.0.1 |
|
--port |
-p |
Bind port | 8080 |
--shutdown-timeout |
-t |
Drain timeout (seconds) | 1 |
--dev |
Single worker, --env=dev, --debug=true |
||
--watch |
File watcher + auto-restart (needs --dev) |
Configuration
# config/packages/amphp.yaml amphp: host: '127.0.0.1' port: 8080 workers: 0 # 0 = auto-detect CPU count max_requests: 1000 # 0 = never restart shutdown_timeout: 5 # drain timeout before force-stop gc_interval: 30 # PHP cycle collector (0 = off) dbal: max_connections: 100 # connection pool size idle_timeout: 60 # close idle connections after N seconds ping_interval: 30 # keepalive ping interval (0 = every request) static_files: enabled: true public_dir: null # defaults to %kernel.project_dir%/public indexes: ['index.html', 'index.htm'] expires_period: 604800 tls: enabled: false cert_file: null key_file: null # See docs/tls.md for full TLS config readiness: enabled: true check_db: true websocket: enabled: true # set false to disable WebSocket entirely
DBAL pool settings from
amphp.dbalare automatically wired into Doctrine'sdriverOptions. Keepmax_connectionsbelow your MySQL server's limit (SHOW VARIABLES LIKE 'max_connections'; default 151).
Per-Request Cleanup (Priority Reset System)
After each request, the bundle runs a chain of resetters to release resources and
prevent state leaking between requests. Built-in resetters handle the Doctrine
connection pool and debug logger; you can add your own via PriorityResetInterface.
How It Works
SymfonyRequestHandlercallsRequestResetter::reset()in thefinallyblock.RequestResettersorts all tagged services by priority (highest first) and callsreset()on each.- Built-in priorities:
DatabaseResetter— 100 (releases pooled DB connection + FiberLocal entries)- User-defined — -255 to 255 (your custom resetters)
DebugLoggerResetter— -255 (clears debug logs)
Adding a Custom Resetter
Create a class implementing PriorityResetInterface:
use PRSW\AmphpBundle\Runtime\Bridge\PriorityResetInterface; use Symfony\Contracts\Service\ResetInterface; final class MyConnectionResetter implements PriorityResetInterface { public function __construct( private readonly MyConnectionPool $pool, ) {} public function reset(): void { $this->pool->releaseConnections(); } public function getPriority(): int { // Run after DatabaseResetter (100) but before DebugLoggerResetter (-255) return 50; } }
That's it — no manual tagging or service registration needed. The bundle's
BootstrapIntegrationPass auto-configures any service implementing
PriorityResetInterface with the amphp.resetter tag, and RequestResetter
discovers it automatically via the tagged iterator.
Execution Order
Higher priority numbers run first. Use the built-in constants as anchor points:
| Priority | Resetter | When it runs |
|---|---|---|
| 100 | DatabaseResetter |
Release pooled connection, clear FiberLocal |
| 50 | User example | Custom pool release |
| 0 | User example | General-purpose cleanup |
| -255 | DebugLoggerResetter |
Clear debug logs (last) |
Real-World Use Cases
- Release custom connection pools (Redis, RabbitMQ, gRPC)
- Clear in-memory caches or request-scoped state
- Reset rate-limit counters or circuit breakers
- Close temporary file handles or streams
Built-in Endpoints
| Path | Type | Description |
|---|---|---|
/healthz |
AMPHP middleware | Returns {"status":"alive"} — no Symfony boot |
/readyz |
Symfony route | Full-stack readiness check |
/metrics |
Symfony route | Prometheus metrics |
| user-defined | WebSocket | #[WebsocketEndpoint] handlers |
Benchmarks
Results with wrk -t1 -c100 -d30s, 1 worker, APP_ENV=prod, APP_DEBUG=0:
| Endpoint | Requests/s | Avg Latency | Description |
|---|---|---|---|
/twig-test |
1,446 | 69 ms | Twig template render, no DB |
/products |
392 | 254 ms | Doctrine DBAL query + Twig render (15 rows) |
Compared to PHP-FPM with the same Symfony app, the async server eliminates the PHP-FPM process spawn overhead and connection pool contention, yielding higher throughput under concurrent load.
SSE Streaming
use PRSW\AmphpBundle\Bridge\Symfony\HttpFoundation\SseEvent; use PRSW\AmphpBundle\Bridge\Symfony\HttpFoundation\SseResponse; #[Route('/events')] public function stream(): SseResponse { return new SseResponse(function () { while (true) { yield new SseEvent(data: ['time' => time()], event: 'tick'); \Amp\delay(1); } }); }
Generator-Based Streaming
#[Route('/stream')] public function stream(): StreamedResponse { return new StreamedResponse(function () { $handle = yield \Amp\File\open('/path/to/file', 'r'); while (null !== $chunk = yield $handle->read()) { yield $chunk; } }); }
Prometheus Metrics
Implement MetricCollectorInterface (autotagged amphp.metric_collector):
use PRSW\AmphpBundle\Metrics\Metric; use PRSW\AmphpBundle\Metrics\MetricCollectorInterface; final class OrderMetrics implements MetricCollectorInterface { public function collect(): array { return [ new Metric('orders_total', 42.0, 'Total orders', 'counter', ['status' => 'completed']), ]; } }
Optional Integrations
| Feature | Packages |
|---|---|
| Async Doctrine (MySQL/PostgreSQL) | amphp/mysql + doctrine/orm |
| Async Redis cache / sessions / messenger | amphp/redis |
| Async Mailer (SMTP + HTTP) | symfony/mailer + amphp/http-client |
| WebSocket server | included with the bundle |
Hot reload (--watch) |
fswatch (system package) |