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

her*_*ing 4 php laravel php-carbon

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

protected $casts = [
    'start' => 'datetime',
    'end' => 'datetime'
];
Run Code Online (Sandbox Code Playgroud)

当我使用以下数据创建模型实例时:

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

创建的模型保持相同的时间(20:45),但时区设置为 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,
   }
Run Code Online (Sandbox Code Playgroud)

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

// 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),
Run Code Online (Sandbox Code Playgroud)

}

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

    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')),,
                ]
            );  
Run Code Online (Sandbox Code Playgroud)

我认为这是重复的,设置应用程序时区还不够吗?有没有更好的方法来做到这一点?我知道我应该将我的应用程序时区设置为 UTC(这是我通常所做的),但是这个项目已经有很多这个时区的数据,我不知道如何转换它。
谢谢。

Tof*_*del 5

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');
Run Code Online (Sandbox Code Playgroud)

这是 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

另外,不要忘记将数据库时区设置为应用程序时区,否则如果您存储日期timestamp而不是datetime在尝试插入日期时可能会出现错误,因为该日期可能属于夏令时

在你的config/database.php

    'connections' => [
        'mysql' => [
            //...
            'timezone'  => '+00:00', // Should match app.timezone
            //...

Run Code Online (Sandbox Code Playgroud)

如果您之前没有这样做,您将需要迁移所有日期,这是一个可以做到这一点的迁移

<?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');
        }
    }
};
Run Code Online (Sandbox Code Playgroud)