2016-09-27 96 views
2

我试图为我的网站上的管理员创建一种方法来安排类似于我将在服务器上安装cron作业来运行特定脚本的任务。我希望他们在任务运行时具有类似的控制,例如,每天在14:00或每周星期四在12:00等PHP任务计划程序由网站管理员配置

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

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

我真的很感激,如果有人能指出我在正确的方向。

+0

这取决于你有多少细粒度控制和可访问给他们?这些管理员有权访问上传php文件吗?您可以从数据库条目生成一个txt文件,并实际修改crontab。 – Blake

+0

[使用PHP创建,编辑和删除crontab作业?]可能重复(http://stackoverflow.com/questions/4421020/use-php-to-create-edit-and-delete-crontab-jobs) – Hackerman

+1

@ Hackerman看起来像是想要一个非cron的方法。那么谁知道呢。 – Drew

回答

2

以下是我在过去的项目中看到过的一个简单的解释和例子。为简洁起见,我没有考虑安全因素,但请注意,让用户指定要运行的命令本身并不安全。

任务SQL表

你需要这三根柱子为您的执行脚本来消费。间隔列是一个cron字符串(分钟小时,每月的一年)。 script_path列是脚本将运行的路径。 last_executed列是上次运行该任务的时间。 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]); 
    } 
} 
+0

您的cron-syntax时间间隔字段需要某种操作,以便在时间比较中使用它。 –

+0

@MikeBrant这是一个实现细节,我已经把这个答案留下了。 OP可以从[许多](https://packagist.org/search/?q=cron+expression+parser)cron解析库中选择,他应该选择使用此实现。 – Enijar

+0

谢谢,这看起来正是我所追求的。如果一切顺利,我会先试一下,然后将其标记为答案。 – nfplee

0

这个想法相当简单,看起来好像你已经很好地掌握了它。如果管理员可以安排一组定义的“任务”,那么将其存储在数据库表中以及应该运行时间戳时很简单。然后,您将有一个脚本(例如,“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”文件只是连接到数据层,并找到计划运行但尚未运行的任何“作业”(您可以将其标记为“已执行”或将其从他们跑完后的表格,你的电话)。

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

很好的答案。唯一我建议的是DB中的'time_to_run'字段应该是本地日期时间或时间戳字段。这种方式你的地方cluase只是'WHERE time_to_run <= NOW()'铸造到'DATE'的方法(它对任务管理器来说可能不够精细)不允许在'time_to_run'上使用的索引这个查询。 –

2

在这里的其他答案一些好主意。我还要指出的是,你应该给予考虑使用PHP的DateTimeDateIntervalDatePeriod,以及相关的类,如果你发现自己需要做更复杂的日期处理(如在GUI管理工具中的日历显示所有预定的任务)

您可能有看起来像一个包含数据库表的任务调度规则:

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 
}