fm/worker-bundle
fm/worker-bundle adds a structured worker layer to PHP applications, helping you organize, run, and monitor background jobs with less boilerplate. It integrates cleanly with existing frameworks and keeps long-running tasks reliable and maintainable.
First, the premise check. fm/worker-bundle is a Symfony bundle, not a Laravel package. It hooks into Symfony\Component\HttpKernel\Bundle\Bundle, the Symfony DI container, and the console component's command lifecycle. Laravel's container, service providers, and artisan are a different contract. There is no composer require that "just works" in a Laravel app.
Second, the status check. The package's own description says it is superseded by treehouselabs/worker-bundle. Zero stars, zero dependents, no maintenance signal. For a Laravel team the correct default answer is do not adopt. The only reason to read further is that it is already sitting in a legacy service's composer.lock and you need to understand or remove it.
Where to look first (legacy Symfony case only):
DependencyInjection/Configuration.php — the authoritative config tree. Blog posts and READMEs lie; this file is what the container validates.Command/ — the console entrypoint Supervisor will call. After registering the bundle in AppKernel/config/bundles.php, run bin/console list and find the worker command.composer.lock across your repos — confirm whether anyone actually depends on it before planning anything.The mental model that carries over to Laravel: this bundle family is "a Symfony console command invoked repeatedly or continuously by an external supervisor." It is not a job framework. The Laravel equivalent is exactly a Job class + php artisan queue:work under Supervisor, or Horizon on Redis. Learn the concept here, then implement it natively.
Smallest first use case — one command you already trust, wrapped:
// src/AppBundle/Command/SendMailCommand.php
protected function configure()
{
$this->setName('app:send-mail')
->addOption('limit', null, InputOption::VALUE_OPTIONAL, '', 100);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$sent = $this->mailer->drain((int) $input->getOption('limit'));
$output->writeln(sprintf('sent %d', $sent));
return 0; // "no work" is success — see Gotchas
}
If the command cannot be run manually, once, and produce a correct result, it is not ready to be a worker.
Laravel-side equivalent to start from instead:
php artisan make:job SendMail
php artisan queue:work --queue=mail --tries=3 --timeout=60
The bundle gives you definitions and lifecycle, not a daemon. You still need Supervisor or systemd to restart it. Every worker you write should be:
INSERT ... ON DUPLICATE KEY / firstOrCreate on the Laravel side.SIGTERM to finish the current item and flush before exit.The bundle centralises worker config. Treat that config as a manifest you will later translate to queue names and Supervisor programs:
# legacy Symfony config — inventory this, do not extend it
fm_worker:
workers:
mail: { command: 'app:send-mail', sleep: 5, limit: 100 }
; the Laravel translation of the same idea
[program:mail-worker]
command=php /var/www/artisan queue:work redis --queue=mail --sleep=5 --tries=3 --max-time=3600
numprocs=2
autostart=true
autorestart=true
stopwaitsecs=90
stdout_logfile=/var/log/worker-mail.log
The bundle advertises start/stop/restart hooks. In Laravel these are queue:restart (signal-based, safe mid-job) and Horizon's horizon:terminate. Model your migration on those, not on bespoke PID-file handling.
The extension points for logging/monitoring map cleanly to Laravel primitives:
| Bundle concept | Laravel equivalent |
|---|---|
| Worker start/stop event | Queue::before / Queue::after, or a JobProcessing listener |
| Custom processor hook | handle() on the Job, or middleware via middleware() |
| Worker logging | Monolog channel + Log::withContext(['job' => ..., 'attempt' => ...]) |
| Monitoring | Horizon metrics, or a heartbeat table written each loop |
If you are extracting:
grep for the bundle namespace, inspect composer.lock).Job class + queue connection.symfony/* constraints.You can write a Laravel service provider that instantiates Symfony commands. Don't. You'd own two container conventions, two config systems, and two debugging paths forever, to run code you could express as a Job in ten minutes. Run any Symfony worker as a separate process, call it over a queue or CLI, and never load it into the Laravel request lifecycle.
The dependency is dead — budget for a fork. A superseded package receives no PHP-version or security patches. Any fix becomes yours. This is a permanent maintenance line item, not a one-time integration cost.
Exit codes drive Supervisor behaviour — get them right. A worker that returns non-zero for "no work available" will be treated as crashed and restarted in a tight loop, burning CPU and filling logs. Return 0 for "ran fine, nothing to do"; reserve non-zero for real failures, and pair with startretries / stopwaitsecs in Supervisor. The return $sent > 0 ? 0 : 1 idiom you'll find in examples is a restart-storm waiting to happen.
stopwaitsecs must exceed your job timeout. Otherwise Supervisor SIGKILLs the process mid-job → duplicate execution of a non-idempotent side effect. Set stopwaitsecs (90s) > --timeout (60s).
Composer will fight you if you try to co-install. Laravel pulls symfony/console, symfony/process, symfony/http-foundation. A Symfony bundle will pin those via symfony/framework-bundle and force downgrades that ripple into unrelated Laravel internals. Always check before spiking:
composer why-not symfony/framework-bundle <version>
composer why-not fm/worker-bundle
Long-running processes leak. Doctrine's EntityManager goes stale and accrues memory. If you inherit a Symfony worker: $em->clear() every iteration, and cap iterations. The Laravel --max-time / --max-jobs flags exist for exactly this reason — use them from day one.
Signal handling needs pcntl. Graceful shutdown in PHP requires the pcntl extension and pcntl_async_signals(true) (PHP 7.1+). If it's missing from the container image, "graceful stop" silently degrades to a hard kill. Check php -m | grep pcntl before you promise anyone a graceful restart.
Config changes need a cache clear. Symfony caches the compiled container; editing the worker YAML without bin/console cache:clear (or --env=prod rebuild) leaves you debugging a config that isn't loaded. Laravel's equivalent trap is config:cache.
No health endpoint. You will build liveness yourself — write a heartbeat row or touch a file at the top of each loop, and alert on staleness. A supervisor that dies without alerting produces silent backlog growth, which is high-severity and low-detectability.
Orphaned children after a parent crash cause duplicate job execution and resource exhaustion. If you must run forked workers, reap with pcntl_waitpid(-1, $status, WNOHANG) in the loop and never assume the parent's death kills the children.
Extension points, ranked. Prefer, in order: (1) a new console command, (2) an event subscriber on the console events, (3) patching the bundle. Anything touching the forking/lifecycle code should be considered a fork you now own — write a test before you change it, because there is no upstream test suite to lean on.
The actionable output is the successor, not the fork. If a lockfile reference turns up, the migration target is treehouselabs/worker-bundle — but subject it to the same checks (last commit, open issues, dependents). Succession does not confer health, and for a Laravel-first stack the genuinely correct target is the native queue plus Horizon.
How can I help you explore Laravel packages today?