按时区发送 cron 作业通知 php

send cron job notification by timezone php

我想编写一个脚本,每天下午 2 点向用户 phone 发送推送通知。但是有来自世界各地的用户,所以我需要根据特定用户的时区及时发送该通知,我将其保存在数据库中 'GMT+4'.

我需要通过 Cron 作业来完成。我从未使用过它,所以我不知道如何编写该脚本并使其在 PHP 中运行。任何人都可以帮助我吗?

一个非常简单的方法是让 cron-job 每 1 分钟调用一个 PHP 脚本,并让它检查数据库中需要完成的事情。如果需要做某事现在(),那就去做吧。

crontab中你可以使用这样的东西,

* * * * *   /usr/bin/php /path/to/my/script.php

script.php可以这样

<?php
$ts = time();

// Run for up to 50 seconds
while($ts + 50 > time())
{
   ... SELECT id, time FROM Table WHERE time <= timeFromGMT(GMTvalue) ...
   if(returns a row){
     Send notification;
     Set a flag that notification is send;
   }
   sleep(5);       
} 

注意:使用一些锁机制防止多个进程同时出现

简介

如果您只对实际的 answer/solution.

感兴趣,请跳至下面的“解决方案摘要”部分

我意识到这是一个老问题,有一个公认的答案并且长期没有活动(坦率地说,在撰写本文时没有那么多观点),但我认为这应该得到更好的答案,因为:

  • 目前接受的这个问题的答案没有深入到细节。例如,答案没有详细说明命名的(伪代码?)mysql 函数“timeFromGMT”,以及使原始问题中描述的程序真正按预期正常工作需要什么。
  • 这个问题几乎涵盖了我的确切场景,但可以通过多种不同方式提出。有很多现有的问答与类似(时区相关)的主题,但 none 到目前为止我发现与这个一样合适;许多答案假设不同的条件(例如 date/time 来自用户的输入)。
  • 我相信这个问题的答案涵盖了比 OP 所描述的更广泛的情况。我认为可以肯定地说,对于很多人来说,使用时区也不是一件微不足道的任务。

前面的一些事情

我重新表述了这个问题以扩大它的范围:

