crunzphp/crunz

从代码中直接安排任务。

资助包维护!
PabloKowalczyk

v3.7.0 2024-02-12 15:14 UTC

README

一次性安装cron作业,从代码中管理其余部分。

Crunz是一个无框架的包,用于使用流畅的API在PHP中安排周期性任务(cron作业)。

Crunz能够执行任何类型的可执行命令以及PHP闭包。

Version Packagist Packagist

路线图

安装

要安装它

composer require crunzphp/crunz

如果安装成功,则名为crunz的命令行实用程序会链接到项目的vendor/bin目录。

它是如何工作的?

想法非常简单:我们不通过在crontab文件中安装cron作业,而是在一个或多个PHP文件中使用Crunz接口来定义它们。

这里有一个基本示例

<?php
// tasks/backupTasks.php

use Crunz\Schedule;

$schedule = new Schedule();
$task = $schedule->run('cp project project-bk');       
$task->daily();

return $schedule;

要运行任务,您只需安装一个常规的cron作业(crontab条目),该作业每分钟运行一次,并将责任委托给Crunz的事件运行器。

* * * * * cd /project && vendor/bin/crunz schedule:run

命令schedule:run负责收集所有PHP任务文件并运行到期的任务。

任务文件

任务文件类似于crontab文件。就像crontab文件一样,它们可以包含一个或多个任务。

通常我们在项目的根目录下的tasks/目录中创建我们的任务文件。

默认情况下,Crunz假设所有任务文件都位于项目的根目录下的tasks/目录中。

指定源目录有两种方法:1) 配置文件 2) 作为事件运行器命令的参数。

我们可以通过将其作为参数传递给事件运行器来显式设置源路径

* * * * * cd /project && vendor/bin/crunz schedule:run /path/to/tasks/directory

创建简单任务

在终端中,更改到您的项目根目录,并运行以下命令

mkdir tasks && cd tasks
nano GeneralTasks.php

然后,添加以下任务

<?php
// tasks/FirstTasks.php

use Crunz\Schedule;

$schedule = new Schedule();

$task = $schedule->run('cp project project-bk'); 
$task
    ->daily()
    ->description('Create a backup of the project directory.');

// ...

// IMPORTANT: You must return the schedule object
return $schedule; 

创建任务文件有一些约定,您需要遵守。首先,文件名应该以Tasks.php结尾,除非我们通过配置设置更改此设置。

此外,我们必须在文件的末尾返回Schedule类的实例,否则事件运行器将跳过文件中的所有任务。

由于Crunz递归地扫描任务目录,我们可以将所有任务放在一个文件中,或者根据它们的用途跨不同的文件(或目录)分散。这种行为有助于我们有一个组织良好的任务目录。

命令

我们可以使用run()运行任何命令或脚本。此方法接受两个参数:要执行的命令,以及如果有,作为关联数组的命令选项。

常规命令或脚本

<?php

use Crunz\Schedule;

$schedule = new Schedule();
$task = $schedule->run(PHP_BINARY . ' backup.php', ['--destination' => 'path/to/destination']);
$task
    ->everyMinute()
    ->description('Copying the project directory');

return $schedule;

在上面的例子中,--destinationbackup.php脚本支持的一个选项。

闭包

我们也可以将内容写入闭包而不是命令

<?php

use Crunz\Schedule;

$schedule = new Schedule();

$x = 12;
$task = $schedule->run(function() use ($x) { 
   // Do some cool stuff in here 
});

$task
    ->everyMinute()
    ->description('Copying the project directory');

return $schedule;

执行频率

有各种方法可以指定任务应该运行的时间和频率。我们可以将这些方法结合起来,以获得所需的频率。

时间单位

存在一组方法,它们将一个时间单位(大于分钟)指定为频率。它们通常以ly后缀结尾,例如hourly()daily()weeklymonthly()quarterly()yearly

使用这组方法安排的所有事件都在该时间单位的开始时发生。例如,weekly()将在星期日运行事件,而monthly()将在每月的第一天运行。

以下任务将在每天午夜(每天时间段的开始)运行。

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' backup.php');    
$task->daily();
// ...

这里还有一个例子,它在每个月的第一天运行。

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php');
$task->monthly();
// ...

在特定时间运行事件

