如何在时区与UTC不同时存储日期时间(Laravel)

4

我的应用程序时区设置为“美国/蒙特利尔”。
我有两个日期时间字段“开始”和“结束”,每个都使用Laravel $casts属性转换为日期时间:

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

当我使用以下数据创建我的模型实例时:
MyModel::create(
                [
                    'start' => "2022-02-08T20:45:58.000Z", // UTC time  
                    'end' => "2022-02-08T20:45:58.000Z",
                ]
            );

创建的模型保持相同的时间(20:45),但时区设置为美国/蒙特利尔。
 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,
   }

当我访问开始和结束属性时,我得到的时间相同,但是带有美国/蒙特利尔的时区,如下所示:

// 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),

我发现使其正常工作的唯一方法是在保存之前手动设置时区:

    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')),,
                ]
            );  

我认为这种方法有点重复了,仅设置应用程序的时区不就足够了吗?有更好的方法吗?我知道应该将应用程序的时区设置为UTC(通常是我的做法),但是这个项目已经用这个时区存储了大量数据,我不确定如何进行转换。
谢谢。


2
谢谢你的回答。是的,我这样做了,但我需要为每个模型中的每个日期时间属性定义一个,希望有更好的方法来解决这个问题。 - hereForLearing
我明白了,你尝试过在你的模型上使用 protected $dateFormat = 'U'; 吗?它来自于日期转换部分下的同一文档页面,虽然我自己没有尝试过,但似乎是你需要的。 - Abdul Rehman
如果以上方法无法解决问题,我建议使用动态特性,这样就不必手动操作了。以下的SO答案正好可以实现这一点:https://dev59.com/IqHia4cB1Zd3GeqPTW0-#48371850 - Abdul Rehman
1
将其设置为 $casts 中的 datetime 的目的是,您可以传递 DateTimeCarbon 对象,而不是字符串。另外,为什么您说“时区设置为 America/Montreal”,而您明显显示的日期是“2022-02-08 20:45:58.0 +00:00”? - miken32
2
这就是问题所在,它在存储时没有进行时区转换到应用程序时区,然后在检索时将其作为应用程序时区,这显然是一个错误。 - Tofandel
显示剩余4条评论
1个回答

5
Laravel只会在模型中存储日期,使用$date->format('Y-m-d H:i:s')来保留原始的小时/时间信息,但不会保留时区信息。
然后,当它检索日期时,由于它只是一个没有时区信息的字符串,它将被转换为具有应用程序时区(通常是UTC)的Carbon日期。
这会导致不一致,因为从getter获取的值与发送给setter的值不同,如果您的日期与应用程序的时区不同。
简单来说,基本上就是这样发生的。
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');

这是 Laravel 中一个非常有争议的问题,它在模型中对日期的处理并不合理和符合预期。本应该在将日期转换为不带时区信息的字符串之前,先将其转换为应用程序的时区。但这个问题被标记为"预期行为"
为了缓解这个问题,你可以创建自己的模型扩展,重写setAttribute方法,并从这个类继承,以便自动将所有日期转换为你的应用程序时区。
<?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);
    }
}

不要忘记将数据库时区设置为应用程序时区,否则如果您将日期存储在timestamp而不是datetime中,当尝试插入日期时可能会出现错误,因为日期可能会落在夏令时期间。
在您的config/database.php文件中。
    'connections' => [
        'mysql' => [
            //...
            'timezone'  => '+00:00', // Should match app.timezone
            //...


如果你之前没有做过这个,你需要迁移所有的日期,这里有一个专门用于迁移的工具。
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class ConvertAllTimestampsToUtc extends Migration {
    public static function getAllTimestampColumns(): array
    {
        $results = DB::select(
            "SELECT TABLE_NAME, COLUMN_NAME from information_schema.columns WHERE DATA_TYPE LIKE 'timestamp' AND TABLE_SCHEMA LIKE :db_name", [
            'db_name' => DB::getDatabaseName()
        ]);

        return collect($results)->mapToGroups(fn($r) => [$r->TABLE_NAME => $r->COLUMN_NAME])->toArray();
    }

    public static function convertTzOfTableColumns($table, $columns, $from = '+00:00', $to = 'SYSTEM')
    {
        $q = array_map(fn($col) => "`$col` = CONVERT_TZ(`$col`, '$from', '$to')", $columns);
        DB::update("UPDATE `$table` SET " . join(', ', $q));
    }

    /**
     * Run the migrations.
     */
    public function up(): void
    {
        foreach (self::getAllTimestampColumns() as $table => $cols) {
            self::convertTzOfTableColumns($table, $cols);
        }
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        foreach (self::getAllTimestampColumns() as $table => $cols) {
            self::convertTzOfTableColumns($table, $cols, 'SYSTEM', '+00:00');
        }
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接