如何使 PHP 脚本(cron 作业)在设定的用户本地时间执行某些操作,仅给定用户的时区 (id)?

  • “仅给出用户的时区 ID”部分是必不可少的。例如,在用户输入计划任务的网络应用程序中,您需要使用时间戳。 但是,如果用户只看到一个选项,例如“每天给我发送一个通知”,我们就没有这些信息。所以我们可能拥有的只是用户的时区 (id),以及他们是否启用了“每天做某事”设置。

  • 这个问题的原帖者提到将用户“时区”以“GMT +4”之类的格式存储,无论是否考虑夏令时都没有提到(或存储?) - 我建议避免这种情况。 我建议存储用户的 PHP timezone (named) id instead (such as "Europe/Amsterdam"). These ids can be used in all of PHP's date & time functions.

  • 处理时区时的另一个一般建议是从 UTC 计算所有内容,因为夏令时不会影响它。 因此,如果您 do 有一个网络应用程序,其中用户在特定 date/time 处输入任务,存储 UTC date/time(或 PHP 的 UNIX 时间戳例如从 time(), which is also always in UTC, even regardless of the timezone set with "date_default_timezone_set() 返回。

  • 为了涵盖所有不同的时区,cron 作业必须至少每 15 分钟 运行 php 脚本。例如。在撰写本文时,时区“Pacific/Chatham”为 UTC +12:45。许多现有文章都介绍了创建 cron 作业本身,应该很容易搜索和设置。

解决方案总结

为了让 cron 作业在用户本地时间的特定时间执行某些操作,我们首先必须知道给定时间当前处于哪个时区。

如果(就像在原始问题中一样)我们想在本地用户时间每天下午 2 点做某事,我们可以遍历所有可能的时区(我们可以用 DateTimeZone::listIdentifiers() 检索),然后检查什么当前每个时区的时间,如果它匹配给定时间(下午 2 点,或本例中的“14:00”),我们知道设置了该特定时区的用户(以及“每天做某事”设置启用)是我们需要从数据库中 select 的那些。为了找到当前给定时间所在的时区,我创建了一个自定义函数 get_timezones_where_time_is().

根据上面的要求,cron 作业需要 运行 在每个可能的时区偏移给定时间以找到所有可能的匹配时区。 IE。至少每 15 分钟一班,并且至少在指定的时间(本例中为下午 2 点)。

这是一个边缘情况,但必须在指定时间的同一分钟内调用 get_timezones_where_time_is() 才能获得预期结果。例如,如果 cron 作业将 运行 在 12:00 服务器时间,但 get_timezones_where_time_is() 仅在 12:01 一分钟后执行,它当然不会找到任何时区此刻它在 14:00 的世界中不再存在(因为那时它已经在某个地方 14:01)。

解决方案

这是一个示例脚本,您可以 运行 使用 cron 作业,有关更多详细信息,请参阅代码中的注释。它应该在 PHP 5 >= 5.2.0.

中工作
<?php
/* Just an example, since inluding files in a cron job can be counter intuitive */
set_include_path('/var/www/vhosts/example.com/');// this must be an absolute path, I recommended to the parent directory of your web/document root.
require_once 'config/config.php';// could contain the mysql login details

// Takes a time (hour and minutes) in the 24 hour clock format WITH LEADING ZERO'S, e.g. "14:00" or "04:00",
// Returns an array of timezone id's (from DateTimeZone::listIdentifiers()) where it currently is that given time, or an empty array.
// Throws exception on false $time input parameter
function get_timezones_where_time_is(string $time)
{
  $time = explode(":", $time);
  if(!isset($time[0]) || !isset($time[1]))
  {
    throw new Exception('Function get_timezones_where_time_is() expects a time paramter in 24 hour format with leading zero\'s, such as "04:00".');
  }
  $ret = array();
  $timeHours = $time[0];
  $timeMinutes = $time[1];
  $timezoneIds = DateTimeZone::listIdentifiers();
  foreach($timezoneIds as $key => $tzId)
  {
    $now = new DateTime(null, new DateTimeZone($tzId));
    if($timeHours == $now->format('H') && $timeMinutes == $now->format('i'))
    {
      $ret[] = $tzId;
    }
  }
  return $ret;
}

try
{
  // get timezone id's where it is currently the given time of the day
  $timezoneMatches = get_timezones_where_time_is('14:00');
  // if no timezones found where it is currently the given time, we have nothing to do
  if(!$timezoneMatches)
  {
    exit;
  }
  // we have atleast 1 found timezone where it is the given time of the day, so prepare and query the database
  $mysqli = new mysqli(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_NAME);
  if ($mysqli->connect_errno)
  {
    throw new Exception('mysqli connection error: ' . $mysqli->connect_error);
  }
  // just an example query, ofcourse
  $query = "SELECT * FROM users WHERE timezone IN ('".implode("', '", $timezoneMatches)."') AND send_daily_notification = 1";
  $selectUsers = $mysqli->query($query);
  if(!$selectUsers)
  {
    throw new Exception("There was probably an error in the query: ".$query);
  }
  while($user = $selectUsers->fetch_assoc())
  {
    // do something for each user, such as sending a notification
  }
  $selectUsers->close();
}
catch (Exception $e)
{
  // logs to php ini defined error log file
  error_log('Cron job "send notification" failed with error message: '.$e->getMessage());
  // output error here for easier debugging when running script directly (e.g. from command line)
  echo 'Failed with error: '.$e->getMessage();
}
?>

后记

  • mysqli 的错误处理和选择当然是个人偏好,我尽我所能将最佳实践包含在我的知识中,并针对我认为访问 Whosebug 的观众。例如,如果您认为自己要处理不同的数据库系统,您 should/would 可能想使用 PHP PDO
  • 在我的 test/development 服务器上 DateTimeZone::listIdentifiers() 包含 425 个时区;当找到 50 个时区时,具有给定时间的(自定义)函数 get_timezones_where_time_is() 在大约 20 毫秒内完成。在给定的场景中,函数 ony 需要被调用一次。
  • 例如,在脚本中使用 DateTime object (in the loop in function get_timezones_where_time_is()) means we're also accounting for daylight saving time. PHP ini timezone settings or earlier calls to date_default_timezone_set() 检查给定时区的实际当前时间不应影响结果。
  • 我建议避免选择夏令时切换期间的时间。例如。 get_timezones_where_time_is('02:00') 可能会做意想不到的事情,如果时钟从 03:00 回到 02:00 脚本很可能会 'run again' 因为它实际上又是那个时候(我没测试过)
  • DateTimeZone::listAbbreviations() 可能看起来像是(部分)解决方案,但它包含历史时区数据,因此相同的时区 ID 可以多次出现在其中,但偏移量不同(请参阅当前最受好评的评论链接 php 手册页),这使得它不适合通过偏移量获取时区 ID。
  • 我没有 real_escape_string 时区 ID,因为我认为它们是可信的输入。如果引入了带引号的时区 ID,则显示的示例数据库查询将中断。
  • 如果像 OP 一样,您只以“GMT +4”格式存储用户时区,而不管这是否包括夏令时,这就有点像猜谜游戏了。我可能会遍历我的 get_timezones_where_time_is() 函数返回的时区 ID,为每个时区创建一个 DateTimeZone 对象并使用 DateTimeZone::getOffset() 以秒为单位获取 GMT 偏移量并更正夏令时。将该偏移量转换为小时,然后使用它以“GMT +4”等格式查询数据库中具有时区的用户。可以的话最好解决数据问题