我正在尝试为我的站点上的管理员创建一种方法来调度任务,类似于我在服务器上设置cron作业以运行特定脚本的方式。我希望他们对任务的运行时间有类似的控制,例如每天14:00或每周周四12:00等。
我想我会有一个表格,询问他们多久想要运行任务,哪天/几周等。然后将其存储在数据库中。接下来,我将创建一个cron作业来运行一个脚本,比如每分钟运行一次。然后,该脚本将选择数据库中应该运行的所有任务并执行每个任务。
我一直在寻找这个任务调度程序,到目前为止,似乎大多数都是为web开发人员以编程方式调度任务而构建的。相反,我想将它们存储在数据库中,然后编写一个SQL查询来选择要运行的正确任务。我想知道我应该使用什么结构来将计划存储在数据库中,以及如何检索在特定时间运行的正确任务?
如果有人能给我指个方向,我将非常感激。
这里是一个简化的解释和示例,说明我在过去的项目中是如何实现的。为简洁起见,我没有考虑安全问题,但请注意,让用户指定要运行的命令本质上是不安全的。
任务SQL表
您将需要设置这三个列以供执行脚本使用。interval列是一个cron字符串(分钟、小时、天、月、年)。script_path列是脚本将运行的路径。last_performed列是该任务最后一次运行的时间。interval和last_performed列将用于确定是否应该执行任务。
+----+------------+----------------------+---------------------+
| 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.
您可以轻松地构建DatePeriod对象的集合,您可以从每个表行迭代。它可能看起来像:
// 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构造。
// 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
}
这个想法相当简单,看起来您已经很好地掌握了它。如果您有一组已定义的"任务",管理员可以对其进行调度,那么将它们存储在数据库表中并附带运行时间戳就很简单了。然后,您将拥有一个脚本(例如,"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
类相关。web表单的服务器端处理程序只需将表单数据存储在数据库表中。
数据库表可以只包含一个time_to_run
列(用于确定作业应该何时运行)和一个job_class
列(用于保存应该实例化/分解/诸如此类的类名)。
"job_runner.php"文件简单地连接到数据层,并找到任何计划运行但尚未运行的"Jobs"(您可以将其标记为"已执行",或者在它们运行后将它们从表中删除,您的调用)。
// 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();
}