Managing users timezone in a Laravel app

If you are having a hard time managing users timezone in your laravel app you are in right place. In this post, I will be showing ways to manage users timezone. We will create a simple laravel application in which we will guess users timezone from the front end and store in users table to format all the timestamp based on this timezone.

What’s timezone?

You probably already know the timezone, its the change in time for a different location in the world, for example, If here in India currently time is 6:44 AM then at this right moment in US it’s 8:15 PM. This gets tricky to show correct time to another user if we don’t apply the offset value for logged in user from US. By default laravel stores all the date time in the database using UTC.

If your application is only dealing with the users of a specific country then you don’t need to anything, just change the app config timezone to match your areas timezone, it can be anything from PHP’s timezone identifier. In case of US  New York, you can change config/app.php timezone to:

'timezone' => 'America/New_York',

Here is complete list of supported timezone http://php.net/manual/en/timezones.php

Make sure your database (MySQL) timezone is in sync with your php setting to avoid some funky time.

But if your app is used by people from all over the world then you should keep the laravel’s default timezone which is set to UTC.

There are two ways we can guess the timezone of a user and formate the date, first is from the front end or we can do it from the backend.

Detect users Timezone from javascript

We have awesome library moment.js in JS world which can do all sorts of things with time, parse, validate, manipulate, and display dates and times in any formatting, it has also support to handle the timezone.

Pull  the following in libraries:

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone.min.js"></script>

Now if you check you chrome developer console you will have access to following methods provided by moment.

-> moment.tz.guess()
<- "Asia/Calcutta"

It’s giving me "Asia/Calcutta", in your case it should be your timezone which based on your computer’s timezone.

Now we can use this timezone value and add that to users table by passing this timezone from registration form.

Run php artisan make:auth to create the auth scaffolding. Now open the resources/views/layouts/app.blade.php and add above moment js library above the script tag.

<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone-with-data-2012-2022.min.js"></script>
<script src="{{ asset('js/app.js') }}"></script>
@stack('js')

Now open the registration page resources/views/auth/register.blade.php and add a hidden input with guessed timezone as value, I am assuming you have jQuery on the page:

<form class="form-horizontal" method="POST" action="{{ route('register') }}">
    {{ csrf_field() }}
    <input type="hidden" name="tz" id="tz">
    ...
</form>

At the bottom of registration view, we can push this script to populate the input.

...
@endsection

@push('js')
    <script>
        $(function () {
            // guess user timezone 
            $('#tz').val(moment.tz.guess())
        })
    </script>
@endpush

Now visit the registration page and inspect the form element, you should see timezone populated with your timezone:

timezone input

If you don’t want to use front end way of getting users timezone, we have other way is to use some IP to location service to guess users timezone.

Detect users Timezone from php

We will use FreeGeoIp.net to get the location with timezone info from users IP. It will return plenty of details along with timezone:

{  
   "ip":"66.102.0.0",
   "country_code":"US",
   "country_name":"United States",
   "region_code":"CA",
   "region_name":"California",
   "city":"Mountain View",
   "zip_code":"94043",
   "time_zone":"America\/Los_Angeles",
   "latitude":37.4192,
   "longitude":-122.0574,
   "metro_code":807
}

But for our use, we will only need the time_zone field.

Also if you are running your application on localhost you should set a testing IP address, to can get your current IP address by googling my ip and use as test ip.

Add users timezone into Database

Laravel app/Http/Controllers/Auth/RegisterController.php controller gives a registered method which will be called once registration is completed. This is good place to update the user row in database with the timezone value.

protected function registered(Request $request, $user)
{
    // set timezone
    $timezone =  $this->getTimezone($request);
    $user->timezone = $timezone;
    $user->save();
}

protected function getClientIp(): string
{
    $ip = \request()->ip();
    return $ip == '127.0.0.1' ? '66.102.0.0' : $ip;
}

