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

问题描述 投票:0回答:1

我的应用程序时区设置为“美国/蒙特利尔”。
我有两个日期时间字段“开始”和“结束”,每个字段都使用 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(这是我通常所做的),但是这个项目已经有很多这个时区的数据,我不知道如何转换它。
谢谢你。

laravel php-carbon
1个回答
4
投票

Laravel 只会将日期存储在执行

$date->format('Y-m-d H:i:s')
的模型中,该模型仅使用日期的原始小时/时间,但不保留时区信息。

然后,当它检索它时,因为它只是一个没有时区信息的字符串,所以它会将其转换为带有应用程序时区(通常是 UTC)的碳日期

这会产生差异,因为如果您的日期与应用程序的时区不同,您从 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->getTimestamp();
        }
        // 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');
        }
    }
};
© www.soinside.com 2019 - 2024. All rights reserved.