Laravel 資料庫時區問題分享
The snippet can be accessed without any authentication.
Authored by
Shih-Tse Chang
簡介:儲存 ISO 8601 字串表示的時間會遺失時區資訊。
開發 Salary 時發現一個神奇的現象,跟大家分享。
案例
-
config('app.timezone')
是Asia/Taipei
(UTC+8) - 有一個 Post model
- 其中有個 Datetime 欄位
published_at
- 在 model 裡有設定
protected $casts = ['published_at' => 'datetime'];
- 其中有個 Datetime 欄位
如果前端要做新增 Post 的動作:
$post = new Post();
$post->published_at = '2023-03-30T22:00:00.000Z'; // ISO 8601 UTC+0
$post->save();
- 原本想儲存的
-
2023-03-30T22:00:00.000Z
==2023-03-30 22:00:00 (UTC+0)
==2023-03-31 06:00:00 (UTC+8)
-
- 資料庫裡儲存的
published_at
資料將會是2023-03-30 22:00:00
- PHP 轉成 JSON 噴給前端的
published_at
則會是-
2023-03-30T16:00:00.000Z
。
-
真神奇
分析
首先,在 CC 的使用上,資料庫是不儲存時區的,在資料庫儲存的時間的時區都是跟著 config('app.timezone')
,所以都是 UTC+8 (admission 系統除外)。
當我們在塞資料進資料庫的時候,Laravel 會直接將提供的字串轉為 Carbon 物件,然後按照Y-m-d H:i:s
的格式儲存進資料庫。因此時區資訊直接被捨去了、也未對時區做轉換。
從資料庫拿取資料的時候 Laravel 又很聰明的認為資料庫儲存的 Datetime 會是 config('app.timezone')
的時區。
解法
寫了一個 custom cast: LocalDatetime。
當開發者 assign attribute value 時,會解析 value 為 Carbon 或 CarbonImmutable 物件,並轉換時區到 config('app.timezone')
的時區。
在資料庫儲存時直接捨去時區後剩下的時間將會是 config('app.timezone')
的時區時間,且 serialize to JSON 時仍會是 ISO8601 Z 結尾的字串(UTC+0)。
參考資料
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Date;
class LocalDatetime implements CastsAttributes
{
/**
* @param bool $time Indicate whether the value is with time
* @param bool $immutable Indicate whether to use immutable instance
*/
public function __construct(public bool $time = true, public bool $immutable = false)
{
}
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function get($model, string $key, $value, array $attributes): mixed
{
return $this->cast($value);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function set($model, string $key, $value, array $attributes)
{
return $this->cast($value);
}
protected function cast($value)
{
if (is_string($value)) {
$value = Date::parse($value);
}
$value = Date::instance($value)->setTimezone(config('app.timezone'));
if (! $this->time) {
$value = $value->startOfDay();
}
if ($this->immutable) {
$value = $value->toImmutable();
}
return $value;
}
}
Please register or sign in to comment