protected function getTimezone(Request $request)
{
    if ($timezone = $request->get('tz')) {
        return $timezone;
    }

    // fetch it from FreeGeoIp
    $ip = $this->getClientIp();

    try {
        $response = json_decode(file_get_contents('http://freegeoip.net/json/' . $ip), true);
        return array_get($response, 'time_zone');
    } catch (\Exception $e) {}
}

Update the users table migration to add timezone field.

$table->string('password');
$table->string('timezone', 60);

Now its time to test, register a user and you should be seeing timezone is set with users record. You should also give option in users profile where user can select a timezone from dropdown, preferably from profile edit screen using DateTimeZone::listIdentifiers(DateTimeZone::ALL) to get the list.

Showing date in users timezone

We have everything setup now we can move on the next step on how to display the date time stored in the database as UTC to users current timezone, it can be done using momentjs or from server side using Carbon library which is used in Laravel, let’s see how we accomplish it in both ways.

Using Moment js

If your laravel app is working as API you will be returning a response as JSON which will be returning all the date mutated fields from Eloquent as UTC. Now to change it in users timezone using moment js is pretty simple:

let localTime = moment.utc("2018-01-22 04:09:31").local();

This will change the UTC time into your browsers local time, there are many momentjs wrapper libraries are present for this, for example, Vue-moment which gives a lot of other filters to work with date time.

Use Carbon on laravel

Another option will be to change the timezone using laravel backend, we have already stored the user’s timezone, now to change any timestamp from UTC to users local timezone you just need to do the following:

// on a date column
$user->created_at->timezone('Asia/Kolkata')->toDateTimeString()

// Directly on a Carbon instance 
Carbon\Carbon::parse('2018-01-22 04:09:31')->timezone('Asia/Kolkata')->toDateTimeString()

Since we will be needing this timezone conversion on most of the eloquent model in laravel app, we should move this logic into a trait:

Trait for Local Date

This trait will give one helper function localize('created_at') on model which will accept the date field name and it will return carbon date instance in users timezone:

namespace App;

use Carbon\Carbon;

trait HasLocalDates {

    /**
     * Localize a date to users timezone 
     * 
     * @param null $dateField
     * @return Carbon
     */
    public function localize($dateField = null)
    {
        $dateValue = is_null($this->{$dateField}) ? Carbon::now() : $this->{$dateField};
        return $this->inUsersTimezone($dateValue);
    }

    /**
     * Change timezone of a carbon date 
     * 
     * @param $dateValue
     * @return Carbon
     */
    private function inUsersTimezone($dateValue): Carbon
    {
        $timezone = optional(auth()->user())->timezone ?? config('app.timezone');
        return $this->asDateTime($dateValue)->timezone($timezone);
    }
}

Now to use this trait in any Eloquent Model just use it like this:

class User extends Authenticatable
{
    use Notifiable, HasLocalDates;
    ...

Formatted Date Trait

In many cases we want a formatted dates returned to us from backend API so we don’t need to do conversion in front end, we can add one more trait which will add formatted dates on all the model fields cast as date into a format you defined in config file or you can give option to the user to choose a format they want to see the date and time in using settings tutorial.

<?php

namespace App;

use Illuminate\Support\Carbon;

trait FormatsDate
{
    /**
     * All the fields other than eloquent model dates array you want to format
     *
     * @var array
     */
    protected $formattedDates = [];


    /**
     * Flag to disable formatting on demand 
     * 
     * @var bool
     */
    protected $noFormat = false;

    /**
     * Prefix which will be added to the fields for formatted date
     *
     * @var string
     */
    protected $formattedFieldPrefix = 'local_';

    /**
     * Override the models toArray to append the formatted dates fields
     *
     * @return array
     */
    public function toArray()
    {
        $data = parent::toArray();

        if( $this->noFormat ) return $data;

        foreach ($this->getFormattedDateFields() as $dateField) {
            $data[$this->formattedFieldPrefix.$dateField] = $this->toDateObject($this->{$dateField});;
        }

        return $data;
    }

