yiisoft / scheduler
Recurring tasks and cron-style scheduling for PHP, best used with Yii3
Requires
- php: 8.1 - 8.5
- dragonmantank/cron-expression: ^3.1
- psr/clock: ^1.0
- psr/container: ^1.1 || ^2.0
- psr/event-dispatcher: ^1.0
- psr/log: ^1.0 || ^2.0 || ^3.0
- psr/simple-cache: ^2.0 || ^3.0
- symfony/console: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.92.5
- phpunit/phpunit: ^10.5.46
- rector/rector: ^2.0.17
- roave/infection-static-analysis-plugin: ^1.35
- shipmonk/composer-dependency-analyser: ^1.8
- spatie/phpunit-watcher: ^1.24.0
- vimeo/psalm: ^5.26.1 || ^6.12
- yiisoft/mutex: ^1.1
- yiisoft/queue: dev-master
Suggests
- ext-pcntl: To handle signals and run due tasks in parallel in the schedule:work command.
- ext-posix: To stop forked task processes gracefully when schedule:work shuts down.
- yiisoft/mutex: To prevent overlapping runs across processes.
- yiisoft/queue: To push due tasks to a queue instead of running them in the scheduler process.
Provides
None
Conflicts
None
Replaces
None
- dev-master / 1.0.x-dev
- dev-fix-periodic-trigger-anchor
- dev-fix-cron-default-timezone
- dev-fix-failed-task-blocking-schedule
- dev-fix-console-application-wiring
- dev-fix-daemon-stopping-on-task-failure
- dev-fix-failed-command-reported-as-success
- dev-fix-task-id-collisions
- dev-fix-run-after-stop
- dev-feature/command-task-object-model
- dev-fix-readme-callable-example
- dev-fix-mutex-held-after-checkpoint-load-failure
- dev-fix/code-review-findings
This package is auto-updated.
Last update: 2026-10-12 14:57:54 UTC
README
Yii Scheduler
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
TriggerInterfacecontract - 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:listcommands
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
TaskContextof 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
PostRunEventand theafterlisteners receivenullas 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_functioncallbacks, 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-processescaps 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
SIGTERMand waits five seconds for them before resorting toSIGKILL. That needsext-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.