Scenario ¶
Following on from international dates, I also wanted times in the local timezone and format. This was a little more tricky but I think I've got a solution. This is only really appropriate if you have an international app.
Setup ¶
Column types ¶
Change all date, time, datetime columns to timestamp. This is because timestamp fields are converted and stored as UTC They can also accept values such as ' 2011-05-25T01:42:48+1000'. You can also then use 'set time_zone' in phpmyadmin to display timestamp fields in your local time zone.
Config ¶
In protected/config/main.php make the following changes
// application components
'components'=>array(
...
'localtime'=>array(
'class'=>'LocalTime',
),
...
'db'=>array(
...
'initSQLs'=>array("set time_zone='+00:00';"),
...
),
This makes the LocalTime class available as Yii::app()->localtime
The sql 'set time_zone' ensures that all dates retrieved are in UTC time. If your MySql server has the time zone names installed then use 'UTC' instead of '+00:00'.
Components ¶
Copy the code at the end of this wiki for LocalTime.php and DefaultDateTimeParser.php into the folder protected/component.
User login ¶
After the user logs in, set these 2 properties
Yii::app()->localtime->Locale = $user->locale->code; // eg 'en_gb'
Yii::app()->localtime->TimeZone = $user->timezone->name; // eg 'Europe/London'
I set these in the authenticate() function in protected/components/useridentity.php. The localtime component saves the values as global variables, so you only need to set them once.
For the locale and timezone data, rather than fill up this wiki, I've posted 2 mysql scripts on my blog at http://www.russellengland.com/2011/05/mysql-scripts-to-create-locale-and.html
Usage in models ¶
For any date or time fields in your model, change the following
Rules ¶
Use either getLocalDateTimeFormat($datewidth,$timewidth) or getLocalDateFormat($datewidth) to get the date/time format for the locale
public function rules()
{
return array(
...
array('date_of_birth',
'date',
'format'=>
Yii::app()->localtime->getLocalDateFormat('short')),
...
array('start_time',
'date',
'format'=>
Yii::app()->localtime->getLocalDateTimeFormat('short','short')),
...
Before save ¶
Use fromLocalDateTime for both date and date+time values. This will convert the local time string into an ISO 8601 format eg: '2011-05-25T01:42:48+1000'
protected function beforeSave()
{
if(parent::beforeSave())
{
...
$this->date_of_birth =
Yii::app()->localtime->fromLocalDateTime(
$this->date_of_birth,'short');
...
$this->start_time =
Yii::app()->localtime->fromLocalDateTime(
$this->start_time,'short','short');
...
return true;
}
else
return false;
}
After find ¶
Finally use either toLocalDate() or toLocalDateTime() to convert the time to the local format
protected function afterFind()
{
...
$this->date_of_birth =
Yii::app()->localtime->toLocalDate(
$this->date_of_birth,'short');
...
$this->start_time =
Yii::app()->localtime->toLocalDateTime(
$this->start_time,'short','short');
...
return (parent::afterFind());
}
Now() ¶
Because the sql time zone is UTC, DBExpression('NOW()') should return a UTC time. But for a belt and braces approach I use
$this->updated = Yii::app()->localtime->UTCNow;
There's also a localNow if you need to display the current time
echo Yii::app()->localtime->localNow;
LocalTime component ¶
Copy this code to protected/component/LocalTime.php
<?php
class LocalTime extends CApplicationComponent
{
/*
* Notes
* 1. Column field types must be timestamp not date, time, datetime
* This is because timestamp columns are stored as UTC then converted to the specified timezone
* date, time and datetime columns don't save the timezone
* 2. Set the timezone to UTC in protected/config/main.php
* so that all retrieved times are in the UTC timezone for consistency
* 'db'=>array(
* ...
* 'initSQLs'=>array("set names utf8;set time_zone='+00:00';"),
* ),
* 3. When using phpMyAdmin, just use "set time_zone='+00:00'"
* or whatever timezone you require to display timestamps in your zone
* 4. Yii::app()->setTimeZone() and setLanguage() is not saved globally
* So this class is used to save the users timezone and locale
* 5. After a user logs in call Yii::app()->localtime->setLocale('en_gb');
* and Yii->app()->localtime->setTimeZone('Europe/London');
* 6. All date/time formats default to 'short' eg: dd/mm/yyyy hh:mm - no seconds
*/
// Used for setting/getting the global variable - change this if there are conflicts
const _globalTimeZone = 'LocalTime_timezone';
const _globalLocale = 'LocalTime_locale';
// Default server time
const _utc = 'UTC';
// Set the timezone - usually after the user has logged in
// Use one of the supported timezones, eg: Europe/London as this will calculate daylight saving hours
// http://php.net/manual/en/timezones.php
public function setTimezone($timezone)
{
// Save the timezone for the session
Yii::app()->setGlobalState(self::_globalTimeZone,$timezone);
}
// Return the current timezone
public function getTimezone()
{
// Get the localDateTimeZone if its been set
$timezone=Yii::app()->getGlobalState(self::_globalTimeZone);
// Default to UTC if it hasn't
if ($timezone===null)
$timezone=self::_utc;
return($timezone);
}
// Set the locale - usually after the user has logged in
// Use one of the supported locales, eg: en_gb
// http://php.net/manual/en/timezones.php
public function setLocale($locale)
{
// Save the timezone for the session
Yii::app()->setGlobalState(self::_globalLocale,$locale);
}
// Return the current locale
public function getLocale()
{
// Get the localDateTimeZone if its been set
$locale=Yii::app()->getGlobalState(self::_globalLocale);
// Default to yii language if it isn't - note that Yii::app()->setLanguage doesn't save globally
if ($locale===null)
$locale=Yii::app()->language;
return($locale);
}
// Local now() function
// Can use any of the php date() formats to return the local date/time value
// http://php.net/manual/en/function.date.php
public function getLocalNow($format=DATE_ISO8601)
{
$localnow=new DateTime(null,$this->localDateTimeZone);
return $localnow->format($format);
}
// UTC Now() function
// Can use any of the php date() formats to return the UTC date/time value
// http://php.net/manual/en/function.date.php
public function getUTCNow($format=DATE_ISO8601)
{
$utcnow=new DateTime(null,$this->serverDateTimeZone);
return $utcnow->format($format);
}
// Return a datetimezone object for the local time
public function getLocalDateTimeZone($timezone=null)
{
if ($timezone===null)
$timezone = $this->timezone;
// Create a local datetimezone object
$datetimezone = new DateTimeZone($timezone);
return($datetimezone);
}
// Return a datetimezone object for UTC
public function getServerDateTimeZone()
{
// Create a local datetimezone object
$datetimezone = new DateTimeZone(self::_utc);
return($datetimezone);
}
// Converts a timestamp from UTC to a local time
// Expects a date in Y-m-d H:i:s type format and assumes it is UTC
// Returns a date in the local time zone
public function fromUTC($servertimestamp)
{
// Its okay if an ISO8601 time is passed because the timezone in the string will be used and the _serverDateTimeZone object is ignored
$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);
// Then set the timezone to local and it will be automagically updated, even allowing for daylight saving
$localtime->setTimeZone($this->localDateTimeZone);
// Return as 2010-08-15 15:52:01 for use in the yii app
return($localtime->format('Y-m-d H:i:s'));
}
// Converts a local timestamp to UTC
// Expects a date in Y-m-d H:i:s format and assumes it is the local time zone
// Returns an ISO date in the UTC zone
public function toUTC($localtimestamp)
{
// Create an object using the local time zone - this will account for daylight saving
$servertime = new DateTime($localtimestamp,$this->localDateTimeZone);
// Then set the timezone to UTC and it will be automagically updated
// In theory this step isn't needed if using the ISO8601 format.
$servertime->setTimeZone($this->serverDateTimeZone);
// Return as 2010-08-15T15:52:01+0000 so the timestamp column is properly updated
return($servertime->format(DATE_ISO8601));
}
// Use in afterFind
// Ensure that the SQL "set time_zone='+00:00'" has been set
// Returns a date/time combination based on the current locale
// Expects a date/time in the yyyy-mm-dd hh:mm:ss type format
public function toLocalDateTime($servertimestamp,$datewidth='short',$timewidth='short')
{
// Create a server datetime object
$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);
// Then set the timezone to local and it will be automagically updated, even allowing for daylight saving
$localtime->setTimeZone($this->localDateTimeZone);
// Get the local yii date+time format
$localformat = $this->getLocalDateTimeFormat($datewidth,$timewidth);
// Convert to php date format
$localformat = $this->YiitoPHPDateFormat($localformat);
// Return as a local datetime
return($localtime->format($localformat));
}
// Use in afterFind
// Ensure that the SQL "set time_zone='+00:00'" has been set
// Returns the date based on the current locale
// Expects a date string in the yyyy-mm-dd hh:mm:ss type format
public function toLocalDate($servertimestamp,$datewidth='short')
{
// Create a server datetime object
$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);
// Then set the timezone to local and it will be automagically updated, even allowing for daylight saving
$localtime->setTimeZone($this->localDateTimeZone);
// Get the local yii date only format
$localformat = $this->getLocalDateFormat($datewidth);
// Convert to php date format
$localformat = $this->YiitoPHPDateFormat($localformat);
// Return as a local datetime
return($localtime->format($localformat));
}
// Use in beforeSave
// Converts a date/time string in the locale format to an ISO time for saving to the server
// eg 31/12/2011 will become 2011-12-31T00:00:00+0000
public function fromLocalDateTime($localtime,$datewidth='short',$timewidth='short')
{
// Local datetime format
$localformat = $this->getLocalDateTimeFormat($datewidth,$timewidth);
// Uses a modified CDateTimeParser that defaults the time values rather than return false
// Also returns a time string rather than a timestamp just in case the timestamp is the wrong timezone
$defaults = array('year'=>$this->getLocalNow('Y'), 'month'=>$this->getLocalNow('m'), 'day'=>$this->getLocalNow('d'), 'hour'=>0, 'minute'=>0, 'second'=>0 );
$timevalues = DefaultDateTimeParser::parse($localtime, $localformat, $defaults);
// Create a new date time in the local timezone
$servertime = new DateTime($timevalues,$this->localDateTimeZone);
// Set the timezone to UTC
$servertime = $servertime->setTimeZone($this->serverDateTimeZone);
// Return it as an iso date ready for saving
return($servertime->format(DATE_ISO8601));
}
// Returns the local date time format from yii
public function getLocalDateTimeFormat($datewidth='short',$timewidth='short')
{
// Set the language so the local date/time format is returned
// This assumes the locale has already been set
Yii::app()->setLanguage($this->locale);
// This returns the order of the date time combination, eg {1} {0}
$localformat = Yii::app()->locale->getDateTimeFormat();
// Get the local date format - eg dd/MM/yyyy
$localformat = str_replace('{1}', Yii::app()->locale->getDateFormat($datewidth), $localformat);
// Get the local time format with seconds - eg hh:mm:ss
$localformat = str_replace('{0}', Yii::app()->locale->getTimeFormat($timewidth), $localformat);
return $localformat;
}
// Just a wrapper of the yii getDateFormat function
public function getLocalDateFormat($width='short')
{
// Set the language so the local date format is returned
// Assumes the locale has been set
Yii::app()->setLanguage($this->locale);
// Get the local date format - eg dd/MM/yyyy
$localformat = Yii::app()->locale->getDateFormat($width);
return $localformat;
}
// Converts a yii date format to a php date format
// Eg dd/MM/yyyy HH:mm:ss will be converted to d/m/Y H:i:s
public function YiitoPHPDateFormat($dateformat)
{
$patterns = array(
// 'ampm'=>array('a'=>'a','A'=>'A'),
'microseconds'=>array('zzzz'=>'u','z'=>'u'),
'seconds'=>array('ss'=>'s','s'=>'s'),
'minutes'=>array('mm'=>'i','m'=>'i'),
'24hours'=>array('HH'=>'H','H'=>'G'),
'12hours'=>array('hh'=>'h','h'=>'g'),
'years'=>array('yyyy'=>'Y','yy'=>'y'),
'months'=>array('MM'=>'m','M'=>'n'),
'days'=>array('dd'=>'d','d'=>'j'),
);
// Run through each time pattern
foreach($patterns as $pattern)
{
foreach($pattern as $search=>$replace)
{
if (strpos($dateformat,$search)!==false)
{
// We have a winner!
// Replace the first pattern found and then get out of here
$dateformat = str_replace($search, $replace, $dateformat);
break;
}
}
}
return($dateformat);
}
}
DefaultDateTimeParser ¶
Used by LocalTime, this is a copy of the CDateTimeParser but uses defaults for missing values rather then returning false. It also returns a string rather than a unix timestamp in case the time zone is wrong.
<?php
/**
* DefaultDateTimeParser converts a date/time string to an array
*
* The following pattern characters are recognized:
* <pre>
* Pattern | Description
* ----------------------------------------------------
* d | Day of month 1 to 31, no padding
* dd | Day of month 01 to 31, zero leading
* M | Month digit 1 to 12, no padding
* MM | Month digit 01 to 12, zero leading
* yy | 2 year digit, e.g., 96, 05
* yyyy | 4 year digit, e.g., 2005
* h | Hour in 0 to 23, no padding
* hh | Hour in 00 to 23, zero leading
* H | Hour in 0 to 23, no padding
* HH | Hour in 00 to 23, zero leading
* m | Minutes in 0 to 59, no padding
* mm | Minutes in 00 to 59, zero leading
* s | Seconds in 0 to 59, no padding
* ss | Seconds in 00 to 59, zero leading
* a | AM or PM, case-insensitive (since version 1.1.5)
* ----------------------------------------------------
* </pre>
*
*
* Modified version of http://www.yiiframework.com/doc/api/1.1/CDateTimeParser
*
* This version will accept a pattern and default the time values for any missing pattern
* It returns a string rather than a timestamp in case its the wrong timezone
* Also uses the LocalTime class to get the time for now() in the users timezone
* For example, DefaultDateTimeParser::parse('31/12/2011','dd/MM/yyyy',array('hour'=>0,'minute'=>0,'day'=>0);
* Will return '2011-12-2011 0:0:0'
*/
class DefaultDateTimeParser
{
public static function parse($value,$pattern='MM/dd/yyyy',$defaults=array())
{
$tokens=self::tokenize($pattern);
$i=0;
$n=strlen($value);
foreach($tokens as $token)
{
switch($token)
{
case 'yyyy':
{
if(($year=self::parseInteger($value,$i,4,4))!==null)
$i+=4;
break;
}
case 'yy':
{
if(($year=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($year);
break;
}
case 'MM':
{
if(($month=self::parseInteger($value,$i,2,2))!==null)
$i+=2;
break;
}
case 'M':
{
if(($month=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($month);
break;
}
case 'dd':
{
if(($day=self::parseInteger($value,$i,2,2))!==null)
$i+=2;
break;
}
case 'd':
{
if(($day=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($day);
break;
}
case 'h':
case 'H':
{
if(($hour=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($hour);
break;
}
case 'hh':
case 'HH':
{
if(($hour=self::parseInteger($value,$i,2,2))!==null)
$i+=2;
break;
}
case 'm':
{
if(($minute=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($minute);
break;
}
case 'mm':
{
if(($minute=self::parseInteger($value,$i,2,2))!==null)
$i+=2;
break;
}
case 's':
{
if(($second=self::parseInteger($value,$i,1,2))!==null)
$i+=strlen($second);
break;
}
case 'ss':
{
if(($second=self::parseInteger($value,$i,2,2))!==null)
$i+=2;
break;
}
case 'a':
{
// If this value isn't present then ignore it
if(($ampm=self::parseAmPm($value,$i))===null)
break;
if(isset($hour))
{
if($hour==12 && $ampm==='am')
$hour=0;
else if($hour<12 && $ampm==='pm')
$hour+=12;
}
$i+=2;
break;
}
default:
{
// If the separator pattern doesn't exist in the value, then ignore it
// eg: a space
if (strpos($value, $token)===false)
break;
$tn=strlen($token);
if($i>=$n || substr($value,$i,$tn)!==$token)
return false;
$i+=$tn;
break;
}
}
}
if($i<$n) // somethings gone wrong
return false;
// Defaults to the date/time for the local timezone
// If you don't want to use Yii::app()-localtime->localNow() then simply replace with the php date() function
// Yii::app()->localtime-> = LocalTime::
if(!isset($year))
$year=isset($defaults['year']) ? $defaults['year'] : Yii::app()->localtime->localNow('Y'); // date('Y');
if(!isset($month))
$month=isset($defaults['month']) ? $defaults['month'] : Yii::app()->localtime->localNow('n'); // date('n');
if(!isset($day))
$day=isset($defaults['day']) ? $defaults['day'] : Yii::app()->localtime->localNow('j'); // date('j');
if(!isset($hour))
$hour=isset($defaults['hour']) ? $defaults['hour'] : Yii::app()->localtime->localNow('H'); // date('H');
if(!isset($minute))
$minute=isset($defaults['minute']) ? $defaults['minute'] : Yii::app()->localtime->localNow('i'); // date('i');
if(!isset($second))
$second=isset($defaults['second']) ? $defaults['second'] : Yii::app()->localtime->localNow('s'); // date('s');
$year=(int)$year;
$month=(int)$month;
$day=(int)$day;
$hour=(int)$hour;
$minute=(int)$minute;
$second=(int)$second;
if(CTimestamp::isValidDate($year,$month,$day) && CTimestamp::isValidTime($hour,$minute,$second))
{
// Return a time string rather than a timestamp because the timestamp might be the wrong timezone?
return $year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second;
}
else
return false;
}
/*
* @param string $pattern the pattern that the date string is following
*/
private static function tokenize($pattern)
{
if(!($n=strlen($pattern)))
return array();
$tokens=array();
for($c0=$pattern[0],$start=0,$i=1;$i<$n;++$i)
{
if(($c=$pattern[$i])!==$c0)
{
$tokens[]=substr($pattern,$start,$i-$start);
$c0=$c;
$start=$i;
}
}
$tokens[]=substr($pattern,$start,$n-$start);
return $tokens;
}
/*
* @param string $value the date string to be parsed
* @param integer $offset starting offset
* @param integer $minLength minimum length
* @param integer $maxLength maximum length
*/
protected static function parseInteger($value,$offset,$minLength,$maxLength)
{
for($len=$maxLength;$len>=$minLength;--$len)
{
$v=substr($value,$offset,$len);
if(ctype_digit($v) && strlen($v)>=$minLength)
return $v;
}
// Changed by Russell England to null rather than false
return null;
}
/*
* @param string $value the date string to be parsed
* @param integer $offset starting offset
*/
protected static function parseAmPm($value, $offset)
{
$v=strtolower(substr($value,$offset,2));
return $v==='am' || $v==='pm' ? $v : false;
}
}
extension?
Would be nice if you could pack all this together with the script on your blog and post as an extension...
@mdomba
I was thinking of doing that, but I'm still new to Yii and wasn't entirely sure how to do it... :)
:)
It's more simpler than you think... you can just take a look at the source of some simple extensions to see how they are done... and I'm pretty sure that in no time you will make your own... if you get some problems while doing it... just ask in the forum for help...
really simple but handy!
thanks a lot for this nice wiki.
getting the following erorr
Call to a member function format() on a non-object in /../protected/components/LocalTime.php on line 209
just worked out..
Wasn't using it inside afterSave. what function can I use to convert the date outside of aftersave?
Found the problem
Was getting it on Aftersave as well.
Have found the reason why -
http://stackoverflow.com/questions/2943591/whats-wrong-with-datetime-object
Use behaviors for beforeSave() and afterFind()
Nice article and I suggest to use behaviors for these two methods.
For example write behavior(s) that accepts column names and simply do the job explained in this wiki. After that you can simply attach that behavior(s) in your AR classes when you need it.
Here's an example how you can write an behavior.
http://www.yiiframework.com/wiki/14/autotimestampbehavior/
Global state is wrong
I wonder why you use global state to set the current users timezone. Global state is shared across all sessions, so whenever another user logs in and you call
setTimezone()
(as you suggest) it will change the timezone for all users.Instead you should rather use user state:
Yii::app()->user->setState('_timeZone',$timezone)
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.