    /**
     * Format time part of timestamp
     *
     * @param $dateValue
     * @return string|null
     */
    private function formattedDate($dateValue)
    {
        if( is_null($dateValue) ) return null;

        return $this->inUsersTimezone($dateValue)
            ->format(config('setting.date_format'));
    }

    /**
     * Format date part of timestamp
     *
     * @param $dateValue
     * @return string|null
     */
    private function formattedTime($dateValue)
    {
        if( is_null($dateValue) ) return null;

        return $this->inUsersTimezone($dateValue)
            ->format(config('setting.time_format'));
    }

    /**
     * Format date diff for humans
     *
     * @param $dateValue
     * @return string|null
     */
    private function formattedDiffForHumans($dateValue)
    {
        if( is_null($dateValue) ) return null;

        return $this->inUsersTimezone($dateValue)
            ->diffForHumans();
    }

    /**
     * Built a date object for serialization
     *
     * @param $dateValue
     * @return array
     */
    private function toDateObject($dateValue): array
    {
        return [
            'date' => $this->formattedDate($dateValue),
            'time' => $this->formattedTime($dateValue),
            'for_human' => $this->formattedDiffForHumans($dateValue)
        ];
    }

    /**
     * Return all the fields which needed formatted dates
     *
     * @return mixed
     */
    private function getFormattedDateFields()
    {
        return array_merge($this->formattedDates, $this->getDates());
    }

    /**
     * Setter for formatted dates fields array 
     * 
     * @param array $formattedDates
     */
    public function setFormattedDates(array $formattedDates)
    {
        $this->formattedDates = $formattedDates;
    }

    /**
     * Get the formatted date object for a field
     *
     * @param $field
     * @return array
     */
    public function toLocalTime($field = null )
    {
        $dateValue = is_null($this->{$field}) ? Carbon::now() : $this->{$field};
        return $this->toDateObject($dateValue);
    }

    /**
     * Disable formatting for the dates
     *
     * @return $this
     */
    public function disableFormat()
    {
        $this->noFormat = true;
        return $this;
    }

    /**
     * Enable formatting for the dates
     *
     * @return $this
     */
    public function enableFormat()
    {
        $this->noFormat = false;
        return $this;
    }

    /**
     * Get the timestamp in users timezone
     *
     * @param $dateValue
     * @return Carbon
     */
    private function inUsersTimezone($dateValue): Carbon
    {
        $timezone = optional(auth()->user())->timezone ?? config('app.timezone');
        return $this->asDateTime($dateValue)
            ->timezone($timezone);
    }
}

Now let’s add the format settings in a config file, create configs/setting.php and add followings:

return [
    'date_format' => 'Y m d',
    'time_format' => 'g:i a',
];

To test this out, let’s add FormatsDate on the Comment model.

class Comment extends Model
{
    use FormatsDate;

    protected $guarded = [];
}

Now if you return Comment model from route it will be automatically serialized to JSON and it will have created_at and updated_at in formatted result

formatted date

As you can see, res the lt doesn’t include the approved_at timestamp into formatted fields, to add that we need to cast approved_at field as date, add following in to Comment model and refresh the page:

class Comment extends Model
{
    use FormatsDate;

    protected $dates = ['approved_at'];
    ...

Now it has included approved_at also into formatted fields.

Conclusion

Now you have ways to manage timezone. Which one you choose it depends on your project. If you want to keep backend and frontend separate you should consider FormattedDate trait option or user moment.js to show the time in users timezone from frontend. But as most of Laravel project are mixed of front end Vue.js component and blade views its also very usable to have a handy method on Eloquent Model itself to get the dates in users timezone, for that you can use HasLocalDates trait. I hope you liked this post and let me know in the comments if you need any help or how you have solved this problem with timezone.