I’ve been working on an interesting problem to solve in Laravel lately for a private monitoring dashboard application that I am building. One of the requirement is to display the date either as a text, or the value of a datepicker input field according to the current timezone.

In this article, I would like to walk through the final decision that I made, and the technical implementation around it.

Essentially what I ended up with are three key points:

  • Always stored the date in UTC.
  • Create a custom cast to handle the conversion.
  • Let the user decides on the timezone themselves.

Always stored the date in UTC

Your implementation may vary, but I think for maximum compatibility and less headache down the road, always stored the date in UTC in the database. That way, you do not have to waste your time to figure out the timezone of the date when you’re fetching it from the database.

Which leads me to the point #2.

Create a custom cast to handle the conversion

On top of various built-in casts provided by Laravel out of the box, it also has support for custom defined cast as mentioned here. Ultimately, all you need to care about are:

  • get method to handle conversion of the date from UTC to the user-defined timezone.
  • set method to handle conversion of the user input back to UTC before it is saved into the database again.

To get started, we can create a custom cast for our model attribute via:

1
php artisan make:casts LocalizedDatetime

The entirety of the LocalizedDatetime cast is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Casts;

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Date;

class LocalizedDatetime implements CastsAttributes
{
    /**
     * Cast the given value.
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): ?CarbonImmutable
    {
        if (!$value) {
            return null;
        }

        return Date::parse($value)->setTimezone(get_timezone());
    }

    /**
     * Prepare the given value for storage.
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): ?CarbonImmutable
    {
        if (!$value) {
            return null;
        }

        return Date::parse($value, get_timezone())->setTimezone(config('app.timezone'));
    }
}

To apply this custom cast to our Eloquent model attribute, we can simply configure them inside the $casts property. Assuming we have an imaginary model called Post that has an attribute called published_at to store the published date, then our Post $casts property will look like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// app/Model/Posts.php

namespace App\Models;

use App\Casts\LocalizedDatetime;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    ...
    
    protected $casts = [
        'published_at' => LocalizedDatetime::class,
    ];
    
    ...
}

Once this is configured, anytime we try to access the published_at property, it will be automatically converted to the date with the correct user-defined timezone.

However, there are a few things I would like to note from the above implementation:

  • I am using CarbonImmutable here, which is outside the scope of this article, but if you are interested, I recommend reading about this ever excellent post by Michael Dyrynda on using immutable dates in Laravel.
  • config('app.timezone') is UTC by default, and I do not recommend changing them.
  • get_timezone is a simple helper to fetch the user-defined timezone that fallback to UTC which I’ll expand on in the next section.

Let the user decides on the timezone themselves

As mentioned above, get_timezone is simply a helper function to retrieve the timezone defined by the user.

1
2
3
function get_timezone(): string {
    return optional(auth()->user())->timezone ?? config('app.timezone');
}

Again, if it’s not defined, or there’s no authenticated user, then it will fall back to UTC.

I’ve seen few other alternatives implementation to fetch the user timezone, ranging from deducing the timezone from the user’s IP address on login, to detecting it via JavaScript then send it to the server.

Personally I think what works best in my use case is to define a new column called timezone that is nullable and allows user to select it manually from a dropdown. A nullable column also helps me to define custom logic that will allow me to display certain notice on the dashboard in case it hasn’t been set yet.