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

Daemonizable Command Laravel Package

wrep/daemonizable-command

Laravel package for turning Artisan commands into long-running daemons. It provides a base command with loop control, graceful shutdown, signal handling, and start/stop/restart support, making it easier to build reliable background workers and scheduled tasks.

Deep Wiki
Context7

Getting Started

Install and verify the environment

composer require wrep/daemonizable-command
php -m | grep -i pcntl          # must be present
php -r 'var_dump(function_exists("pcntl_alarm"));'

The package is plain classes, not a bundle — nothing to register in config/bundles.php on current versions. It requires ext-pcntl and therefore a POSIX host; Windows is out. If pcntl_signal/pcntl_alarm show up in disable_functions (common on shared hosting), the command will fail at runtime rather than at install time, so check before you build on it.

Smallest working command

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Wrep\Daemonizable\Command\DaemonizableCommand;

class PingCommand extends DaemonizableCommand
{
    protected function configure(): void
    {
        parent::configure();               // mandatory: adds --run-once / --ignore-errors and signal wiring
        $this->setName('app:ping')->setDescription('Ticks forever');
        $this->setLoopInterval(5);         // seconds between iterations
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('tick ' . date(DATE_ATOM));
        return 0;                          // body of ONE iteration; the base class owns the loop
    }
}

