“Why, sometimes I’ve believed as many as six impossible things before breakfast.”
― Lewis Carroll, Alice in Wonderland
A few months ago, we were working on the WooCommerce Pre-Orders extension and had to think about how we wanted to store the release date & time. Like most concepts that look simple and easy at first, this turned out to be somewhat complex. Throughout development, I ended up learning quite a bit about working with timezones inside WordPress. This article should help you understand how to work with timezones inside WordPress better and give you some time-saving shortcuts.
So back to our Pre-Orders example. First, store admins expect the date & time to be displayed in the same timezone as they’ve set for the site, but this makes determining when a pre-order needs to be released (and automatically charging all those saved credit cards) complicated and potentially unreliable. If the site timezone is changed at some point, there’s no way to know what the old timezone was, unless we store it along with the date & time itself. If it’s stored, then we need to do some calculations each time we check if the pre-order needs to be released. Not really our best option.
Instead let’s save the release date & time as a unix timestamp, which has no timezone (technically it’s UTC, but not exactly). This really simplifies our process to check if a pre-order should be released — a simple call to time()
and we can compare it against the release timestamp. Easy-peasy!
But wait, now we have to convert the release date & time entered by the admin for the pre-order into a unix timestamp. The first try was something like $timestamp = strtotime( $release_datetime );
, but this is wrong because strtotime
uses the default timezone set in PHP which may or may not be the same as the actual site timezone set within WordPress.
What we need to do is grab the site timezone and then create a unix timestamp of the release date & time:
try { // get datetime object from site timezone $datetime = new DateTime( $datetime_string, new DateTimeZone( get_option( 'timezone_string' ) ); // get the unix timestamp (adjusted for the site's timezone already) $timestamp = $datetime->format( 'U' ); } catch ( Exception $e ) { // you'll get an exception most commonly when the date/time string passed isn't a valid date/time }
This makes use of the neat DateTime class, which is available in PHP 5.2+ (making it safe for use with WP) and works great except for one small problem. When the site timezone is set to a UTC offset instead of a timezone, get_option( 'timezone_string' );
is blank, which throws an exception. Argh. Now we need some reliable way to get the site’s timezone string even if it’s set to a UTC offset. It turns out that WordPress has no built-in function to make this happen. Let’s make one:
/** * Returns the timezone string for a site, even if it's set to a UTC offset * * Adapted from http://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155 * * @return string valid PHP timezone string */ function wp_get_timezone_string() { // if site timezone string exists, return it if ( $timezone = get_option( 'timezone_string' ) ) return $timezone; // get UTC offset, if it isn't set then return UTC if ( 0 === ( $utc_offset = get_option( 'gmt_offset', 0 ) ) ) return 'UTC'; // adjust UTC offset from hours to seconds $utc_offset *= 3600; // attempt to guess the timezone string from the UTC offset if ( $timezone = timezone_name_from_abbr( '', $utc_offset, 0 ) ) { return $timezone; } // last try, guess timezone string manually $is_dst = date( 'I' ); foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) return $city['timezone_id']; } } // fallback to UTC return 'UTC'; }
Much better! Now we can just replace our code above with this new function:
try { // get datetime object from site timezone $datetime = new DateTime( $datetime_string, new DateTimeZone( wp_get_timezone_string() ); // get the unix timestamp (adjusted for the site's timezone already) $timestamp = $datetime->format( 'U' ); } catch ( Exception $e ) { // you'll get an exception most commonly when the date/time string passed isn't a valid date/time }
Boom. A WordPress-friendly way to convert a date & time string into a unix timestamp. Let’s consider the opposite direction now. What if we need to convert a unix timestamp (remember unix timestamps are in UTC timezone) back into a timestamp adjusted the site’s timezone? We’ll need to add the timezone offset (in seconds) to the unix timestamp:
try { // get datetime object from unix timestamp $datetime = new DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); // set the timezone to the site timezone $datetime->setTimezone( new DateTimeZone( wp_get_timezone_string() ) ); // return the unix timestamp adjusted to reflect the site's timezone return $timestamp + $datetime->getOffset(); } catch ( Exception $e ) { // something broke }
This works by creating a new DateTime
object from the stored timestamp. The @
in this "@{$timestamp}"
syntax means that the string is a timestamp. The timezone for the object is then set to the site’s timezone, which ensures that the DateTime::getOffset()
method returns the proper offset (in seconds) for the site’s timezone. As an example, this will return -14400
when the site’s timezone is set to EST or UTC-4.
However, timestamps aren’t very useful for displaying to a customer, so let’s use the ever-useful date_i18n()
and the date_format
option to format the date properly for display:
date_i18n( get_option( 'date_format' ), $timestamp );
This will display something like “August 21, 2013”. If you wanted to add on the time, you could use the time_format
option along with date()
function:
date( get_option( 'time_format' ), $timestamp );
That’s it! Have questions about timezones and WordPress? Let us know in the comments!
The definitive guide to wrangling timezones with WordPress/WooCommerce has just been written. Thanks Max!
Welcome!
Yeah, WP’s a bag of hurt when you want to set a timezone reliably. Thank you for doing this writeup!
Really an awesome function this is. 🙂
I’ve been searching for 2 hours and found nothing, But your function did my job.. Thanks a lot Max.. 🙂
welcome 🙂
Hello,
While your wp_get_timezone_string() function is great, there seems to be two errors in your code.
You try to detect the timezone in that order:
1) get the timezone_string option
2) get UTC offset
3) using timezone_name_from_abbr()
4) using timezone_abbreviations_list()
The problem is that while you return the timezone in cases 1, 2 and 4, you don’t with case 3. If you successfully determine the timezone in case 3, you go straight to the “return UTC” at the end.
Also, it looks like timezone_name_from_abbr() needs its third parameter when the first one is an empty string rather than an abbreviation, or it does indeed return false (from what I can judge testing it).
WP is quite a mess when it comes to timezones and I’m in the following case: in France (UTC +1 set in WP settings). When I edit your fonction like this, it gives me the correct timezone:
[…]
// attempt to guess the timezone string from the UTC offset
if ( $timezone = timezone_name_from_abbr( ”, $utc_offset, 0 ) )
return $timezone;
// last try, guess timezone string manually
[…]
I’m not sure it’ll still work next time the DST is adjusted (next spring), though. I find a bit odd that I have to put 0 as the third parameter.
Yep you’re absolutely right here — this function made it’s way into WooCommerce core, can you submit a patch with your changes? https://github.com/woothemes/woocommerce/blob/master/includes/wc-formatting-functions.php#L394-L438
thanks!
Erm, I don’t really know how to submit a patch on GitHub. Can’t you do it?
Well, I opened an issue: https://github.com/woothemes/woocommerce/issues/6897
Not sure it’s the proper way to do it but I don’t know of a better way.
BTW, you should update your article. I’m not sure people will read the comments. 😉
Thanks, I’ll fix it on GitHub. Just updated the article as well 🙂