August 25, 2015 15:45

Here is a simple way to manage storing and displaying dates and times in Laravel in the user's timezone.

First of all, we are going to store all dates and times in UTC. In Laravel, make sure that the timezone is set to UTC in config/app.php

Second, you'll need a way to let users specify their timezone. I'll leave that up to the reader for now. You can either do this through a settings page, or auto-detect it with JavaScript. I prefer to store timezones as an hourly offset from UTC, e.g. -5 for Central time, -4 for Eastern.

Finally, we'll have a base model that all of our Eloquent models extend. If you already have one you can just add this function to it. Save this into app/Model.php

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model as Eloquent;

class Model extends Eloquent
{
    /**
     * Display timestamps in user's timezone
     */
    protected function asDateTime($value)
    {
        if ($value instanceof Carbon) {
            return $value;
        }

        $me = \Auth::user();
        $tz = $me->timezone ?: \Cookie::get('tz');

        $value = parent::asDateTime($value);

        return $value->addHours($tz);
    }
}

Make sure to update all of your existing models to extend the new model class instead of the Eloquent model.

Eloquent has an asDateTime function which converts MySQL timestamps to Carbon entities. It calls this function both when setting and getting timestamps. So first we check if the $value is already a Carbon instance - if so, we return it without any adjustments, because when setting a timestamp we do NOT want to apply the timezone adjustment. **

The next two lines are simply retrieving the user's timezone from one of two sources, an attribute or a cookie. If you are setting the cookie through JavaScript, make sure it is added as an exception to the EncryptCookies middleware. Again, this value in my case is an hour offset e.g. -5. Finally we get the Carbon instance that Eloquent returns and add the hour offset.

Now all dates and times will be stored in UTC but displayed in the user's timezone!

Two caveats that I've discovered so far. First, when setting timestamps, to make sure to set them as Carbon instances. (You don't need to worry about the created_at, updated_at, or deleted_at timestamps, just any others that you use). For example, do this:

$post->scheduled_at = Carbon::now();

Not these:

$post->scheduled_at = \DB::raw('now');
$post->scheduled_at = date('Y-m-d H:i:s');

Otherwise your timestamps will not be stored in UTC.

The more annoying caveat is that for certain Carbon methods, you need to undo the hour adjustment in order for them to work correctly. The two that I've discovered so far are age and diffForHumans, because both of those methods compare the given timestamp to the current timestamp (in UTC). So if your user's timezone is -5 hours, and you ask for $date->diffForHumans() on a brand new timestamp, it'll falsely report that the date is 5 hours ago. I personally added a helper method undo_tz to wrap around those methods. For example:

echo undo_tz($post->created_at)->diffForHumans();

And the method:

function undo_tz(Carbon $date)
{
    $me = \Auth::user();
    $tz = $me->timezone ?: \Cookie::get('tz');
    return $date->subHours($tz);
}

Laravel, PHP