PHP 站点管理员配置的任务计划程序

PHP Task Scheduler Configured by Site Admins

我正在尝试为我的站点上的管理员创建一种方法来安排任务,类似于我在服务器上为 运行 特定脚本设置 cron 作业的方式。我希望他们对任务 运行 的时间有类似的控制,例如每天 14:00 或每周四 12:00 等

我想我会有一个表格,询问他们想要运行 任务的频率,days/weeks 等。然后将其存储在数据库中。接下来,我将创建一个 cron 作业来 运行 一个脚本,每分钟说一次。然后该脚本将 select 数据库中的所有任务 运行 并执行每个任务。

我一直在为此寻找任务调度器,到目前为止,它们中的大多数似乎都是为 Web 开发人员构建的,以便以编程方式调度任务。相反,我想将它们存储在数据库中,然后将 SQL 查询写入 select,将正确的任务写入 运行。我想知道我应该使用什么结构将计划存储在数据库中,以及如何在特定时间将正确的任务检索到 运行?

如果有人能指出正确的方向,我将不胜感激。

这个想法相当简单,看起来你已经很好地掌握了它。如果您定义了一组管理员可以安排的 "Tasks",这很简单,只需将它们存储在数据库中 table 以及它们应该 运行 的时间戳。然后,您将拥有一个脚本(例如,"job_runner.php"),您可以根据需要(例如,通过 cron)将其安排到 运行(这是您必须定义的业务需求)。

您可以为管理员定义您的工作,以便像这样安排:

interface JobInterface {
    public function run();
} 

class RunSalesReport implements JobInterface {
    public function run(){ 
        // .. business logic 
    };

    // or maybe just __invoke() would be fine! your call!
}

您的 "Task Scheduler" 网络表单将包含管理员可以安排到 运行 的作业列表,例如,该列表可能包含与上述 "Run Sales Report" 相关的作业 RunSalesReport class。 Web 表单的服务器端处理程序只会将表单数据存储在数据库中 table.

数据库 table 可能只包含一个 time_to_run 列(用于确定作业何时应该 运行)和一个 job_class 列(用于保存 class 名称应该是 instantiated/factoried/whatever).

"job_runner.php" 文件简单地连接到数据层并找到任何 "Jobs" 计划到 运行 但还没有 运行 的(你可以然后将它们标记为 "executed" 或在它们 运行 之后将它们从 table 中删除)。

// job_runner.php - executed via cron however often you need it to be
// if admin can only schedule jobs on the hour, then run on the hour, etc.
$jobs = $pdo->execute("SELECT * FROM scheduled_jobs WHERE DATE(time_to_run) <= DATE(NOW())");
foreach($pdo->fetchAll($jobs) as $jobRow){
    $jobClassName = $jobRow['job_class'];
    $job = new $jobClassName; // or get from IOC container, your decision
    $job->run();
}

这是我在过去的项目中如何实现这一点的简单解释和示例。为简洁起见,我省略了安全方面的考虑,但请注意,让用户指定命令 运行 本身就是不安全的。

任务SQLTable

您需要设置这三列供您的执行脚本使用。间隔列是一个 cron 字符串(分钟小时日月年)。 script_path 列是脚本所在的路径 运行。 last_executed 列是该任务最后一次 运行 的时间。间隔和 last_executed 列将用于确定是否应执行任务。

+----+------------+----------------------+---------------------+
| id |  interval  |      script_path     |    last_executed    |
+----+------------+----------------------+---------------------+
| 1  | 5 * * * *  | /path/to/script1.php | 2016-01-01 00:00:00 |
+----+------------+----------------------+---------------------+
| 2  | * 12 * * * | /path/to/script2.php | 2016-01-01 00:00:00 |
+----+------------+----------------------+---------------------+

任务执行脚本

此脚本将通过 cron 作业每分钟 运行。

#/usr/bin/env php
<?php

// Get tasks from the database
$db = new PDO('dsn', 'username', 'password');
$stmt = $db->prepare('SELECT * FROM `tasks`');
$stmt->execute();
$tasks = $stmt->fetchAll(PDO::FETCH_OBJ);

foreach ($tasks as $task) {
    $timestamp = time();
    $lastExecutedTimestamp = strtotime($task->last_executed);
    // Convert cron expression to timestamp
    $intervalTimestamp = $task->interval;

    // Check if the task should be run.
    if ($timestamp - $lastExecutedTimestamp >= $intervalTimestamp) {
        // Execute task
        // ...

        // Update the task's last_executed time.
        $stmt = $db->prepare('UPDATE `tasks` SET `last_executed` = ? WHERE `id` = ?');
        $stmt->execute([date('Y-m-d H:i:s', $timestamp), $task->id]);
    }
}

