crazy-goat/workerman-bundle
Run Symfony on Workerman: a pure-PHP event loop HTTP server, scheduler, and supervisor replacing php-fpm, nginx, cron, and supervisord. The kernel and DI container persist between requests for speed, with a composable middleware pipeline, console commands, and no external binaries.
This bundle is a runtime replacement, not a library you call from controllers. The day-one mental model: your Symfony app becomes a long-running process tree (HTTP listeners, cron-like scheduler, process supervisor) that you manage with bin/console instead of php-fpm, nginx, cron and supervisord.
Minimum path to a running server:
composer require crazy-goat/workerman-bundle
// config/bundles.php
\CrazyGoat\WorkermanBundle\WorkermanBundle::class => ['all' => true],
# config/packages/workerman.yaml
workerman:
servers:
- name: 'Symfony webserver'
listen: http://127.0.0.1:8080
processes: 4
middlewares:
- workerman.middleware.static_files
reload_strategy:
exception: { active: true }
file_monitor: { active: true }
Register the static file middleware as a service (the README example is authoritative here — the middleware path is the supported one, the serve_files/root_dir config is deprecated), then start the server with workerman:server start, curl /, and workerman:server status / workerman:server connections to confirm workers are alive.
Look first at: bin/console config:dump-reference workerman (the complete option list is the real documentation), then docs/security.md (TLS, trusted hosts, slowloris, static file protection), docs/build-packaging.md (PHAR/bin output, writable paths), and docs/troubleshooting.md. On the repo side, docs/helpers/faq.md and docs/helpers/decisions.md are the accumulated pitfall knowledge; UPGRADE.md matters because this is a fork of luzrain/workerman-bundle.
Two onboarding traps worth knowing before you start: listen is effectively required (a server without it starts fine but accepts no connections), and binding below 1024 needs root or CAP_NET_BIND_SERVICE — use 8080 locally, and in production either drop privileges via user/group or front it with nginx/Caddy.
Middleware pipeline per server. Each servers[] entry gets its own ordered middleware list. The standard composition is StaticFilesMiddleware first (handles ETag/Last-Modified/304, blocks dot-files and configured extensions, protects against traversal) and the Symfony controller middleware second. The pipeline is built once and cached, so middlewares should be stateless services with constructor-injected config — not per-request factories.
Treat the persistent kernel as the core contract. The kernel and DI container survive between requests. That is the performance win and also the main correctness constraint: any service holding request state must be reset by services_resetter after termination. The bundle calls it for you, but services registered outside the container's normal lifecycle, static properties, or third-party clients holding connections need explicit review. Audit for state on day one, not after the first cross-request data leak.
One server definition per concern. Use separate entries for the public HTTP listener, a WebSocket listener (ws:///wss://), and any internal ports. Set processes deliberately (default is CPU cores × 2, which is often too many for CPU-heavy Symfony apps); reuse_port: true when you want multiple binds. Apply body_size_cap per server and keep the global max_package_size tight.
Replace cron with the scheduler, replace daemons with the supervisor. Cron-like triggers use CronExpression detection, so existing five-field expressions port over directly; the supervisor covers long-running workers with a process count. This is where the bundle earns its keep operationally — one deploy artifact, one log story, one PID story, instead of four systems with four failure modes.
Deploy as a managed service. Build a PHAR or standalone binary (workerman:build:phar, workerman:build:bin) so runtime dependencies collapse into one file. Set runtime_dir explicitly (or WORKERMAN_RUNTIME_DIR) to a writable path — in PHAR mode the default shifts to the archive's directory and the archive itself is not writable. On code deploys use workerman:server reload (graceful) rather than restart; use Utils::reload() from application code if a deploy hook needs to trigger it. reload is not a substitute for a process restart when you change bootstrap config — config and container changes need restart.
Production reload strategy. Disable file_monitor outside development (it is skipped in PHAR mode anyway) and enable the memory strategy with limit tuned to your real per-worker footprint (default 128 MB, gc_limit 96 MB, gc_cooldown 60 s). Watch the exception strategy too: with active: true a worker that throws fatally gets recycled, which hides bugs if you never read workerman.log.
Response paths. Let large downloads go through BinaryFileResponse (the withFile() strategy, including deleteFileAfterSend cleanup for temp files) and streaming/SSE through StreamedResponse with chunked transfer, tuned via response_chunk_size. Avoid building large bodies as strings in controllers — that's the upstream behavior this fork deliberately fixes.
listen means a process that runs and accepts nothing. workerman:server status and connections are your first diagnostic, not the app log.echo, var_dump, and library output go to stdout_file when running as a daemon. Debugging "my dump doesn't show" costs people an hour; tail workerman.stdout.log.allowed_extensions caveat is real. With StaticFilesMiddleware registered as a service, the static_files.allowed_extensions config sub-key is silently ignored. Configure extension blocking on the middleware itself, otherwise you may be serving more than you think.live/ path will fail at boot, not at handshake.limit.gc_collect_cycles() is conditional and can be deferred. It runs synchronously only when the worker is above limit, and is deferred to the next tick otherwise, throttled by gc_cooldown. Don't assume every request above the threshold triggers a collection.php tests/App/index.php stop, and the Docker worktree isolation script is documented as broken for real git worktrees. Run suites serially.cache_warmup_timeout, or WORKERMAN_CACHE_WARMUP_TIMEOUT). A warmup that hangs produces a timeout, not a stack trace — raise the timeout on slow hosts before assuming a bug.connection_timeout (120 s) and keepalive_timeout (30 s) are your slowloris and idle-connection defenses. Behind a proxy, make sure the proxy's keepalive is shorter than the worker's, or you'll get sporadic 502s on reused connections.trusted_hosts fails closed with a 400. Adding it to an existing app behind a proxy or with multiple hostnames requires the regex list to be complete before you ship — including health-check and internal hostnames.WorkermanExceptionInterface at boundaries and its category bases (ServerException, KernelException, MiddlewareException, SchedulerException, ValidationException) for targeted handling. This is a genuine DX advantage over the upstream fork's generic exceptions.UPGRADE.md on every bump, and keep a smoke test that boots the server and hits a route — there's no community safety net to catch a regression for you.How can I help you explore Laravel packages today?