由站点管理员配置的PHP任务调度器

由站点管理员配置的PHP任务调度器,php,mysql,cron-task,Php,Mysql,Cron Task,我正在尝试为我的站点上的管理员创建一种方法来安排一项任务,类似于我在服务器上设置cron作业以运行特定脚本的方式。我希望他们对任务运行的时间有类似的控制,例如每天14:00或每周周四12:00等 我想我会有一个表单,询问他们希望多长时间运行一次任务,几天/几周等。然后这些信息会存储在数据库中。接下来,我将创建一个cron作业,以便每分钟运行一个脚本。然后,该脚本将选择数据库中应该运行和执行的所有任务 我一直在研究这方面的任务调度器,到目前为止,似乎大多数任务调度器都是为web开发人员以编程方式调

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

我想我会有一个表单,询问他们希望多长时间运行一次任务,几天/几周等。然后这些信息会存储在数据库中。接下来,我将创建一个cron作业,以便每分钟运行一个脚本。然后,该脚本将选择数据库中应该运行和执行的所有任务

我一直在研究这方面的任务调度器,到目前为止,似乎大多数任务调度器都是为web开发人员以编程方式调度任务而构建的。相反,我希望将它们存储在数据库中,然后编写SQL查询来选择要运行的正确任务。我想知道我应该使用什么结构在数据库中存储计划,以及如何检索在特定时间运行的正确任务


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

这个想法相当简单,而且看起来你已经很好地掌握了它。如果您已经定义了一组管理员可以安排的任务,那么简单的方法就是将它们存储在数据库表中,并附上它们应该运行的时间戳。然后,您将有一个单独的脚本,例如,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!
}
任务调度器web表单将包含管理员可以计划运行的作业列表,例如,该列表可能包含与前面提到的RunSalesReport类相关的RunSalesReport作业。web表单的服务器端处理程序只是将表单数据存储在数据库表中

数据库表可以只包含一个time_to_run列,用于确定作业应该何时运行,以及一个job_class列,用于保存应该实例化/分解的类名

job_runner.php文件只需连接到数据层,并查找计划运行但尚未运行的任何作业。您可以将其标记为已执行,或者在运行后将其从表中删除,然后调用

// 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();
}

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

任务SQL表

您需要为执行脚本设置这三列以使用它们。interval列是一个cron字符串分钟小时天月年。脚本路径列是脚本运行的路径。最后执行的列是该任务上次运行的时间。interval和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]);
    }
}

其他答案中有一些好的想法。我还想指出,如果您发现自己需要执行更复杂的日期处理,例如在GUI管理工具中在日历中显示所有计划的任务,那么您应该考虑使用PHP的DateTime、DateInterval、DatePeriod和相关类

您可能有一个包含任务计划规则的DB表,该规则类似于:

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.
// 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;
}
而且,您可以轻松构建DatePeriod对象的集合,可以从每个表行进行迭代。这可能看起来像:

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.
// 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;
}

这取决于您为它们提供了多少粒度控制和可访问性?这些管理员有权上传php文件吗?您可以从数据库条目生成一个txt文件,并实际修改crontab。可能的@Hackerman副本看起来op需要一种非cron方法。谁知道呢,回答得好。我唯一建议的是,数据库中的time-to-run字段应该是原生的datetime或timestamp字段。这样,where cluase就是运行cron syntax interval字段的时间,需要进行某种操作,使其在时间比较中有意义。@MikeBrant这是一个实现细节,我在回答这个问题时省略了它。OP可以从cron解析库中选择,如果他选择使用这个实现的话。我会做一个测试,如果一切正常,我会把它标记为答案。