要安排一次性的任务,可以使用如下on()方法

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php'); 
$task->on('13:30 2016-03-01');
// ...

上述任务将在2016年3月1日下午1:30运行。

On()接受由PHP的strtotime函数解析的任何日期格式。

要指定任务的执行时间,我们使用at()方法

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php'); 
$task
    ->daily()
    ->at('13:30');
// ...

如果我们只向on()方法传递一个时间,它将具有与使用at()相同的效果

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php');   
$task
    ->daily()
    ->on('13:30');
         
// is the sames as
$task = $schedule->run(PHP_BINARY . ' email.php');       
$task
    ->daily()
    ->at('13:30');
// ...

如果愿意,我们可以将“时间单位”方法(如daily()、monthly())与at()或on()约束结合在一个语句中。

以下任务将在每个小时的15分钟运行

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' feedmecookie.php'); 
$task
    ->hourlyAt('15');
// ...

可以使用hourlyOn('15')代替hourlyAt('15'),效果相同

以下任务将在星期一13:30运行

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' startofwork.php'); 
$task
    ->weeklyOn(1,'13:30');
// ...

星期日被视为一周的第0天。

如果我们希望任务在星期二(一周的第2天)上午9:00运行,我们将使用

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' startofwork.php'); 
$task
    ->weeklyOn(2,'09:00');
// ...

任务生命周期

在crontab条目中,我们无法轻松地指定任务的生命周期(任务活跃的时长)。然而,Crunz已经使其变得简单。

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->from('12:30 2016-03-04')
    ->to('04:55 2016-03-10');
 //       

或者,我们可以使用between()方法来实现相同的结果

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->between('12:30 2016-03-04', '04:55 2016-03-10');

 //       

如果我们不指定日期部分,任务将在每一天都处于活跃状态,但仅限于指定的时长内。

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
     ->everyFiveMinutes()
     ->between('12:30', '04:55');

 //       

上述任务将在每天下午12:30至下午4:55之间每5分钟运行一次。

以下是一个限制任务每小时仅在特定分钟范围内运行的方法

<?php
//

$hour = date('H');
$startminute = $hour.':05';
$endminute = $hour.':15';

$task = $schedule->run(PHP_BINARY . ' email.php');
$task
     ->hourly()
     ->between($startminute, $endminute);

 //       

上述任务将在每个小时的5分钟至15分钟之间运行

工作日

Crunz还提供了一组方法,用于指定一周中的某一天。

  • mondays()
  • tuesdays()
  • wednesdays()
  • thursdays()
  • fridays()
  • saturdays()
  • sundays()
  • weekdays()

这些方法被设计为用作约束,不应单独使用。原因在于工作日方法只是修改cron作业表达式的“星期几”字段。

以下是一个例子

<?php
// Cron equivalent:  * * * * 1
$task = $schedule->run(PHP_BINARY . ' startofwork.php');
$task->mondays();

乍一看,任务似乎每周一运行,但由于它只修改了cron作业表达式的“星期几”字段,因此任务在每周一每分钟都会运行。

这是使用工作日方法的正确方式

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' startofwork.php');
$task    
    ->mondays()
    ->at('13:30');

// ...

(一个更容易阅读的替代方案 -> weeklyOn(0,'13:30'),与上述例子中所示的结果相似)

经典方式

我们还可以像在crontab文件中一样使用旧方法进行安排

<?php

$task = $schedule->run(PHP_BINARY . ' email.php');
$task->cron('30 12 * 5-6,9 Mon,Fri');

设置单独字段

Crunz的方法不仅限于上述解释的现成方法。我们还可以设置单独的字段来组合定制的频率,类似于经典crontab的组合方式。这些方法可以接受一个值数组,或者用逗号分隔的列表参数。

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php');
$task       
    ->minute(['1-30', 45, 55])
    ->hour('1-5', 7, 8)
    ->dayOfMonth(12, 15)
    ->month(1);

或者

<?php
// ...
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->minute('30')
    ->hour('13')
    ->month([1,2])
    ->dayofWeek('Mon', 'Fri', 'Sat');

// ...

根据我们的用例,我们可以选择并组合合适的工具集,这使它们更容易使用。

运行条件

在传统的crontab文件中,我们很难设置任务运行的条件。通过when()skip()方法,这一问题得到了解决。

请看以下代码

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->between('12:30 2016-03-04', '04:55 2016-03-10')
    ->when(function() {
        if ((bool) (time() % 2)) {
            return true;
        }
        
        return false;
    });

方法when()接受一个回调函数,该函数必须返回TRUE以使任务运行。当我们需要在执行资源密集型任务之前检查资源时,这非常有用。

我们还可以使用skip()方法在特定条件下跳过任务。如果传递的回调函数返回TRUE,则任务将被跳过。

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->between('12:30 2016-03-04', '04:55 2016-03-10')
    ->skip(function() {
        if ((bool) (time() % 2)) {
            return true;
        }
        
        return false;  
    });

 //       

对于单个任务,我们可以多次使用这些方法。它们按顺序评估。

更改目录

您可以使用in()方法在运行命令之前更改目录

<?php

// ...

$task = $schedule->run('./deploy.sh');
$task
    ->in('/home')
    ->weekly()
    ->sundays()
    ->at('12:30')
    ->appendOutputTo('/var/log/backup.log');

// ...

return $schedule;

并行性和锁定机制

Crunz并行运行计划中的事件(在单独的进程中),因此所有具有相同执行频率的事件都将异步同时运行。为了实现这一点,Crunz利用symfony/Process库在子进程中运行任务。

如果一个任务的执行时间持续到下一个间隔甚至更长,我们说相同实例的任务是重叠的。在某些情况下,这可能不是问题。但是,有时这些任务会修改数据库数据或文件,这可能会导致意外的行为,甚至数据丢失。

为了防止关键任务相互重叠,Crunz通过preventOverlapping()方法提供锁定机制,该机制确保如果前一个实例正在运行,则不会运行任何任务。

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->preventOverlapping();
 //       

默认情况下,crunz使用基于文件的锁定(如果未传递参数给preventOverlapping)。对于替代锁定机制,crunz使用symfony/lock组件,该组件提供具有各种存储的锁定机制。要使用此组件,可以将存储传递给preventOverlapping()方法。在以下示例中,使用基于文件的FlockStore来提供替代锁定文件路径。

<?php

use Symfony\Component\Lock\Store\FlockStore;

$store = new FlockStore(__DIR__ . '/locks');
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->preventOverlapping($store);

保留输出

cron作业通常有输出,这些输出通常通过电子邮件发送到crontab文件的所有者,或者通过crontab文件内的MAILTO环境变量设置的用户。

我们还可以使用>>>运算符将标准输出重定向到物理文件

* * * * * /command/to/run >> /var/log/crons/cron.log

Crunz已自动化了此类输出记录。要自动将每个事件的输出发送到日志文件,我们可以在配置文件中相应地设置log_outputoutput_log_file选项。

# Configuration settings

## ...
log_output:      true
output_log_file: /var/log/crunz.log
## ...

这会将事件的输出(如果成功执行)发送到/var/log/crunz.log文件。但是,我们需要确保我们有权写入相应的文件。

如果我们需要在事件级别记录输出,我们可以像这样使用appendOutputTo()sendOutputTo()方法

<?php
//
$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->appendOutputTo('/var/log/crunz/emails.log');

 //       

方法appendOutputTo()将输出追加到指定的文件。要在每个运行后用新数据覆盖日志文件,我们使用saveOutputTo()方法。

我们还可以通过在配置文件中设置email_outputmailer设置,将错误作为电子邮件发送给一组收件人。

错误处理

Crunz通过记录错误并允许在发生错误时添加一组回调来简化错误处理。

错误回调

您可以根据需要设置任意数量的回调,以在发生错误时运行

<?php

use Crunz\Schedule;

$schedule = new Schedule();

$task = $schedule->run('command/to/run');
$task->everyFiveMinutes();

$schedule
->onError(function() {
   // Send mail
})
->onError(function() {
   // Do something else
});

return $schedule;

如果有错误,将执行两个定义的回调。

错误记录

要记录每次运行期间可能出现的错误,我们可以在配置文件中设置以下log_errorerror_log_file设置

# Configuration settings

# ...
log_errors:      true
errors_log_file: /var/log/error.log
# ...

因此,如果由于某些原因事件执行失败,错误信息将被附加到指定的错误日志文件中。每条记录都提供了有用的信息,包括发生时间、事件描述、导致错误的执行命令以及错误信息本身。

