通过时区php发送cron作业通知

时间:2015-07-07 07:45:10

标签: php cron timezone timezone-offset

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

我需要通过Cron工作来做到这一点。我从来没有使用它,所以我不知道如何编写该脚本并使其在PHP中工作。有人可以帮帮我吗?

2 个答案:

答案 0 :(得分:5)

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

crontab 中,你可以使用类似的东西,

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //other init code here...

    mGestureMask = GestureHandler.GESTURE_ALL;
    mGestureHandler = new GestureHandlerAndroid(metaioSDK, mGestureMask);
}

script.php 可以像这样

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

注意: 使用一些锁定机制来同时阻止多个进程。

答案 1 :(得分:0)

简介

如果您只对实际答案/解决方案感兴趣,请跳到下面的“解决方案摘要”部分。

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

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

前面的一些事情

我稍微重新表述了这个问题以扩大它:

如何让 PHP 脚本(cron 作业)在设定的用户本地时间做某事,只给定用户的时区 (id)?

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

  • 这个问题的原始海报提到让用户“时区”以“GMT + 4”等格式存储,无论是否考虑夏令时都没有提到(或存储? ) - 我建议避免这种情况。 我建议改为存储用户的 PHP timezone (named) id(例如“欧洲/阿姆斯特丹”)。这些 ID 可用于所有 PHP's date & time functions

  • 处理时区时的另一个一般建议是从 UTC 计算所有内容,因为夏令时不会影响它。 因此,如果您确实有一个用户在特定日期/时间输入任务的 Web 应用程序,请存储 UTC 日期/时间(或从 time() 返回的 PHP 的 UNIX 时间戳,它也总是在UTC,即使时区设置为“date_default_timezone_set()”,例如)。

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

解决方案总结

为了让 cron 作业在用户本地时间的特定时间做某事,我们首先必须知道当前在哪个时区。

如果(就像在原始问题中一样)我们想在本地用户时间每天下午 2 点做某事,我们可以遍历所有可能的时区(我们可以用 DateTimeZone::listIdentifiers() 检索),然后检查它的时间是针对当前的每个时区,如果它与给定的时间(下午 2 点,或本例中的“14:00”)匹配,我们知道设置了该特定时区的用户(以及启用了“每天做某事”设置)是我们需要从数据库中选择的那些。为了查找当前给定时间所在的时区,我创建了一个自定义函数 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 的错误处理和选择当然是个人喜好,我尽我所能将最佳实践纳入我的知识中,并针对我认为访问 StackOverflow 的观众。例如,如果您认为自己要处理不同的数据库系统,则您应该/可能想要使用 PHP PDO
  • 在我的测试/开发服务器上 DateTimeZone::listIdentifiers() 包含 425 个时区;当找到 50 个时区时,具有给定时间的(自定义)函数 get_timezones_where_time_is() 在大约 20 毫秒内完成。在给定的场景中,仅需要调用一次函数。
  • 使用 DateTime 对象(在函数 get_timezones_where_time_is() 的循环中)检查给定时区中的实际当前时间意味着我们还考虑了夏令时。例如,PHP ini 时区设置或之前在脚本中调用 date_default_timezone_set() 不应影响结果。
  • 我建议避免选择可能在夏令时切换期间的时间。例如。 get_timezones_where_time_is('02:00') 可能会做意想不到的事情,如果时钟从 03:00 回到 02:00,脚本很可能会“再次运行”,因为它实际上又是那个时间(我还没有测试过) .
  • DateTimeZone::listAbbreviations() 可能看起来像是(一部分)解决方案,但它包含历史时区数据,因此相同的时区 id 可以以不同的偏移量多次出现在其中(请参阅链接的 php 上当前最受好评的评论手册页),这使得例如通过偏移量获取时区 ID 是不合适的。
  • 我没有 real_escape_string 时区 ID,因为我认为它们是可信输入。如果引入了带引号的时区 ID,则显示的示例数据库查询将中断。
  • 如果像 OP 一样,您仅以“GMT +4”的格式存储用户时区,并且不包括夏令时,那么这就有点像猜谜游戏了。我可能会遍历 get_timezones_where_time_is() 函数返回的时区 ID,为每个时区创建一个 DateTimeZone 对象并使用 DateTimeZone::getOffset() 以秒为单位获取 GMT 偏移量并校正 dst。将该偏移量转换为小时,然后使用它来查询数据库中时区格式为“GMT +4”的用户。如果可能,最好解决数据问题。