此处其他答案中的一些好想法。我还要指出,如果您发现自己需要,您应该考虑使用 PHP 的 DateTimeDateIntervalDatePeriod 和相关的 类做更复杂的日期处理(比如在 GUI 管理工具中显示日历中的所有计划任务)

您可能有一个数据库 table 包含类似于以下内容的任务计划规则:

id - unique auto-increment
name - human-readable task name
owner - perhaps forieg key to user tables so you know who owns tasks
interval - An string interval specification as used in DateInterval
start_time - Datetime When rule goes into effect
end_time - Datetime When rule is no longer in effect
script_path - path to script of some sort of command recognized by your applcation
last_execution - Datetime for last time script was triggered
next_execution - Datetime in which you store value calculated to be next execution point
active - maybe a flag to enable/disable a rule
perhaps other admin fields like created_time, error_tracking, etc.

并且您可以轻松地构建一组 DatePeriod 对象,您可以从每个 table 行迭代这些对象。这可能看起来像:

// have one authoritative now that you use in this script 
$now = DateTime();
$now_sql = $now->format('Y-m-d H:i:s'); 


$sql = <<<EOT

SELECT
    id,
    name,
    interval,
    /* etc */
FROM task_rules
WHERE
    active = 1
    AND
        (IS_NULL(start_time) OR start_time <= '{$now_sql}')
    AND
        (IS_NULL(end_time) OR eend_time > '{$now_sql}')
    /* Add this filter if you are trying to query this table
        for overdue events */
    AND
        next_execution <= '{$now_sql}'
    /* any other filtering you might want to do */
/* Any ORDER BY and LIMIT clauses */

EOT;


$tasks = array();
//logic to read rows from DB
while ($row = /* Your DB fetch mechanism */) {
    // build your task (probably could be its own class,
    // perhaps saturated via DB retrieval process), but this is jist
    $task = new stdClass();
    $task->id = $row->id
    $task->name = $row->name;
    $task->interval = $row->interval;
    $task->start_time = $row->start_time;
    // etc. basically map DB row to an object

    // start building DateTime and related object representations
    // of your tasks
    $task->dateInterval = new DateInterval($task->interval);

    // determine start/end dates for task sequence
    if(empty($task->start_time)) {
        // no defined start date, so build start date from last executed time
        $task->startDateTime = DateTime::createFromFormat(
            'Y-m-d H:i:s',
            $task->last_execution
        );
    } else {
        // start date known, so we want to base period sequence on start date
        $task->startDateTime = DateTime::createFromFormat(
            'Y-m-d H:i:s',
            $task->start_date
        );
    }

    if(empty($task->end_time)) {
        // No defined end. So set artificial end date based on app needs
        // (like we need to show next week, month, year)
       $end_datetime = clone $now;
       $end_datetime->modify(+ 1 month);
       $task->endDateTime = $end_datetime;
    } else {
       $task->endDateTime = DateTime::createFromFormat(
            'Y-m-d H:i:s',
            $task->end_time
        );
    }

    $task->datePeriod = new DatePeriod(
        $task->startDateTime,
        $task->dateInterval,
        $task->endDateTime
    );

    // iterate datePeriod to build array of occurences
    // which is more useful than just working with Traversable
    // interface of datePeriod and allows you to filter out past
    // scheduled occurences
    $task->future_occurrences = [];
    foreach ($task->datePeriod as $occurence) {
        if ($occurence < $now) {
            // this is occcurrence in past, do nothing
            continue;
        }

        $task->future_occurrences[] = $occurence;
    }

    $task->nextDateTime = null;    
    if(count($task->future_occurrences) > 0) {
        $task->nextDateTime = $task->future_occurrences[0];
        $task->next_execution = $task->nextDateTime->format('Y-m-d H:i:s');
    }     

    $tasks[] = $task;
}

此处 $tasks 将包含一个对象数组,每个对象代表一个规则以及有形的 PHP DateTime、DatePeriod 构造,您可以使用它们来执行 and/or 显示任务。

例如:

// execute all tasks
// just using a simple loop example here
foreach($tasks as $task) {
    $command = 'php ' . $task->script_path;
    exec($command);

    // update DB
    $sql = <<<EOT

UPDATE task_rules
SET
    last_execution = '{$now_sql}',
    next_execution = '{$task->next_execution}'
WHERE id = {$task->id}

EOT;

     // and execute using DB tool of choice
}