laraveladmin / think-schedule
Laravel-style task scheduler for ThinkPHP: define tasks in PHP and keep only one system cron entry.
Requires
- php: >=8.0
- dragonmantank/cron-expression: ^3.3
- topthink/framework: ^6.1|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^9.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-19 09:55:56 UTC
README
ThinkPHP 的 Laravel 风格任务调度器:系统 crontab 只留一条,所有任务定义写在 PHP 代码里。
* * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1
// app/ScheduleDefinition.php
protected function schedule(Schedule $schedule): void
{
$schedule->command('stats:daily')->dailyAt('01:00')->withoutOverlapping(120);
$schedule->command('wx:sync --type=cron')->everyThirtyMinutes()->runInBackground();
$schedule->exec('node /path/to/script.js')->hourly();
$schedule->call(fn () => cache()->clear())->weeklyOn(1, '03:00');
}
以后增减任务、调整时间只改 PHP,不需要再登录服务器改 crontab。
一、为什么又做一个包
调研过的现成方案:
| 方案 | 结论 |
|---|---|
yzh52521/schedule | 最接近,API 直接照搬 Laravel。但最后一次发版是 2023-08,依赖 Carbon 2 + Symfony Process;CacheEventMutex::create() 用普通 set() 加锁(非原子),withoutOverlapping 也没有先检查 exists();没有测试。 |
topthink/think-crontab | ThinkPHP 5 时代产物,任务主要写在配置/数据库里,不是"代码里定义"的 Laravel 风格。 |
peppeocchi/php-cron-scheduler | 通用 PHP 调度器,没有 ThinkPHP 命令/容器/服务发现集成。 |
本包的目标是:不把 Carbon / Symfony Process 拉进业务工程,同时把互斥锁做成真正原子、可跨机(Redis)的实现,并补齐测试。
二、安装
composer require laraveladmin/think-schedule
包通过 extra.think.services 自动注册 schedule:* 命令;composer install/dump-autoload 时会执行
php think service:discover,无需手工改 config/console.php。
首次使用生成定义类和配置:
php think schedule:init
会创建:
app/ScheduleDefinition.php:任务定义(日常只改这里)config/schedule.php:锁驱动、时区、PHP 路径等运行参数
如果不想放在
app/根目录,可以把定义类移到app/crontab/ScheduleDefinition.php之类的独立目录,再把config/schedule.php的definition指向新类即可。
如果配置已经通过 vendor:publish 发布过,schedule:init 会跳过,不覆盖。
源码与安装方式
- Packagist:https://packagist.org/packages/laraveladmin/think-schedule
- 源码:https://gitee.com/zsping1989/think-schedule.git
- 当前版本:
1.0.0
已发布到 Packagist,直接安装即可,不需要在业务工程里额外配置 VCS:
composer require laraveladmin/think-schedule
知睿侯 CRM 的 server/composer.json 也只保留普通 require,生产 composer install
会从 Packagist 解析并安装。
三、快速开始
1. 定义任务
<?php
declare(strict_types=1);
namespace app;
use LaravelAdmin\ThinkSchedule\Schedule;
use LaravelAdmin\ThinkSchedule\ScheduleDefinition as BaseScheduleDefinition;
class ScheduleDefinition extends BaseScheduleDefinition
{
protected function schedule(Schedule $schedule): void
{
// ThinkPHP 命令(可带参数)
$schedule->command('stats:daily')
->dailyAt('01:00')
->withoutOverlapping(120)
->appendOutputTo(runtime_path('schedule/stats-daily.log'));
// 命令类 + 参数数组
$schedule->command(\app\command\WxSync::class, ['--type' => 'cron'])
->everyThirtyMinutes()
->runInBackground();
// 系统命令
$schedule->exec('node /home/forge/script.js')->hourly();
// 闭包(支持容器依赖注入)
$schedule->call(function (\app\common\service\Foo $foo) {
$foo->run();
})->everyFiveMinutes();
}
}
2. 查看
php think schedule:list
php think schedule:list --format=json
3. 手工触发某个任务
php think schedule:test stats:daily
php think schedule:test stats:daily --force # 先清互斥锁再执行
4. 安装 crontab(只此一条)
* * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1
调试时建议把输出留到日志:
* * * * * cd /path/to/project && php think schedule:run >> runtime/schedule/runner.log 2>&1
5. 没有 cron 的本地开发
php think schedule:work # 常驻,每秒检查
php think schedule:work --once # 只跑一轮
schedule:work 也是使用 everySecond() 等秒级频率的唯一方式。
四、命令
| 命令 | 作用 |
|---|---|
schedule:run | 执行当前时刻到期的任务(crontab 每分钟调用) |
schedule:run --dry-run --at="2026-10-19 01:00" | 调试:看指定时间会跑哪些任务,不执行 |
schedule:list | 列出任务、cron、下次运行时间、标记 |
schedule:list --format=json | JSON 输出,便于监控/脚本 |
schedule:test <片段> | 立即执行匹配的任务,忽略 cron |
schedule:work | 常驻调度器,支持秒级频率 |
schedule:clear-cache | 清理 withoutOverlapping / onOneServer 锁 |
schedule:clear-cache --all | 清空当前前缀下全部调度锁 |
schedule:init | 生成定义类与配置文件 |
schedule:exec | 内部命令:后台任务执行器(不要手工调用) |
schedule:run 的退出码:有任务失败返回 1,否则 0;配合 cron 告警:
* * * * * cd /path/to/project && php think schedule:run >> runtime/schedule/runner.log 2>&1 || echo "schedule failed $(date)" >> runtime/schedule/alert.log
五、频率
| 方法 | 说明 |
|---|---|
->cron('* * * * *') | 自定义 5 段 cron |
->everySecond() … ->everyThirtySeconds() | 秒级(需 schedule:work) |
->everyMinute() / ->everyTwoMinutes() … ->everyThirtyMinutes() | 分钟级 |
->hourly() / ->hourlyAt(17) | 每小时 / 每小时第 17 分钟 |
->everyTwoHours() / everyThreeHours() / everyFourHours() / everySixHours() | 小时间隔 |
->daily() / ->dailyAt('13:00') | 每天 |
->twiceDaily(1, 13) / ->twiceDailyAt(1, 13, 15) | 每天两次 |
->weekly() / ->weeklyOn(1, '8:00') | 每周 |
->monthly() / ->monthlyOn(4, '15:00') | 每月 |
->twiceMonthly(1, 16, '13:00') | 每月两次 |
->lastDayOfMonth('15:00') | 每月最后一天 |
->quarterly() / ->quarterlyOn(4, '14:00') | 每季度 |
->yearly() / ->yearlyOn(6, 1, '17:00') | 每年 |
约束:
$schedule->command('report:generate')
->weekdays()
->hourly()
->between('8:00', '17:00')
->timezone('Asia/Shanghai')
->when(fn () => config('app.report_enabled'))
->skip(fn () => is_holiday());
weekdays() / weekends() / mondays() … sundays() / days([1, 3])、
between() / unlessBetween() / when() / skip() 均可用。
Schedule::SUNDAY … Schedule::SATURDAY 常量与 Laravel 一致。
六、互斥、单机/多机
$schedule->command('wx:sync --type=cron')
->everyThirtyMinutes()
->withoutOverlapping(1440) // 锁最长 1440 分钟
->runInBackground();
$schedule->command('report:generate')
->dailyAt('17:00')
->onOneServer(); // 多机部署时同一分钟只允许一台执行
锁驱动
config/schedule.php:
'lock' => [
'driver' => env('SCHEDULE_LOCK_DRIVER', 'file'), // file | redis
'store' => env('SCHEDULE_LOCK_STORE', 'redis'),
'prefix' => env('SCHEDULE_LOCK_PREFIX', 'schedule:'),
'path' => '', // file 驱动目录,留空 = runtime/schedule/locks
],
file:单机锁,fopen(..., 'x')原子占位 + pid/TTL 自愈;不依赖 flock,Docker Desktop / 网络盘也能用。redis:SET NX EX原子加锁 + Lua 比较 token 释放;跨机共享,onOneServer()必须使用它。
多机部署:
SCHEDULE_LOCK_DRIVER=redis
SCHEDULE_LOCK_STORE=redis
调度器自身锁
默认 run.overlap_lock = false:schedule:run 每分钟独立执行,上一分钟的长任务不会阻塞下一分钟的其它任务。
同一个任务是否并发由 withoutOverlapping() 控制(本包所有耗时任务都建议加上)。
如果业务上确实要求"整台机器同时只能有一个 schedule:run",可打开:
'run' => ['overlap_lock' => true, 'lock_ttl' => 3600],
打开后长任务会让后续任务延迟,按需选择。
后台任务
runInBackground() 会把真正的命令交给内部子进程 schedule:exec;
子进程持有 withoutOverlapping 锁并执行任务,所以父进程退出后锁仍然有效。
后台任务无法在启动时得知最终退出码,因此 onSuccess / onFailure 对后台任务不生效;
子进程失败会追加到 runtime/schedule/background.log,任务自身输出仍在 appendOutputTo() 指定的文件。
七、输出与回调
$schedule->command('report:generate')
->daily()
->sendOutputTo(runtime_path('report.log')) // 覆盖
->appendOutputTo(runtime_path('report.log')) // 追加(后者生效)
->before(fn () => Log::info('report start'))
->after(fn () => Log::info('report end'))
->onSuccess(fn () => Log::info('report ok'))
->onFailure(function (\Throwable $e) {
Log::error('report failed: ' . $e->getMessage());
});
sendOutputTo()/appendOutputTo()会自动创建目录;timeout(300)可限制前台任务执行时间,超时退出码为124;onFailure可以接收Throwable参数(容器会按类型注入)。
八、时区
默认取 config/app.php 的 default_timezone。单任务可覆盖:
$schedule->command('report:generate')
->timezone('America/New_York')
->dailyAt('02:00');
注意:使用夏令时时区的任务在切换日可能重复或跳过,能不用尽量不用。
九、实现说明 / 限制
- 依赖只有
dragonmantank/cron-expression和topthink/framework,不引入 Carbon / Symfony Process。 schedule:run按分钟评估;everySecond()等秒级频率需要schedule:work(或自己写一个常驻进程)。file锁只保证单机互斥;多机请用redis+onOneServer()。- 后台任务的
onSuccess/onFailure不会在任务真正结束时触发(父进程已退出)。 - 锁目录 / 日志文件默认在
runtime/下,记得纳入日志轮转与清理策略。 schedule:clear-cache --all会清空当前前缀下的锁;只应在确认没有任务运行时使用。
十、测试
# 在接入本包的业务工程里(本仓库)
vendor/bin/phpunit -c ../packages/think-schedule/phpunit.xml
# 或包独立开发
composer install
vendor/bin/phpunit
测试覆盖:cron 频率、时区、between/when/skip、闭包/命令/脚本执行、
输出重定向、withoutOverlapping、后台子进程、文件锁自愈、Redis 互斥锁(Redis 可用时)。
License
MIT