Register it like any console command (#[AsCommand], console.command tag, or autoconfigure: true). You should get --run-once, --ignore-errors, and a command that never returns.

First use case

Start with --run-once: it turns the daemon into an ordinary command, so you can debug it, run it in CI, and confirm DI/output/exit codes before you ever leave it looping. Then:

timeout --signal=TERM 15 bin/console app:ping -v

You should see roughly three ticks (15s ÷ 5s) and a clean exit after the iteration in flight — not a kill. If the process dies instantly, or --run-once is "unknown option", the usual cause is a missing parent::configure().

Where to look first

  1. vendor/wrep/daemonizable-command/src/.../DaemonizableCommand.php — it is small; read the loop, the option definitions, and the signal handlers once. Everything surprising about this package is in that file.
  2. The package Tests/ fixtures — the canonical examples of loop interval, stopping, and timeout behaviour.
  3. bin/console app:ping --help — confirms which options actually exist in the version you installed (the surface has shifted across releases; don't trust blog posts).

Implementation Patterns

Setup in initialize(), work in execute()

initialize() (and interact()) run once; execute() runs every iteration. Put service resolution, lock acquisition, and "starting" logs in initialize(); put the unit of work in execute(). Do not call parent::execute().

Poll loop with a fixed interval

protected function getLoopInterval(): float
{
    return 2.5;   // seconds (float is fine on modern versions)
}

This is sleep after work, not "run every 5 seconds". For a cron-like cadence (every minute, on the minute) compute your own delay and return that, or handle scheduling outside the package.

Worker that drains a queue, then idles

protected function execute(InputInterface $input, OutputInterface $output): int
{
    while (($job = $this->queue->pop()) !== null) {
        $this->handler->handle($job);
        if (!$this->isRunning()) {          // cooperative shutdown between jobs
            $output->writeln('shutdown requested, stopping after current job');
            return 0;
        }
    }
    return 0;                               // nothing left; base class sleeps, then loops again
}

Graceful shutdown

The base class traps SIGTERM/SIGINT and flips its running flag; your job is to (a) not block for minutes, and (b) check isRunning() at natural boundaries inside long batches. Override isRunning() for custom stop conditions (e.g. a "drain" flag in Redis, or a wall-clock deadline), and call stopRunning() from inside execute() when the work is genuinely finished.

Errors

By default, an uncaught exception ends the process — which is what Supervisor wants for a broken dependency. If a single bad message must not kill the daemon, catch that exception narrowly and log it with context; do not reach for --ignore-errors as a blanket policy, because it also hides programming errors and will silently exit 0.

Recycling the process

Long-lived PHP processes leak (Doctrine identity map, Monolog buffers, accumulated references). Let the daemon exit after a bounded lifetime and have the supervisor restart it: either set a timeout on the command so it stops itself, or count iterations and stopRunning() after N. Supervisor autorestart=true closes the loop.

Supervisor / systemd / Docker

[program:app-ping]
command=/app/bin/console app:ping --no-interaction
autorestart=true
startsecs=5
stopwaitsecs=60        ; must exceed your worst-case iteration
stopasgroup=true
killasgroup=true

In Docker use the exec form (CMD ["bin/console","app:ping"]) so PID 1 receives the signal, or use an init (--init) — otherwise docker stop hits the shell and you get a SIGKILL after the grace period.

Guarding against duplicates

One daemon per queue is usually a requirement. Acquire a Symfony\Component\Lock in initialize() and bail (or stopRunning()) if it is already held; two copies of a poller is the most common production incident with this class of command.

Testing

CommandTester plus --run-once gives you a normal, terminating functional test. For the loop itself, assert that a SIGTERM sent mid-run (spawn the process, posix_kill) leads to a clean exit code and no half-processed unit of work.

Gotchas and Tips

  • parent::configure() is not optional. Without it you lose --run-once/--ignore-errors and, on some versions, the signal handler installation. The symptom is The "--run-once" option does not exist.
  • Signals are only delivered between PHP statements. A sleep(60), a slow MySQL query, or a blocking HTTP call delays shutdown by that long. Keep per-iteration work short, set statement/HTTP timeouts, and check isRunning() between chunks. If pcntl_async_signals is not enabled by your version, dispatch is tied to the loop.
  • Don't sleep() inside execute(). It stacks on top of the loop interval and makes your effective cadence unreadable. Use setLoopInterval().
  • Doctrine will bite you. The EntityManager closes on a dropped connection (MySQL wait_timeout, network blip) and every later flush() throws. Wrap work in try/catch, check $em->isOpen() (DBAL 3 removed ping()), reset the manager, and call $em->clear() every iteration or every N iterations to stop the identity map growing.
  • Memory is the real enemy. gc_collect_cycles() periodically, avoid appending to properties across iterations, and watch memory_get_peak_usage() in a -vvv log line. If RSS climbs monotonically, you have a leak, not a tuning problem.
  • Console events fire once, not per iteration. ConsoleEvents::COMMAND/TERMINATE, profiler listeners, and ConsoleHandler-style Monolog bridges see one command execution. Don't build per-iteration instrumentation on them — instrument inside execute().
  • Verbose output can itself be a leak. ConsoleOutput writes to a stream (fine), but buffered handlers (fingers_crossed, in-memory test handlers) grow without bound in a process that never ends. Log to stderr/rotating files, and guard expensive $output->writeln() calls behind OutputInterface::VERBOSITY_* checks.
  • --run-once --ignore-errors is a lie in CI. A failing iteration will be swallowed and the exit code will look clean. Use --run-once alone in pipelines.
  • Exit codes from the loop body are mostly ignored once looping; the return value matters for --run-once and for CommandTester assertions.
  • Version surface has changed. Pin the version in composer.json — method names around timeouts/loop interval differ across majors, and the "last release 2025-06-26" tag means it is maintained, but old tutorials describe APIs that no longer exist. Confirm against --help and the vendored source.
  • Debugging a stuck daemon: ps -o pid,stat,etime,cmd -p <pid> to spot S vs D state, strace -p <pid> -f -e trace=network,read,write for what it is blocked on, and always send SIGTERM (never SIGKILL) first so you can see whether graceful shutdown works. A daemon that only dies on -9 will corrupt in-flight work under Supervisor.
  • Extension points worth knowing: getLoopInterval()/setLoopInterval(), isRunning()/stopRunning(), initialize() for one-time setup, and the timeout mechanism (if your version exposes it) for self-recycling. Subclass rather than copy-paste; the base class is the only place the loop, signals, and option parsing are wired together.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
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