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

Daemon Bundle Laravel Package

m6web/daemon-bundle

Symfony bundle for turning console commands into long-running daemons. Extend the provided daemon command, implement your loop, and get signal handling, graceful shutdown, and simple process lifecycle management without boilerplate. Ideal for workers, queue consumers, and background jobs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and register the bundle. On Symfony 4+/Flex, auto-discovery usually handles it; otherwise add new M6Web\Bundle\DaemonBundle\M6WebDaemonBundle() to config/bundles.php or your kernel. As of v9.0.0 the bundle targets Symfony 7.3+ explicitly — pin ^9.0 and confirm your framework version before upgrading from 8.x.

The first place to look is the base class itself: M6Web\Bundle\DaemonBundle\Command\DaemonCommand, plus the new ConsoleCommandListener class introduced in v9.0.0 (this is now where signal handling lives — see the gotchas). Any example commands in the README still apply. The key contract is simple: extend DaemonCommand, implement configure() and doExecute(), and do not implement execute() — the bundle owns execute() and uses it to wire up a ReactPHP event loop with a periodic timer.

A minimal command looks like this:

<?php

namespace App\Command;

use M6Web\Bundle\DaemonBundle\Command\DaemonCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class PollQueueCommand extends DaemonCommand
{
    protected function configure(): void
    {
        $this->setName('app:poll-queue')
            ->setDescription('Process new queue items every second');

        $this->setLoopInterval(1_000_000); // microseconds, i.e. 1 second
    }

    protected function doExecute(InputInterface $input, OutputInterface $output): void
    {
        // One tick: fetch a bounded batch, process it, return quickly.
    }
}

Run it in the foreground:

php bin/console app:poll-queue --env=prod --no-debug

The first realistic use case is a polling loop: check a queue, a database table, or an API every second and process new work. Run it locally for a few ticks, watch the output, then stop it with Ctrl-C and verify the shutdown path works before putting it under a supervisor. On v9.0.0 that Ctrl-C path now goes through ConsoleCommandListener rather than the command itself, so verify shutdown actually fires after upgrading.

Implementation Patterns

Use initialize() for one-time setup, doExecute() for the loop body. Symfony calls initialize() once before execute(), so it is the right place to resolve services, open connections, or validate configuration. doExecute() runs every tick and should be short, idempotent, and bounded — process a fixed batch size rather than “all available work.”

Keep the tick interval in configure(). setLoopInterval() configures the periodic timer; in the versions I have seen the unit is microseconds (default roughly one second). Choose the slowest interval that meets your latency requirement. Shorter intervals mean more wakeups, more DB/API calls, and more log volume.

Stop cleanly with stopDaemon(). Call $this->stopDaemon() from inside doExecute() when the work is finished: a --max-iterations option, a --max-memory guard, a lost lock, or a drain signal. The loop exits normally and the command returns an exit code. Pair this with the bundle's signal handling (now in ConsoleCommandListener) so systemctl stop and supervisorctl stop do not kill the process mid-write.

Handle signals through the listener, not the command, on v9+. The bundle's handleSignal() method was removed from the command in v9.0.0 and reimplemented in M6Web\Bundle\DaemonBundle\Listener\ConsoleCommandListener to avoid colliding with Symfony 7.3's own handleSignal. If you previously overrode or called $this->handleSignal() in a custom command, move that logic into a listener of your own, decorate/extend ConsoleCommandListener, or hook the console command events directly.

Use the lifecycle events for cross-cutting concerns. The bundle dispatches events such as DAEMON_PRE_EXECUTE, DAEMON_POST_EXECUTE, DAEMON_LOOP_EXCEPTION, and DAEMON_STOP. Register listeners as services to record tick duration, processed counts, heartbeat, and exception details. This is usually cleaner than subclassing further — and it is now the only supported way to observe loop and signal behaviour without touching bundle internals.

Run it under a supervisor. “Daemonized” here means the process manages its own loop; it does not fork into the background. Use systemd or Supervisor with Restart=always / autorestart=true, a non-root user, and stdout/stderr captured to a log or journal. Always run with --env=prod --no-debug.

Guard against duplicate instances. There is no built-in single-instance lock. Use flock() on a lock file, Symfony Lock, or a Redis lock acquired in initialize() and refreshed each tick. Two copies of the same polling daemon will double-process work.

Make it testable. Unit-test the service that doExecute() delegates to, not the loop. For an integration smoke test, add a --once option that performs one batch and calls stopDaemon(), or extract the tick body into a callable you can invoke directly. Avoid booting the full command just to test one iteration. Since signal handling moved out of the command, you can also test your listeners in isolation now.

Know when not to use it. For new work, compare against Symfony Messenger’s worker or a plain while loop with sleep(). This bundle is a good fit when you are on a compatible Symfony/PHP stack and want a small polling daemon with signal handling and event hooks; it is not a job queue, scheduler, or worker pool.

Gotchas and Tips

  • v9.0.0 is a breaking release. handleSignal() is gone from the command; signal handling is now owned by ConsoleCommandListener. Any subclass that overrode it, or any code calling it directly, will break on upgrade. Grep your codebase for handleSignal before bumping. This was done specifically because the method name collided with Symfony 7.3's new handleSignal.
  • Symfony 7.3 is now the target. If you are pinned below 7.3, stay on 8.x until you can upgrade; if you are on 7.3+, 8.x will conflict. Check the composer.json constraints for the exact PHP/Symfony matrix rather than assuming.
  • Do not override execute(). That is the bundle’s loop implementation. Implement doExecute() instead. Overriding execute() either causes a fatal error (if it is final) or silently removes the daemon loop.
  • Exceptions are typically swallowed to keep the daemon alive. A doExecute() failure is often caught and re-dispatched as DAEMON_LOOP_EXCEPTION. That is good for resilience and bad for visibility: if no listener logs or alerts, failures are silent. Always log with context and alert on repeated exceptions.
  • Memory grows in long-running PHP. Doctrine’s identity map, caches, and local arrays accumulate. Call $em->clear() after each batch, avoid unbounded arrays, and log memory_get_usage(true) periodically. A --max-memory option that calls stopDaemon() and relies on the supervisor to restart is a pragmatic safety valve.
  • Connections go stale. MySQL and Redis connections idle out. Reconnect on failure or use keepalives. Do not hold a transaction open across ticks, and do not cache an EntityManager forever without clearing it.
  • Signals need pcntl. Signal handling is unavailable on Windows and requires the pcntl extension. If SIGTERM is not handled — for example because the new ConsoleCommandListener was not registered after upgrading — supervisorctl stop waits for the kill timeout. Verify SIGINT/SIGTERM behaviour after every major bump. Keep ticks short so shutdown is responsive.
  • configure() runs during construction. The container has not been injected yet when configure() executes, so do not call $this->getContainer() there. If the interval must be runtime-configurable, pass it via the constructor or an option rather than reading a container parameter in configure().
  • The interval is not a schedule. ReactPHP periodic timers drift, and a slow tick delays subsequent work; callbacks are not run in parallel. This is not cron. If you need fixed wall-clock execution, use cron or a scheduler.
  • Output can hurt. $output->writeln() on every tick in production is I/O and log volume. Gate it behind -v/-vv or a sampled logger.
  • Prefer DI over getContainer(). Resolve services once in initialize() or inject them via the constructor. If the command is registered as a service (console.command tag, automatic in modern Symfony), constructor injection is available and keeps doExecute() clean.
  • Small ecosystem, so read the diff. The project has a small maintainer base and few dependents; the v8→v9 changelog is essentially one PR. Read that PR (ConsoleCommandListener) and the linked issue if you rely on any signal-related behaviour, and run the deprecation log on your Symfony version before committing.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums