Yii 1.1: Local time zones and locales

27 followers

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;
    }
}

Total 9 comments

#7710 report it
Mike at 2012/04/11 04:42am
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)
#5234 report it
ManInTheBox at 2011/09/25 11:52am
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/

#4314 report it
simonweb at 2011/06/24 05:04pm
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

#4313 report it
simonweb at 2011/06/24 04:09pm
just worked out..

Wasn't using it inside afterSave. what function can I use to convert the date outside of aftersave?

#4312 report it
simonweb at 2011/06/24 03:56pm
getting the following erorr

Call to a member function format() on a non-object in /../protected/components/LocalTime.php on line 209

#4247 report it
bipu at 2011/06/20 02:56am
really simple but handy!

thanks a lot for this nice wiki.

#3992 report it
Maurizio Domba Cerin at 2011/05/25 08:31am
:)

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...

#3987 report it
Russell England at 2011/05/25 06:23am
@mdomba

I was thinking of doing that, but I'm still new to Yii and wasn't entirely sure how to do it... :)

#3986 report it
Maurizio Domba Cerin at 2011/05/25 04:56am
extension?

Would be nice if you could pack all this together with the script on your blog and post as an extension...

Leave a comment

Please to leave your comment.

Write new article