Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Daemons Bundle Laravel Package

edo/daemons-bundle

A Symfony bundle for running and managing long-running daemon processes in PHP applications. It offers daemon lifecycle control, process supervision, and console integration to help you build reliable background workers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require edo/daemons-bundle

Then register the bundle class (whatever namespace the package ships, e.g. Edo\DaemonsBundle\DaemonsBundle) in config/bundles.php on Symfony 4+, or AppKernel::registerBundles() on older kernels. This package predates Flex recipe auto-discovery, so assume you must register it by hand until proven otherwise.

Look here first, in this order

  1. vendor/edo/daemons-bundle/Command/ — find the base daemon command class. Everything hangs off it. Note the exact method you override (execute, executeDaemon, runDaemon, …) and whether it exposes a foreground/debug flag.
  2. vendor/edo/daemons-bundle/DependencyInjection/ — the extension class tells you the config tree (PID directory, log path, poll interval, user). If there is no extension class, there is no config: paths are hardcoded.
  3. README.md and any Resources/config/services.yml — confirms the service IDs and whether the bundle wires anything into the container for you.

First use case

Take an existing long-running command you currently run under nohup or cron — a queue worker, a filesystem watcher, an importer that polls a table — and convert it into a daemon. The typical shape:

use Edo\DaemonsBundle\Command\DaemonCommand;

class PollOrdersCommand extends DaemonCommand
{
    protected function configure(): void
    {
        $this->setName('app:poll-orders')
             ->setDescription('Polls the order queue forever');
    }

    protected function executeDaemon(): void
    {
        while (true) {
            $this->processBatch();
            sleep(5);
        }
    }
}

Start it, then verify with ps -ef | grep app:poll-orders that the PID matches whatever file the bundle wrote. Getting that first start → ps → stop loop working end to end takes ten minutes and tells you more than the README does.

Implementation Patterns

Extend the base command, don't wrap a Command. The bundle's value is the fork/PID/signal plumbing. Subclass the base class and put only your loop body in it. Don't try to daemonize an arbitrary existing command from the outside — you will lose the lifecycle hooks and the shutdown handling.

Lifecycle verbs. Expect something like app:poll-orders start|stop|restart|status, or a separate daemon:run <command> wrapper plus a --daemon flag on your own command. Whichever it is, build your operational runbook around it:

bin/console app:poll-orders start
bin/console app:poll-orders status
bin/console app:poll-orders stop     # should send SIGTERM, not kill -9

Always keep a foreground mode. Run the loop in the foreground under --verbose (or whatever the bundle calls it) during development. Daemonized debugging is a waste of an afternoon. If the package has no foreground flag, add one in your subclass that skips the parent's fork step.

Signal handling for graceful shutdown. Long iterations must finish their unit of work before dying. Register handlers for SIGTERM/SIGINT that flip an $this->running = false flag checked at the top of the loop, so stop doesn't sever a half-written record. SIGHUP is a good hook for "reload config, don't restart".

Log to a file, never to stdout. Once detached, stdout is either /dev/null or an inherited buffer nobody drains. Route your Monolog channel to a rotating file handler and make the bundle's log path point at var/log/. A daemon that logs only to the console is invisible the moment it works.

Reset state inside the loop. In a long-lived process, these leak:

  • Doctrine: $em->clear() after each batch; on the EntityManager is closed exception, $this->getContainer()->reset('doctrine') or reconnect explicitly.
  • Symfony services: reboot the kernel periodically on older versions ($kernel->reboot(null)), or ResetServices/ServicesResetter on newer ones, so request-scoped state doesn't accumulate.
  • Your own caches, SplObjectStorage, static arrays — gc_collect_cycles() every N iterations and confirm RSS is flat over an hour.

Supervise the daemon with something else. The bundle starts a process; it does not resurrect one that died from an OOM or an unhandled exception. Either:

  • wrap the loop body in try/catch and keep going after transient errors, and/or
  • put a cron watchdog in place: status returns non-zero → start.

If you already run supervisord or systemd, prefer autorestart=true there and use the bundle only for its fork/PID convenience — or skip the bundle entirely and let the supervisor run the foreground command.

Multiple instances. For parallel workers, give each one a name (--worker=3) and make sure the PID file and log file are keyed by that name. A shared PID path means the second start clobbers the first, and stop kills the wrong process.

Docker. Don't daemonize inside a container. The container itself is the supervision boundary; fork-and-detach inside PID 1 makes logs disappear and makes docker stop unreliable. Run the command in the foreground there.

Gotchas and Tips

Verify Symfony compatibility before you commit to this. The package is unmaintained-looking (zero dependents, zero stars) and its API surface will be whatever Symfony version was current when it was written. If it uses ContainerAwareCommand, AppKernel, or getContainer()->get() on private services, it will not run cleanly on Symfony 5.3+/6.x. Open composer.json first and check the symfony/* constraints against your project.

Inherited file descriptors survive the fork. An open DB connection, an open socket, or an inherited log handle in the parent is still open in the child and points at the same underlying resource. Always reconnect — or better, acquire connections after the fork, inside the loop.

Stale PID files are the #1 operational failure. After kill -9, a SIGKILL, or a host reboot, the PID file remains. A naive status reports "running" forever, and start refuses with "already running". Insist that the bundle (or your subclass) checks posix_kill($pid, 0) before trusting the file, and delete the file on shutdown. If the bundle doesn't, patch it in your subclass.

MySQL/Postgres idle timeouts. wait_timeout will kill your connection mid-sleep and the next query throws MySQL server has gone away. Either ping the connection each iteration ($connection->ping() / $connection->executeQuery('SELECT 1')) or catch the disconnect and reconnect.

The loop must never exit silently. An uncaught exception in a detached process is invisible except in the log. Wrap the whole while body in try/catch, log with stack trace, sleep() with backoff, and continue. Reserve exit for unrecoverable config errors.

Restart backoff, not a hot retry loop. If processing fails instantly, while(true) { try { ... } catch {} } will spin a core and flood the log. Sleep on consecutive failures, exponentially.

Permissions. The process runs as whoever started it. If you start it as root during a deploy, the PID and log files become root-owned and the next www-data start fails. Start it as the application user, and make the PID/log directory writable by that user.

Debugging a live daemon:

ps -o pid,ppid,etime,rss,cmd -p $(cat var/run/app-poll-orders.pid)
cat /proc/$PID/status | grep -E 'State|VmRSS|Threads'
ls -l /proc/$PID/fd          # what it has open, incl. inherited sockets
strace -p $PID -f -e trace=network,read,write -s 200

Flat VmRSS over an hour means the loop is fine; a slow climb means a leak in a service you thought was stateless.

Extension points worth patching. If the bundle is thin, the useful seams are: the PID file resolver, the logger, the sleep/interval config, and the signal registration. Subclass or decorate those rather than editing files in vendor/. If it turns out there are none, the honest assessment is that a 40-line pcntl_fork + posix_setsid command in your own codebase is more maintainable than a zero-dependent bundle, and lets you run under supervisord with no third-party risk.

Testing. Don't test the daemon wrapper — test the batch method as a plain unit test with the loop factored out into processBatch(). The fork/PID layer is not worth test coverage; the lifecycle smoke test (start, assert PID alive, stop, assert PID gone) belongs in a shell script in CI.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons