Scheduler Sample Application
Build an app that runs recurring maintenance jobs on fixed cadences. You put #[Scheduled] on zero-argument methods of a #[Service] bean and enable scheduling on the application class. The framework runs each tick in a dedicated worker process, so a slow job never blocks web requests.
Use scheduled tasks for work that runs on a fixed cadence — cleanup, reports, cache warming. Use a daemon thread instead when the work must run continuously.
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:
scheduler/├── bin/│ └── application.php # Application entry point├── config/│ └── application.yml # Scheduling pool config├── src/│ ├── SchedulerSampleApplication.php # Main application class│ └── schedule/│ └── MaintenanceScheduler.php # Scheduled job methods└── 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. #[EnableScheduling] activates the #[Scheduled] ticks in the scanned namespace.
<?php
namespace dev\example;
use dev\winterframework\stereotype\task\EnableScheduling;use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . "/../config"], scanNamespaces: [ ['dev\\example', __DIR__ . ''] ])]#[EnableScheduling]class SchedulerSampleApplication {
public static function main(): void { $winterApp = new \dev\winterframework\core\app\WinterWebSwooleApplication(); $winterApp->run(self::class); }}The jobs. hourlyCleanup() deletes temp files older than one hour, and dailySummary() appends a one-line summary to the log. Each method is public, takes no arguments, returns void, and declares exactly one interval.
<?php
namespace dev\example\schedule;
use dev\winterframework\stereotype\Service;use dev\winterframework\task\scheduling\stereotype\Scheduled;use dev\winterframework\util\log\Wlf4p;
#[Service]class MaintenanceScheduler { use Wlf4p;
private const TMP_DIR = '/tmp/scheduler-tmp'; private const LOG_FILE = '/tmp/scheduler.log';
/** * Hourly temp-file cleanup tick. */ #[Scheduled(fixedDelay: 3600, initialDelay: 60)] public function hourlyCleanup(): void { @mkdir(self::TMP_DIR, 0777, true);
$deleted = 0; $cutoff = time() - 3600; foreach (glob(self::TMP_DIR . '/*') ?: [] as $file) { if (is_file($file) && filemtime($file) < $cutoff) { unlink($file); $deleted++; } }
self::logInfo("Hourly cleanup deleted {$deleted} temp files"); }
/** * Daily summary tick. */ #[Scheduled(fixedDelay: 86400, initialDelay: 120)] public function dailySummary(): void { $line = sprintf( "[%s] Daily summary: tmp dir holds %d files", date('Y-m-d H:i:s'), count(glob(self::TMP_DIR . '/*') ?: []) ) . PHP_EOL;
file_put_contents(self::LOG_FILE, $line, FILE_APPEND | LOCK_EX);
self::logInfo(trim($line)); }}fixedDelay waits that many seconds after the previous run completes before starting the next, so runs never overlap. initialDelay postpones the first run after boot. All values are in seconds and must be positive.
The launch script. It loads the Composer autoloader and starts the application.
<?php
use dev\example\SchedulerSampleApplication;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
SchedulerSampleApplication::main();The sample declares the framework dependency with PSR-4 autoloading for its own namespace.
{ "name": "suvera/winter-boot-scheduler-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 — scheduling is a core framework feature. The full sample application.yml sets the server, app identity, and the scheduling worker pool:
server: port: 8080 address: 0.0.0.0 context-path: /winter: application: name: Scheduler Sample Application id: scheduler-sample-app version: 1.0.0 task: scheduling: poolSize: 1 queueCapacity: 50See Configuration for every application.yml key.
Run the app
Section titled “Run the app”Start the app and trigger the jobs with short delays to verify them quickly.
1. Start the application:
composer installphp bin/application.php2. Seed a stale temp file and watch the hourly tick:
mkdir -p /tmp/scheduler-tmptouch -d "2 hours ago" /tmp/scheduler-tmp/stale.tmpAbout a minute after boot (initialDelay: 60), the app log shows the cleanup result:
Hourly cleanup deleted 1 temp files3. Check the daily summary log:
cat /tmp/scheduler.logAbout two minutes after boot (initialDelay: 120), the summary line appears:
[2026-09-10 12:02:00] Daily summary: tmp dir holds 0 filesThe short initial delays let you verify both ticks without waiting an hour or a day — the steady-state cadence stays hourly and daily via fixedDelay.
Next steps
Section titled “Next steps”- Read Scheduling for
fixedRate, placeholder-driven intervals, and pool tuning. - Read the Daemon example when your work must run continuously instead of on a cadence.