还可以通过在配置文件中设置email_errormailer设置,将错误作为电子邮件发送给一组收件人。

自定义日志记录器

要使用自己的日志记录器,创建一个实现\Crunz\Application\Service\LoggerFactoryInterface的类,例如

<?php

namespace Vendor\Package;

use Crunz\Application\Service\ConfigurationInterface;
use Crunz\Application\Service\LoggerFactoryInterface;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

final class MyEchoLoggerFactory implements LoggerFactoryInterface
{
    public function create(ConfigurationInterface $configuration): LoggerInterface
    {
        return new class extends AbstractLogger {
            /** @inheritDoc */
            public function log(
                $level,
                $message,
                array $context = array()
            ) {
                echo "crunz.{$level}: {$message}";   
            }
        };
    }
}

然后在配置中使用这个类名

# ./crunz.yml file
 
logger_factory: 'Vendor\Package\MyEchoLoggerFactory'

完成。

预处理和后处理钩子

有时我们想在事件前后执行某种操作。这可以通过将预处理和后处理回调附加到相应的事件来实现。

为此,我们在EventSchedule对象上使用before()after(),这意味着我们可以在基于事件以及基于调度的基础上拥有预和后钩子。绑定到调度的钩子将在所有事件之前运行,并在所有事件完成后运行。

<?php

use Crunz\Schedule;

$schedule = new Schedule();

$task = $schedule->run(PHP_BINARY . ' email.php');
$task
    ->everyFiveMinutes()
    ->before(function() { 
        // Do something before the task runs
    })
    ->before(function() { 
        // Do something else
    })
    ->after(function() {
        // After the task is run
    });
 
$schedule
    ->before(function () {
       // Do something before all events
    })
    ->after(function () {
       // Do something after all events are finished
    })
    ->before(function () {
       // Do something before all events
    });

我们可能需要根据需要多次使用这些方法,可以通过链接它们来实现。

只有在事件执行成功时才会调用后执行回调。

其他有用的命令

我们已经使用了一些crunz命令,如schedule:runpublish:config

要查看crunz的所有有效选项和参数,我们可以运行以下命令

vendor/bin/crunz --help

列出任务

这些命令中的一个就是crunz schedule:list,它会以表格格式列出定义的任务(在收集的*.Tasks.php文件中)。

vendor/bin/crunz schedule:list

+---+---------------+-------------+--------------------+
| # | Task          | Expression  | Command to Run     |
+---+---------------+-------------+--------------------+
| 1 | Sample Task   | * * * * 1 * | command/to/execute |
+---+---------------+-------------+--------------------+

默认情况下,列表为文本格式,但可以通过--format选项更改格式。

json格式列出,命令

vendor/bin/crunz schedule:list --format json

将输出

[
    {
        "number": 1,
        "task": "Sample Task",
        "expression": "* * * * 1",
        "command": "command/to/execute"
    }
]

强制运行

在开发过程中,强制运行所有任务(无论它们的实际运行时间如何)可能很有用,这可以通过在schedule:run中添加--force来实现。

vendor/bin/crunz schedule:run --force

要强制运行单个任务,使用上面的schedule:list命令确定任务编号,然后按照以下方式运行

vendor/bin/crunz schedule:run --task 1 --force

生成任务

还有一个名为make:task的有用命令,它可以生成具有所有默认值的任务文件骨架,这样我们就不需要从头开始编写它们。我们可以在以后根据我们的要求修改输出文件。

例如,要创建一个每小时在星期一运行/var/www/script.php的任务,我们运行以下命令

vendor/bin/crunz make:task exampleOne --run scripts.php --in /var/www --frequency everyHour --constraint mondays
Where do you want to save the file? (Press enter for the current directory)

当我们运行此命令时,Crunz将询问我们想要保存文件的路径。默认情况下,它是我们源任务目录。

因此,事件被定义在指定任务目录中的exampleOneTasks.php文件中。

要查看事件是否已成功创建,我们列出事件

crunz schedule:list

+---+------------------+-------------+----------------+
| # | Task             | Expression  | Command to Run |
+---+------------------+-------------+----------------+
| 1 | Task description | 0 * * * 1 * | scripts.php    |
+---+------------------+-------------+----------------+

要查看make:task命令的所有选项以及所有默认值,我们运行以下命令

