eurotolia/mastercron

直接从代码中安排您的任务。


README

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

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

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

Version Packagist Packagist

路线图

安装

要安装它

composer require lavary/crunz

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

它是如何工作的?

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

以下是一个基本示例

<?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日13: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天)09: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之间,每五分钟运行一次。

以下是如何限制任务在每小时的一定范围内运行的示例

<?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()
  • ...
  • sundays()
  • weekedays()
  • weekends()

这些方法已被设计为约束条件,不应单独使用。原因在于工作日方法仅修改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();
 //       

默认情况下,cunz使用基于文件的锁定(如果未向preventOverlapping传递参数)。对于替代锁定机制,cunz使用提供各种存储的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);

从Symfony 5.0开始,StoreInterface已拆分为BlockingStoreInterfacePersistingStoreInterface。要使用任何持久性锁(Redis、PDO等),它们需要通过RetryTillSaveStore进行装饰。

<?php

use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\Store\RetryTillSaveStore;

$redis = new Redis();
$redis->connect('localhost');
$persistingStore = new RedisStore($redis);
$blockingStore = new RetryTillSaveStore($persistingStore);

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

保留输出

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 |
+---+---------------+-------------+--------------------+

强制运行

在开发过程中,强制运行所有任务(无论它们的实际运行时间如何)可能很有用,这可以通过在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弃用处理器的标志,仅适用于集成测试。默认情况下测试中禁用。

贡献

我应该选择哪个分支?

在大多数情况下,你应该针对分支 1.12.x,因为这是活跃的开发分支。分支 master 是用于未来发布,但所有错误/功能都应该提交到 1.11.x

如果您需要帮助

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

致谢

许可证

Crunz是在MIT许可证下分发的免费软件。