Search by

yiisoft / scheduler

samdarkvjik

Recurring tasks and cron-style scheduling for PHP, best used with Yii3

Package info

github.com/yiisoft/scheduler

Homepage

Chat

Forum

Wiki

pkg:composer/yiisoft/scheduler

Fund package maintenance!

Opencollective

yiisoft

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 4

dev-master / 1.0.x-dev 2026-10-12 14:56 UTC

README

Yii

Yii Scheduler


Latest Stable Version Total Downloads Build status Code Coverage Mutation testing badge static analysis

Recurring tasks and cron-style scheduling for PHP applications: define the tasks in code, and let a single cron entry or a long-running worker run them when due — with missed-run catch-up, overlap prevention and failure handling included.

Features

  • Cron expressions and fixed intervals through a single TriggerInterface contract
  • Tasks as callables, console commands, or queue messages (via yiisoft/queue)
  • Catch-up of missed runs and crash-resume through persisted checkpoints (any PSR-16 cache)
  • Overlap prevention across processes through yiisoft/mutex
  • PSR-14 events and per-schedule listeners (before / after / onFailure)
  • schedule:run (single tick, for system cron), schedule:work (daemon), schedule:list commands

Requirements

  • PHP 8.1 - 8.5.

Installation

composer require yiisoft/scheduler

General usage

Declare tasks in the configuration:

// config/params.php
use App\Token\TokenStorage; // a class of your application
use Yiisoft\Queue\Message\GenericMessage;
use Yiisoft\Scheduler\RecurringTask;

return [
    'yiisoft/scheduler' => [
        'tasks' => [
            RecurringTask::cron('0 5 * * *', 'report:daily'),
            RecurringTask::every('10 minutes', [TokenStorage::class, 'purgeExpired']), // a static method
            RecurringTask::cron('*/5 * * * *', GenericMessage::fromPayload('metrics.collect', [])),
        ],
    ],
];

A task is one of:

  • the name of a console command, with its arguments and options if any;
  • a callable other than a string, since a string is always read as a command. It is called with the TaskContext of the run, as is: the container does not resolve its arguments, and a configuration file has no services to capture in a closure — so a task that needs them is better off as a console command;
  • a queue message, pushed to the queue when the container has a yiisoft/queue producer (QueueProducerInterface).

Run them either with a single system cron entry:

* * * * * php yii schedule:run

or as a daemon:

php yii schedule:work

The daemon sleeps until the next scheduled run instead of polling on a fixed interval, and (with ext-pcntl) forks a child process per due task, so tasks sharing a due time run in parallel instead of blocking one another. Pass --sequential to run due tasks one after another as before, or --max-processes to cap how many run at once.

A failing task does not stop the daemon: the failure goes to the FailureEvent listeners and to the log as an error, while the other tasks carry on. Make sure the application has a PSR-3 logger, or handle FailureEvent, to hear about it.

A few things to know about the forked mode:

  • a task's return value cannot cross the process boundary, so PostRunEvent and the after listeners receive null as the result; a failure is reported as the child's exit status;
  • a child inherits the parent's open connections and sockets, so a task that talks to a database or similar should acquire its own connection rather than reuse one opened before the fork — or push the work to the queue, which is fast enough to do in the parent. To keep the parent's own resources out of it, the child does not go through PHP's shutdown at all: no destructors, no register_shutdown_function callbacks, its own included, so a task that needs to clean up after itself should do so before it returns;
  • a task whose previous run is still going is skipped instead of being started a second time, so one that takes longer than its own interval cannot pile up a process per due time. --max-processes caps the total as well: once that many are running, the next due task waits for a slot instead of being started alongside them;
  • stopping the daemon asks the running tasks to stop with SIGTERM and waits five seconds for them before resorting to SIGKILL. That needs ext-posix; without it they are left running, to finish on their own.

schedule:run exits between runs, so a persisted checkpoint is the only way for it to tell which runs already happened: it requires a stateful schedule and reports an error without one. The bundled configuration makes the schedule stateful automatically when the application has a PSR-16 cache. schedule:work keeps the position in memory and works either way, though a checkpoint also lets it compensate the runs missed while it was down.

When building a schedule by hand, persist its checkpoint, and add a mutex to prevent overlapping runs across processes:

use Yiisoft\Scheduler\Schedule;

$schedule = (new Schedule())
    ->stateful($cache) // any PSR-16 implementation
    ->lock($mutex)     // any yiisoft/mutex implementation
    ->task(...);

Task identifiers

Every task gets an identifier generated from the cron expression or the interval of its trigger and from what it runs: the command name, the class of an object, the type of a queue message, or the file and line a closure is declared at. Two tasks alike in all of that cannot share a schedule until they are given identifiers of their own. Such are closures created in a loop, queue messages differing only in their payload, and tasks whose triggers differ only in their timezone or the date they count from:

$tasks = [];

foreach (['eu', 'us'] as $region) {
    $tasks[] = RecurringTask::cron('*/5 * * * *', GenericMessage::fromPayload('metrics.collect', ['region' => $region]))
        ->withId('metrics-' . $region);
}

Running on multiple servers

The same setup scales horizontally without a single point of failure: put the schedule:run cron entry (or the schedule:work daemon) on every application server. Two things then need to be shared between the servers:

  • the mutex needs a shared driver — yiisoft/mutex-redis, yiisoft/mutex-pdo-mysql or yiisoft/mutex-pdo-pgsql — so that the first server acquiring it runs the due tasks while the others skip the tick silently;
  • the checkpoint cache needs a shared PSR-16 pool — for example yiisoft/cache-redis — so that every server sees which runs already happened, and one resuming after downtime compensates only the runs nobody covered instead of re-running them.

The bundled configuration wires the checkpoint cache automatically as soon as the application has a PSR-16 cache; the mutex is opt-in — add ->lock($mutex) as shown above. With both in place, a task is still executed exactly once no matter how many servers the schedule runs on, and no single server going down stops the schedule.

Documentation

If you need help or have questions, contact us.

License

The Yii Scheduler is free software. It is released under the terms of the BSD License. Please see LICENSE for more information.

Maintained by Yii Software.

Support the project

Open Collective