At sign-up, I collect the user’s country and set the nearest timezone (they can change this if it is incorrect).
I want to display time local to a user. Every is stored in the database as UTC (in the code below, ‘Zulu’ = UTC), and that zone is set at the start of every run.
Some Googling didn’t really help (e.g. Dan Grossman’s question a few years ago had the same problem).
What I have: (you can re-use this if you want)
class TimezoneTable extends Singleton
{
private $zones;
protected function __construct()
{
$dir = sfConfig::get('sf_data_dir');
$dataFile = $dir.'/timezones';
$timerFile = $dir.'/timezones-timer';
$logFile = $dir.'/timezones-log';
// See if the cache is less than a day old.
if (is_file($timerFile))
{
$lastTime = (integer) file_get_contents($timerFile);
if ($lastTime > (time() - 89000))
{
// Cache is fresh.
$this->zones = unserialize(file_get_contents($dataFile));
return;
}
}
$vals = json_decode(file_get_contents('http://api.coronatus.com/clock/read'), true);
$zones = array();
foreach ($vals as $val)
{
$zones[$val['timezone']] = $val['utc_offset'];
}
file_put_contents($dataFile, serialize($zones));
file_put_contents($timerFile, time());
file_put_contents($logFile, '['.date('Y-m-d H:i:i')."] Refreshed\
", FILE_APPEND);
$this->zones = $zones;
}
public function getAllNames()
{
return array_keys($this->zones);
}
/**
* Return the number of hours $timezone is off of UTC.
*
* @param string $timezone
* @return integer Number of hours.
*/
public function getOffsetInHours($timezone)
{
if (!isset($this->zones[$timezone]))
{
throw new InvalidArgumentException('Timezone not known: '.$timezone);
}
return $this->zones[$timezone];
}
}
/**
* Try to change any string/unix-timestamp/DateTime-object to a unix timestamp.
*
* Timezones are ignored.
*
* @param mixed $time Any type of date-time, in Zulu
* @return string Seconds since Epoch, in Zulu
*/
function convert_to_seconds($time)
{
if ($time instanceof DateTime)
{
$seconds = $time->format('U');
}
// Already in seconds.
elseif (ctype_digit($time) && (10 === strlen($time)))
{
$seconds = $time;
}
// YYYY-MM-DD HH:MM:SS
elseif (1 === preg_match('/\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d/', $time))
{
$seconds = strtotime($time);
}
else
{
throw new InvalidArgumentException(
'Time couldn\\'t be interpreted: '.Coronatus_Util::getArgDescription($time)
);
}
return $seconds;
}
/**
* Describe the difference in time between $then and now.
*
* Timezones are irrelevant to this function.
*
* @param mixed $then A zulu time.
* @return string
*/
function how_long_ago($then)
{
$diff = time() - convert_to_seconds($then);
if ($diff < 0)
{
throw new InvalidArgumentException('Time is in the future: '.$then);
}
return Coronatus_UtilDateTime::timespan($diff).' ago';
}
/**
* Use this function for every date/time displayed.
*
* It alters the time to be in the actor's timezone.
*
* @param string $format How to format the output.
* @param mixed $time The Zulu time to convert.
* @return string
*/
function display_time($format, $time)
{
switch (strtolower($format))
{
case 'mysql':
$format = 'Y-m-d H:i:s';
break;
case 'date':
$format = 'jS F Y';
break;
case 'datetime':
$format = 'jS F Y, H:i';
break;
}
if (!isset($userOffset))
{
static $userOffset;
$userOffset = sfContext::getInstance()->getUser()->getTzSecondOffset();
}
return date($format, convert_to_seconds($time) + $userOffset);
}
display_time() does the showing.
Is there a better way to do this, that doesn’t involve changing PHP’s TZ multiple times?
Thanks