I've been reading up about the best approach to handling localised times, when a Laravel application is used across multiple timezones.
My understanding is that the app timezone should remain set as the default, which is UTC.
This means that all datetime / timestamps are recorded in the database (MySQL in my case) as their UTC value - in other words, consistently.
For Eloquent models to have the correct (localised) date / time values, the user's timezone must be obeyed. It is at this point that I am less clear on how to proceed - specifically, in terms of:
Edit I should mention that my app supports both anonymous and authenticated users, so I don't want to force the user to explicitly select their timezone.
I ended up implementing this with my own model trait, primarily because I needed to implement this in a transparent way.
Firstly, I created my own getAttribute()
method, to retrieve the stored values (stored as the app's default timezone - likely UTC) and then apply the current timezone.
The trait also alters the model's create()
and update()
methods, to support fields in a model's dates
property being stored as the app's timezone, when they've been set by the user in the current active timezone.
The self::getLocale()
static method in the trait in my case is provided by another trait in my app, although this logic can be adjusted to suit your own app.
trait LocalTime
{
/**
* Override create() to save user supplied dates as app timezone
*
* @param array $attributes
* @param bool|mixed $allow_empty_translations
*/
public static function create(array $attributes = [], $allow_empty_translations=false)
{
// get empty model so we can access properties (like table name and fillable fields) that really should be static!
// https://github.com/laravel/framework/issues/1436
$emptyModel = new static;
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $emptyModel->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// https://github.com/laravel/framework/issues/17876#issuecomment-279026028
$model = static::query()->create($attributes);
return $model;
}
/**
* Override update(), to save user supplied dates as app timezone
*
* @param array $attributes
* @param array $options
*/
public function update(array $attributes = [], array $options = [])
{
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $this->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// update model
return parent::update($attributes, $options);
}
/**
* Override getAttribute() to get times in local time
*
* @param mixed $key
*/
public function getAttribute($key)
{
$attribute = parent::getAttribute($key);
// we apply current timezone to any timestamp / datetime columns (these are Carbon objects)
if ($attribute instanceof Carbon) {
$attribute->tz(self::getLocale()->timezone);
}
return $attribute;
}
}
I'd be interested in feedback of the above approach.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With