bakame/cron

PHP CRON: 验证 CRON 表达式,计算运行日期,确定 CRON 表达式是否到期

0.5.1 2022-01-11 16:11 UTC

This package is auto-updated.

Last update: 2024-09-20 02:12:02 UTC


README

Latest Version Software License Build

注意 这是一个基于 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::INCLUDEDDatePresence::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 方法接受

  • stringDateTimeDateTimeImmutable 对象来表示日期对象;
  • stringDateInterval 对象来表示日期间隔对象;
  • 正整数或 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

贡献

欢迎贡献,并将完全归功于您。请参阅 CONTRIBUTINGCONDUCT 了解详细信息。

安全

如果您发现任何与安全相关的问题,请通过电子邮件 nyamsprod@gmail.com 而不是使用问题跟踪器。

更新日志

请参阅 CHANGELOG 了解最近发生了什么更改。

鸣谢

许可证

MIT 许可证 (MIT)。请参阅 LICENSE 了解更多信息。