How to store datetime when timezone is different from UTC (Laravel)

Viewed 1047

My app timezone is set to 'America/Montreal'.
I have a two datetime fields 'start' and 'end', each casted to datetime using laravel $casts property:

protected $casts = [
    'start' => 'datetime',
    'end' => 'datetime'
];

When I create an instance of my model with the following data:

MyModel::create(
                [
                    'start' => "2022-02-08T20:45:58.000Z", // UTC time  
                    'end' => "2022-02-08T20:45:58.000Z",
                ]
            );

The created model keeps the same time (20:45) but the timezone is set to America/Montreal:

 App\MyModel {#4799
     id: 44,
     created_at: "2022-02-08 15:49:02",
     updated_at: "2022-02-08 15:49:02",
     start: 2022-02-08 20:45:58,
     end: 2022-02-08 20:45:58,
   }

when I access the start and end attributes I get the same time but with America/Montreal timezone like this:

// accessing 'start' attribute of the instance I just created
Illuminate\Support\Carbon @1644371158 {#4708
 date: 2022-02-08 20:45:58.0 America/Montreal (-05:00),

}

The only way I found to get it to work correctly is to manually setTimezone before saving:

    MyModel::create(
                [
                    'start' => Carbon::parse("2022-02-08T20:45:58.000Z")->setTimeZone(config('app.timezone')),, 
                    'end' => Carbon::parse("2022-02-08T20:45:58.000Z")->setTimeZone(config('app.timezone')),,
                ]
            );  

which is, I think, repetitive, isn't setting the app timezone enough ? Is there any better way to do this ? I know that I'm supposed to set my app timezone to UTC (which is what I do usually) but this projet has already a lot of data with this timezone and I'm not sure how to convert it.
Thank you.

1 Answers

Given that laravel will just store the date in the model doing $date->format('Y-m-d H:i:s') which formats it in it's current timezone, but without preserving timezone, and then retrieves it as if it was in the app timezone (most of the time utc), this creates a disparity because you do not get the same value from the getter than the one you put in, if the one you set had a different timezone than the app

This is basically what happens

Carbon\Carbon::parse(Carbon\Carbon::parse('2022-11-08 00:00', 'America/Montreal')->format('Y-m-d H:i:s'), 'UTC');

Carbon\Carbon @1667865600 {#4115
   date: 2022-11-08 00:00:00.0 UTC (+00:00), // As you can see it is UTC,
  // which is ok because the database does not store the timezone information,
  // but the time is 2022-11-08 00:00 and should be 2022-11-08 05:00:00 in UTC
}

// This would yield the correct result
Carbon\Carbon::parse(Carbon\Carbon::parse('2022-11-08 00:00', 'America/Montreal')->setTimezone('UTC')->format('Y-m-d H:i:s'), 'UTC');

You can make your own model extension overriding the setAttribute method and extend from this class instead to convert all the dates automatically to your app timezone

<?php

namespace App;

use DateTimeInterface;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model as BaseModel;

class Model extends BaseModel
{

    /**
     * Set a given attribute on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    public function setAttribute($key, $value)
    {
        if ($value instanceof CarbonInterface) {
            // Convert all carbon dates to app timezone
            $value = $value->clone()->setTimezone(config('app.timezone'));
        } else if ($value instanceof DateTimeInterface) {
            // Convert all other dates to timestamps
            $value = $value->unix();
        }
        // They will be reconverted to a Carbon instance but with the correct timezone
        return parent::setAttribute($key, $value);
    }
}
Related