Daemon Threads Sample Application
Build an app with a supervised background daemon that watches an inbox directory and processes new files as they arrive. You use the #[DaemonThread] annotation on a ServerWorkerProcess subclass. The framework starts the daemon with your app and restarts it if it crashes, so you never wire up your own process supervisor.
Use a daemon for work that must always be running — watching a directory, polling a queue, or tailing a stream. Use scheduled tasks instead when the work runs on a fixed cadence.
Prerequisites
Section titled “Prerequisites”You need PHP 8.5 or later with the swoole and pcntl extensions. No external services are needed.
Project structure
Section titled “Project structure”The sample uses this layout:
daemon/├── bin/│ └── application.php # Application entry point├── config/│ └── application.yml # Winter Boot application config├── src/│ ├── DaemonSampleApplication.php # Main application class│ └── daemon/│ └── InboxWatcherDaemon.php # Daemon thread implementation└── composer.json # DependenciesInstall dependencies
Section titled “Install dependencies”Require the framework package:
composer require suvera/winter-bootSource files
Section titled “Source files”Switch between the source files. Each tab shows the exact file from the sample.
The single entry point. It points Winter Boot at the config directory and at the sample namespace.
<?php
namespace dev\example;
use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . "/../config"], scanNamespaces: [ ['dev\\example', __DIR__ . ''] ])]class DaemonSampleApplication {
public static function main(): void { $winterApp = new \dev\winterframework\core\app\WinterWebSwooleApplication(); $winterApp->run(self::class); }}The daemon. Every 5 seconds it scans /tmp/inbox for .txt files, logs each file’s line count, and moves processed files to /tmp/processed. The loop never exits — the process lives as long as run() runs, and the framework restarts it on failure.
<?php
namespace dev\example\daemon;
use dev\winterframework\io\process\ProcessType;use dev\winterframework\io\process\ServerWorkerProcess;use dev\winterframework\stereotype\cli\DaemonThread;
#[DaemonThread( name: 'inbox-watcher', coreSize: 1)]class InboxWatcherDaemon extends ServerWorkerProcess {
private const INBOX_DIR = '/tmp/inbox'; private const PROCESSED_DIR = '/tmp/processed';
public function getProcessType(): int { return ProcessType::OTHER; }
public function getProcessId(): string { return 'inbox-watcher'; }
protected function run(): void { @mkdir(self::INBOX_DIR, 0777, true); @mkdir(self::PROCESSED_DIR, 0777, true);
self::logInfo('InboxWatcherDaemon started, watching ' . self::INBOX_DIR);
while (1) { $this->processInbox();
// Sleep 5 seconds without blocking the event loop \Co::sleep(5); } }
private function processInbox(): void { $files = glob(self::INBOX_DIR . '/*.txt') ?: [];
foreach ($files as $file) { $lines = count(file($file, FILE_IGNORE_NEW_LINES)); $name = basename($file);
self::logInfo("Processing {$name}: {$lines} lines");
rename($file, self::PROCESSED_DIR . '/' . $name);
self::logInfo("Moved {$name} to processed"); } }}Three members are mandatory: getProcessType() returns the process role, getProcessId() returns a unique id used by the supervisor, and run() holds the main loop. Always sleep with \Co::sleep() instead of sleep() so you never block the Swoole event loop.
The launch script. It loads the Composer autoloader and starts the application.
<?php
use dev\example\DaemonSampleApplication;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
DaemonSampleApplication::main();The sample declares the framework dependency with PSR-4 autoloading for its own namespace.
{ "name": "suvera/winter-boot-daemon-sample", "require": { "ext-pcntl": "*", "ext-swoole": "*", "suvera/winter-boot": "@dev" }, "autoload": { "psr-4": { "dev\\example\\": "src/" } }}The runnable sample in winter-boot-samples adds a local path repository for winter-boot so it resolves from a sibling checkout. You do not need that entry when you install the released package from Packagist.
Configuration
Section titled “Configuration”No module is needed — the daemon is a core framework feature. The full sample application.yml sets only the server and app identity:
server: port: 8080 address: 0.0.0.0 context-path: /winter: application: name: Daemon Threads Sample Application id: daemon-sample-app version: 1.0.0See Configuration for every application.yml key.
Run the app
Section titled “Run the app”Start the app, drop a file into the inbox, and watch the daemon process it.
1. Start the application:
composer installphp bin/application.phpThe daemon creates /tmp/inbox and /tmp/processed on startup and begins scanning.
2. Drop a file into the inbox:
echo -e "line one\nline two\nline three" > /tmp/inbox/report.txt3. Verify processing:
ls /tmp/processedYou see report.txt in the processed directory within 5 seconds, and the app log shows the line count:
Processing report.txt: 3 linesMoved report.txt to processedNext steps
Section titled “Next steps”- Read Daemon Threads for process types,
coreSizescaling, and supervision details. - Read the Scheduler example when your work runs on a fixed cadence instead of continuously.