vendor/bin/crunz make:task --help

任务调试

以显示任务运行的基本信息

vendor/bin/crunz task:debug 1

上面的命令应该输出类似以下的内容

+----------------------+-----------------------------------+
| Debug information for task '1'                           |
+----------------------+-----------------------------------+
| Command to run       | php -v                            |
| Description          | Inner task                        |
| Prevent overlapping  | No                                |
+----------------------+-----------------------------------+
| Cron expression      | * * * * *                         |
| Comparisons timezone | Europe/Warsaw (from config)       |
+----------------------+-----------------------------------+
| Example run dates                                        |
| #1                   | 2020-03-08 09:27:00 Europe/Warsaw |
| #2                   | 2020-03-08 09:28:00 Europe/Warsaw |
| #3                   | 2020-03-08 09:29:00 Europe/Warsaw |
| #4                   | 2020-03-08 09:30:00 Europe/Warsaw |
| #5                   | 2020-03-08 09:31:00 Europe/Warsaw |
+----------------------+-----------------------------------+

配置

Crunz提供了一些以YAML格式提供的配置选项。要修改配置设置,强烈建议您有自己的配置文件副本,而不是修改原始文件。

要创建配置文件的副本,我们首先需要发布配置文件

/project/vendor/bin/crunz publish:config
The configuration file was generated successfully

结果,配置文件的副本将在我们项目的根目录中创建。

配置文件看起来如下所示

# Crunz Configuration Settings

# This option defines where the task files and
# directories reside.
# The path is relative to the project's root directory,
# where the Crunz is installed (Trailing slashes will be ignored).
source: tasks

# The suffix is meant to target the task files inside the ":source" directory.
# Please note if you change this value, you need
# to make sure all the existing tasks files are renamed accordingly.
suffix: Tasks.php

# Timezone is used to calculate task run time
# This option is very important and not setting it is deprecated
# and will result in exception in 2.0 version.
timezone: ~

# This option define which timezone should be used for log files
# If false, system default timezone will be used
# If true, the timezone in config file that is used to calculate task run time will be used
timezone_log: false

# By default the errors are not logged by Crunz
# You may set the value to true for logging the errors
log_errors: false

# This is the absolute path to the errors' log file
# You need to make sure you have the required permission to write to this file though.
errors_log_file:

# By default the output is not logged as they are redirected to the
# null output.
# Set this to true if you want to keep the outputs
log_output: false

# This is the absolute path to the global output log file
# The events which have dedicated log files (defined with them), won't be
# logged to this file though.
output_log_file:

# By default line breaks in logs aren't allowed.
# Set the value to true to allow them.
log_allow_line_breaks: false

# By default empty context arrays are shown in the log.
# Set the value to true to remove them.
log_ignore_empty_context: false

# This option determines whether the output should be emailed or not.
email_output: false

# This option determines whether the error messages should be emailed or not.
email_errors: false

# Global Swift Mailer settings
#
mailer:
    # Possible values: smtp, mail, and sendmail
    transport: smtp
    recipients:
    sender_name:
    sender_email:


# SMTP settings
#
smtp:
    host:
    port:
    username:
    password:
    encryption:

如您所见,有几个选项,如用于指定源任务目录的source,其他选项用于错误/输出记录/电子邮件目的。

每次我们运行Crunz命令时,它都会查看项目的根目录,看看是否有用户修改过的配置文件。如果配置文件不存在,它将使用随包一起提供的配置文件。

开发环境标志

以下环境标志仅应在开发时使用。典型最终用户不需要也不应该更改它们。

CRUNZ_CONTAINER_DEBUG

用于启用/禁用容器调试模式的标志,仅对开发有用。默认情况下在docker-compose中启用。

CRUNZ_DEPRECATION_HANDLER

用于启用/禁用Crunz弃用处理器的标志,仅对集成测试有用。默认情况下禁用测试。

赞助商

Blakfire.io logo

支持

您可以通过GitHub支持Crunz的进一步开发。

贡献

我应该选择哪个分支?

错误修复和readme更改应针对3.6,新功能应针对3.7

如果您需要帮助

请使用GitHub问题提交所有问题和疑问,我将尽力帮助您。

致谢

许可

Crunz是免费软件,根据MIT许可条款分发。