Display all dates on models in the user’s timezone (Laravel)
Asked Answered
J

1

7

I have the users timezone stored (there is timezone column in the users DB table) and I want to display all $dates attributes on all models in the user’s timezone, if authenticated.

I'm trying to find an elegant way to do this... Ideally, when there is something like this in Blade views:

{{ $post->created_at }}

OR

{{ $post->created_at->format('h:i:s A') }}

... for authenticated users it would be automatically in their timezones.

How would you handle this?


I'm thinking about creating one trait (for example, app/Traits/UserTimezoneAware.php) and place there accessors that will simply return Carbon::createFromFormat('Y-m-d H:i:s', $value)->timezone(auth()->user()->timezone) if the current user is authenticated. For example:

<?php

namespace App\Traits;

use Carbon\Carbon;

trait UserTimezoneAware
{
    /**
     * Get the created_at in the user's timezone.
     *
     * @param $value
     * @return mixed
     */
    public function getCreatedAtAttribute($value)
    {
        if (auth()->check()) {
            return Carbon::createFromFormat('Y-m-d H:i:s', $value)->timezone(auth()->user()->timezone);
        }

        return Carbon::createFromFormat('Y-m-d H:i:s', $value);
    }

    /**
     * Get the updated_at in the user's timezone.
     *
     * @param $value
     * @return mixed
     */
    public function getUpdatedAtAttribute($value) { ... }
}

But I'm not sure if this is good or bad to do (to create these accessros for the Laravel's $datesattributes)?

Also, models will have different attributes specified in $dates array: for example, User model can have:

/**
 * The attributes that should be mutated to dates.
 *
 * @var array
 */
protected $dates = [
    'created_at',
    'updated_at',
    'last_login_at'
];

and Post model can have:

protected $dates = [
    'created_at',
    'updated_at',
    'approved_at',
    'deleted_at'
];

Is it possible to dynamically create accessors in trait, based on the attirbutes specified in the $dates array of a model that uses that trait?

Or maybe there is a better way to handle this, without accessors?

Jolin answered 8/6, 2018 at 21:16 Comment(0)
J
6

One way (without accessors) is to use this trait:

<?php

namespace App\Traits;

use DateTimeInterface;
use Illuminate\Support\Carbon;

trait UserTimezoneAware
{
    /**
     * Return a timestamp as DateTime object.
     *
     * @param  mixed  $value
     * @return \Illuminate\Support\Carbon
     */
    protected function asDateTime($value)
    {
        $timezone = auth()->check() ? auth()->user()->timezone : config('app.timezone');

        if ($value instanceof Carbon) {
            return $value->timezone($timezone);
        }

        if ($value instanceof DateTimeInterface) {
            return new Carbon(
                $value->format('Y-m-d H:i:s.u'), $timezone
            );
        }

        if (is_numeric($value)) {
            return Carbon::createFromTimestamp($value)->timezone($timezone);
        }

        if ($this->isStandardDateFormat($value)) {
            return Carbon::createFromFormat('Y-m-d', $value)->startOfDay()->timezone($timezone);
        }

        return Carbon::createFromFormat(
            str_replace('.v', '.u', $this->getDateFormat()), $value
        )->timezone($timezone);
    }
}

When using this trait, we are overriding asDateTime($value) defined in Concerns\HasAttributes trait (which is used in Illuminate\Database\Eloquent\Model).

This seems to work OK, I have not yet encountered any problems.

But I'm not sure if there are any risks or potential problems when doing this (when using this trait that overrides asDateTime method).

Jolin answered 8/6, 2018 at 23:31 Comment(1)
Tested this just now on Laravel 11, it changes the values if creating model, so it's storing the timezone in the database instead of the UTC I store for everything, probably a simple fix for this, having a look myself now. Thanks for sharing.Unquiet

© 2022 - 2024 — McMap. All rights reserved.