bakame / cron
PHP CRON: 验证 CRON 表达式,计算运行日期,确定 CRON 表达式是否到期
Requires
- php: ^8.1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.4
- phpstan/phpstan: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-phpunit: ^1.0
- phpstan/phpstan-strict-rules: ^1.1
- phpunit/phpunit: ^9.5.11
README
注意 这是一个基于 https://github.com/dragonmantank/cron-expression 的分支,而该分支本身又是基于原始的 https://github.com/mtdowling/cron-expression 包的一个分支的重大重写。
要了解更多关于 CRON 表达式的内容,您可以查看 Unix 文档
动机
如果这两个列出的包不存在,这个包将不会存在。虽然这些包在社区中广为人知并被广泛使用,但我希望看看我是否可以提出一种处理 CRON 表达式的替代方法。
创建分支而不是提交 PR 的原因是因为对公共 API 的更改如此重要,以至于这需要多个 PR,其中一些通过而另一些则没有。希望这里的一些想法可以在源包中得到重用。
系统要求
您需要 PHP >= 8.1,但推荐使用最新的稳定版本。
安装
使用 composer
将依赖项添加到您的项目中
composer require bakame-php/cron
独立使用
您也可以不使用 Composer 就使用 Bakame\Cron
,通过下载库并
- 使用任何其他兼容 PSR-4 的自动加载器。
- 使用如下所示的包自动加载脚本
require 'path/to/bakame/cron/repo/autoload.php'; use Bakame\Cron\Expression; Expression::fromString('@daily')->toString(); //display '0 0 * * *'
其中 path/to/bakame/cron/repo
表示库提取的路径。
用法
以下示例说明了该包的一些功能。
use Bakame\Cron\Scheduler; use Bakame\Cron\Expression; Expression::registerAlias('@every_half_hour', '*/30 * * * *'); $scheduler = Scheduler::fromSystemTimezone('@every_half_hour'); $scheduler->isDue('2022-03-15 14:30:01'); // returns true $scheduler->isDue('2022-03-15 14:29:59'); // returns false $runs = $scheduler->yieldRunsBetween(new DateTime('2019-10-10 23:29:25'), '2019-10-11 01:30:25'); var_export(array_map( fn (DateTimeImmutable $d): string => $d->format('Y-m-d H:i:s'), iterator_to_array($runs, false) )); // array ( // 0 => '2019-10-10 23:30:00', // 1 => '2019-10-11 00:00:00', // 2 => '2019-10-11 00:30:00', // 3 => '2019-10-11 01:00:00', // 4 => '2019-10-11 01:30:00', // )
计算运行时间
实例化不可变值对象调度器
为了确定 CRON 表达式的下一次运行时间,该包使用 Bakame\Cron\Scheduler
类。
为了按预期工作,此类需要
- CRON 表达式(作为字符串或作为
Expression
对象) - 时区作为 PHP
DateTimeZone
实例或时区字符串名称。 - 通过
DatePresence
枚举的值DatePresence::INCLUDED
和DatePresence::EXCLUDED
知道是否应将startDate
包含在结果中。
为了简化 Scheduler
的实例化,它附带两个关于时区使用的命名构造函数
Scheduler::fromUTC
:使用UTC
时区实例化调度器。Scheduler::fromSystemTimezone
:使用底层系统时区实例化调度器。
两个命名构造函数默认排除结果中的起始日期。
<?php use Bakame\Cron\Expression; use Bakame\Cron\Scheduler; use Bakame\Cron\DatePresence; require_once '/vendor/autoload.php'; // You can define all properties on instantiation $expression = '0 7 * * *'; $timezone = 'UTC'; $scheduler1 = new Scheduler(Expression::fromString($expression), new DateTimeZone($timezone), DatePresence::INCLUDED); $scheduler2 = new Scheduler($expression, $timezone, DatePresence::INCLUDED); $scheduler3 = Scheduler::fromUTC($expression, DatePresence::INCLUDED); //all these instantiated object are equals.
Scheduler
的公共 API 方法接受
string
、DateTime
或DateTimeImmutable
对象来表示日期对象;string
、DateInterval
对象来表示日期间隔对象;- 正整数或
0
来表示重复或跳过的出现次数;
任何其他类型将在使用时引发异常。对于返回日期对象的这些方法,它们将始终返回 DateTimeImmutable
对象,并带有调度器指定的 DateTimeZone
。
知道 CRON 表达式是否会在特定日期运行
Scheduler::isDue
方法可以确定特定的 CRON 是否会在特定日期运行。
$scheduler = Scheduler::fromSystemTimezone(Expression::fromString('* * * * MON#1')); $scheduler->isDue(new DateTime('2014-04-07 00:00:00')); // returns true $scheduler->isDue('NOW'); // returns false
查找CRON表达式的运行日期
Scheduler::run
方法允许根据特定日期查找以下运行日期。
$scheduler = new Scheduler('@daily', 'Africa/Kigali', DatePresence::EXCLUDED); $run = $scheduler->run(new Carbon\CarbonImmutable('now')); echo $run->format('Y-m-d H:i:s, e'), PHP_EOL; //display 2021-12-29 00:00:00, Africa/Kigali echo $run::class; //display Carbon\CarbonImmutable
Scheduler::run
方法允许指定在计算下一个运行日期之前要跳过的匹配数。
$scheduler = new Scheduler(Expression::fromString('@daily'), 'Africa/Kigali', DatePresence::EXCLUDED); echo $scheduler->run('now', 3)->format('Y-m-d H:i:s, e'), PHP_EOL; //display 2022-01-01 00:00:00, Africa/Kigali
如果您想获取过去的运行日期,Scheduler::run
方法接受负数。
$scheduler = new Scheduler(Expression::fromString('@daily'), 'Africa/Kigali', DatePresence::EXCLUDED); echo $scheduler->run('2022-01-01 00:00:00', -2)->format('Y-m-d H:i:s, e'), PHP_EOL; //display 2021-12-31 00:00:00, Africa/Kigali
在 Scheduler
实例化或使用适当的配置方法上使用 DatePresence
枚举,以允许 Scheduler
方法在其结果中包含起始日期(如果适用)。
Scheduler::includeInitialDate
(如果适用,将包括起始日期)Scheduler::excludeInitialDate
(将排除起始日期)
由于 Scheduler
是一个不可变对象,因此每次配置设置更改时,都会返回一个新的对象而不是修改当前对象。
$date = new DateTimeImmutable('2022-01-01 00:04:00', new DateTimeZone('Asia/Shanghai')); $scheduler = new Scheduler('4-59/2 * * * *', 'Asia/Shanghai', DatePresence::EXCLUDED); echo $scheduler->run($date)->format('Y-m-d H:i:s, e'), PHP_EOL; //display 2022-01-01 00:06:00, Asia/Shanghai echo $scheduler->includeInitialDate()->run($date)->format('Y-m-d H:i:s, e'), PHP_EOL; //display 2022-01-01 00:04:00, Asia/Shanghai
迭代多个运行
您可以迭代一组应运行的重复日期。迭代可以是正向或反向,具体取决于提供的端点。与其他方法一样,起始日期的包含仍然取决于调度器配置。
以下列出的所有方法都返回一个包含 DateTimeImmutable
对象的生成器。
使用重复项进行正向迭代
重复值始终应为正整数或 0
。任何负值都将触发异常。
$scheduler = Scheduler::fromSystemTimezone('30 0 1 * 1')->includeInitialDate(); $runs = $scheduler->yieldRunsForward(new DateTime('2019-10-10 23:20:00'), 5); var_export(array_map(fn (DateTimeImmutable $d): string => $d->format('Y-m-d H:i:s'), iterator_to_array($runs, false))); //returns //array ( // 0 => '2019-10-14 00:30:00', // 1 => '2019-10-21 00:30:00', // 2 => '2019-10-28 00:30:00', // 3 => '2019-11-01 00:30:00', // 4 => '2019-11-04 00:30:00', //)
使用重复项进行反向迭代
重复值始终应为正整数或 0
。任何负值都将触发异常。
$scheduler = Scheduler::fromSystemTimezone('30 0 1 * 1')->includeInitialDate(); $runs = $scheduler->yieldRunsBackward(new DateTime('2019-10-10 23:20:00'), 5);
使用起始日期和间隔进行迭代
间隔值始终应为正的 DateInterval
。任何负值都将触发异常。
$scheduler = Scheduler::fromSystemTimezone('30 0 1 * 1')->includeInitialDate(); $runs = $scheduler->yieldRunsAfter('2019-10-10 23:20:00', new DateInterval('P1D'));
使用结束日期和间隔进行迭代
间隔值始终应为正的 DateInterval
。任何负值都将触发异常。
$scheduler = Scheduler::fromSystemTimezone('30 0 1 * 1')->includeInitialDate(); $runs = $scheduler->yieldRunsBefore('2019-10-10 23:20:00', '1 DAY');
使用起始日期和结束日期进行迭代
如果起始日期大于结束日期,则返回的 DateTimeImmutable
对象将反向。
$scheduler = Scheduler::fromSystemTimezone('30 0 1 * 1')->includeInitialDate(); $runs = $scheduler->yieldRunsBetween('2019-10-10 23:20:00', '2019-09-09 00:30:00');
Bakame\Cron\Scheduler
对象通过 Bakame\Cron\Expression
不可变值对象公开 CRON 表达式。
处理CRON表达式
为了按预期工作,Bakame\Cron\Expression
会像在 CRONTAB 文档 中描述的那样解析 CRON 表达式。
CRON 表达式是一个字符串,表示特定命令的执行计划。CRON 调度的各个部分如下
* * * * *
- - - - -
| | | | |
| | | | |
| | | | +----- day of week (0 - 7) (Sunday=0 or 7)
| | | +---------- month (1 - 12)
| | +--------------- day of month (1 - 31)
| +-------------------- hour (0 - 23)
+------------------------- min (0 - 59)
表达式值对象还支持以下表示法
- L: 代表“最后”,指定一个月的最后一天;
- W: 用于指定最接近给定日期的星期几(星期一至星期五);
- #: 允许用于
dayOfWeek
字段,并且必须后跟一个介于 1 和 5 之间的数字; - 范围、分割表示法以及 ? 字符;
实例化
通过实例化一个 Expression
对象,您正在验证其相关的 CRON 表达式字段。每个 CRON 表达式字段都通过实现 CronField
的对象进行验证。
<?php use Bakame\Cron\MonthField; $field = new MonthField('JAN'); //!works $field = new MonthField(23); //will throw a SyntaxError
该软件包包含以下 CRON 表达式字段值对象
MinuteField
HourField
DayOfMonthField
MonthField
DayOfWeekField
可以使用这些 CRON 表达式字段值对象来实例化一个 Expression
实例。
<?php $expression = new Expression( new MinuteField('3-59/15'), new HourField('6-12'), new DayOfMonthField('*/15'), new MonthField('1'), new DayOfWeekField('2-5'), ); $expression->toString(); // display 3-59/15 6-12 */15 1 2-5 // At every 15th minute from 3 through 59 past // every hour from 6 through 12 // on every 15th day-of-month // if it's on every day-of-week from Tuesday through Friday // in January.
为了简化实例化,Expression
对象公开了更容易使用的命名构造函数。
Expression::fromString
从字符串返回一个新的实例。
<?php use Bakame\Cron\Expression; $cron = Expression::fromString('3-59/15 6-12 */15 1 2-5'); echo $cron->toString(); //displays '33-59/15 6-12 */15 1 2-5' echo $cron->minute->toString(); //displays '3-59/15' echo $cron->hour->toString(); //displays '6-12' echo $cron->dayOfMonth->toString(); //displays '*/15' echo $cron->month->toString(); //displays '1' echo $cron->dayOfWeek->toString(); //displays '2-5' var_export($cron->toFields()); // returns // array ( // 'minute' => '3-59/15', // 'hour' => '6-12', // 'dayOfMonth' => '*/15', // 'month' => '1', // 'dayOfWeek' => '2-5', // )
Expression::fromFields
从关联数组返回一个新的实例,使用与 Expression::toFields
返回的相同的索引。
<?php use Bakame\Cron\Expression; $cron = Expression::fromFields(['minute' => 7, 'dayOfWeek' => '5']); echo $cron->toString(); //displays '7 * * * 5' echo $cron->minute->toString(); //displays '7' echo $cron->hour->toString(); //displays '*' echo $cron->dayOfMonth->toString(); //displays '*' echo $cron->month->toString(); //displays '*' echo $cron->dayOfWeek->toString(); //displays '5'
如果未提供字段,则将其替换为 *
字符。 如果提供了未知字段,则将抛出 SyntaxError
异常。
格式化
值对象实现了 JsonSerializable
接口,以简化互操作性,并公开了一个 toString
方法,以返回 CRON 表达式的字符串表示形式。如上例所示,每个 CRON 表达式字段都由一个公共只读属性表示。它们还公开了一个 toString
方法,并实现了 JsonSerializable
接口。
<?php use Bakame\Cron\Expression; $cron = Expression::fromString('3-59/15 6-12 */15 1 2-5'); echo $cron->minute->toString(); //display '3-59/15' echo json_encode($cron->hour); //display '"6-12"' echo $cron->toString(); //display '3-59/15 6-12 */15 1 2-5' echo json_encode($cron); //display '"3-59\/15 6-12 *\/15 1 2-5"' echo json_encode($cron->toFields()); //display '{"minute":"3-59\/15","hour":"6-12","dayOfMonth":"*\/15","month":"1","dayOfWeek":"2-5"}'
Expression::toFields
返回一个 CRON 表达式字段字符串表示的关联数组;
两种方法产生相同的 JSON 输出字符串
更新
通过其 with*
方法更新 CRON 表达式,其中 *
被相应的 CRON 表达式字段名称替换。
这些方法期望一个 CronField
实例、一个字符串或一个整数。
<?php use Bakame\Cron\Expression; $cron = Expression::fromString('3-59/15 6-12 */15 1 2-5'); echo $cron->withMinute('2')->toString(); //displays '2 6-12 */15 1 2-5' echo $cron->withHour($cron->month)->toString(); //displays '3-59/15 1 */15 1 2-5' echo $cron->withDayOfMonth(2)->toString(); //displays '3-59/15 6-12 2 1 2-5' echo $cron->withMonth('2')->toString(); //displays '3-59/15 6-12 */15 2 2-5' echo $cron->withDayOfWeek(2)->toString(); //displays '3-59/15 6-12 */15 1 2'
注册 CRON 表达式别名
Expression
类处理 CRON 表达式的以下默认别名,除了 @reboot
。
<?php use Bakame\Cron\Expression; echo Expression::fromString('@DaIlY')->toString(); // displays "0 0 * * *" echo Expression::fromString('@DAILY')->toString(); // displays "0 0 * * *"
可以通过别名名注册更多的表达式。一旦注册,它将在使用 Expression
对象时可用,也可以在通过 CRON 表达式字符串实例化 Scheduler
类时使用。别名名需要是一个只包含 ASCII 字母和数字的单个单词,并以 @
字符为前缀。它们应与任何有效的表达式相关联。注意:别名不区分大小写
<?php use Bakame\Cron\Expression; use Bakame\Cron\Scheduler; Expression::registerAlias('@every', '* * * * *'); Scheduler::fromUTC('@every')->run('TODAY', 2)->format('c'); // display 2022-01-08T00:03:00+00:00
在任何给定时间,都可以
- 列出所有已注册的表达式及其关联的别名
- 移除已注册的别名,但不包括上表中列出的默认别名。
<?php use Bakame\Cron\Expression; use Bakame\Cron\Scheduler; if (!Expression::supportsAlias('@every')) { Expression::registerAlias('@every', '* * * * *'); } Expression::aliases(); // returns // array ( // '@yearly' => '0 0 1 1 *', // '@annually' => '0 0 1 1 *', // '@monthly' => '0 0 1 * *', // '@weekly' => '0 0 * * 0', // '@daily' => '0 0 * * *', // '@midnight' => '0 0 * * *', // '@hourly' => '0 * * * *', // '@every' => '* * * * *', // ) Expression::supportsAlias('@foobar'); //return false Expression::supportsAlias('@daily'); //return true Expression::supportsAlias('@every'); //return true Scheduler::fromUTC('@every'); // works! Expression::unregisterAlias('@every'); //return true Expression::unregisterAlias('@every'); //return false Expression::supportsAlias('@every'); //return false Scheduler::fromUTC('@every'); //throws SyntaxError unknown or unsupported expression Expression::unregisterAlias('@daily'); //throws RegistrationError exception
测试
该包有一个
- 一个 PHPUnit 测试套件
- 一个使用 PHP CS Fixer 的编码风格合规性测试套件。
- 一个使用 PHPStan 的代码分析合规性测试套件。
要运行测试,请从项目的文件夹中克隆源代码库,并通过 composer 安装包后,运行以下命令。
composer test
贡献
欢迎贡献,并将完全归功于您。请参阅 CONTRIBUTING 和 CONDUCT 了解详细信息。
安全
如果您发现任何与安全相关的问题,请通过电子邮件 nyamsprod@gmail.com 而不是使用问题跟踪器。
更新日志
请参阅 CHANGELOG 了解最近发生了什么更改。
鸣谢
许可证
MIT 许可证 (MIT)。请参阅 LICENSE 了解更多信息。