🍣

Laravelでタイムゾーン付きISO8601のdatetime値がおかしい

2022/10/26に公開

前提条件

config/app.phptimezoneAsia/Tokyo になっている

期待動作

ISO8601フォーマットをタイムゾーン情報持ちながらパースしてくれる

>>> (new Carbon\Carbon('2022-10-18T15:00:00.000000Z'))
=> Carbon\Carbon @1666105200 {#0000
     date: 2022-10-18 15:00:00.0 +00:00,
   }

>>> (new Carbon\Carbon('2022-10-18T15:00:00.000000Z'))->tz
=> Carbon\CarbonTimeZone {#0000
     timezone: +00:00,
   }

Modelでの挙動

ISO8601のタイムゾーンが無視される

>>> $class = new class() extends \Illuminate\Database\Eloquent\Model { protected $guarded = []; protected $casts = ['expired_at' => 'datetime']; }
=> Illuminate\Database\Eloquent\Model@anonymous {#0000}

>>> (new $class(['expired_at' => '2022-10-18T15:00:00.000000Z']))->expired_at
=> Illuminate\Support\Carbon @1666072800 {#0000
     date: 2022-10-18 15:00:00.0 Asia/Tokyo (+09:00),
   }

>>> (new $class(['expired_at' => '2022-10-18T15:00:00.000000Z']))->expired_at->tz
=> Carbon\CarbonTimeZone {#0000
     timezone: Asia/Tokyo (+09:00),
   }

他の方法でもダメ

>>> $class->expired_at = \Illuminate\Support\Facades\Date::parse('2022-10-18T15:00:00.000000Z')
=> Illuminate\Support\Carbon @1666105200 {#000
     date: 2022-10-18 15:00:00.0 +00:00,
   }

>>> $class->expired_at
=> Illuminate\Support\Carbon @1666072800 {#0000
     date: 2022-10-18 15:00:00.0 Asia/Tokyo (+09:00),
   }

実際なにをやっているか

最終的には Illuminate\Database\Eloquent\Concerns\HasAttributes::fromDateTime

    /**
     * Convert a DateTime to a storable string.
     *
     * @param  mixed  $value
     * @return string|null
     */
    public function fromDateTime($value)
    {
        return empty($value) ? $value : $this->asDateTime($value)->format(
            $this->getDateFormat()
        );
    }

asDateTimeでタイムゾーンの情報を持ったCarbonインスタンスを生成したのにformatでタイムゾーン落ちした文字列にしてしまっていたという話でした

>>> \Illuminate\Support\Facades\Date::parse('2022-10-18T15:00:00.000000Z')->format('Y-m-d H:i:s')
=> "2022-10-18 15:00:00"

対応

タイムゾーンが存在する場合はtzメソッドを通すことで意図した値になります

>>> \Illuminate\Support\Facades\Date::parse('2022-10-18T15:00:00.000000Z')->tz(config('app.timezone'))->format('Y-m-d H:i:s')
=> "2022-10-19 00:00:00"

Dateインスタンスを渡してもタイムゾーン落ちしてしまうので、自前で先に変換することにしました

>>> $class = new class() extends \Illuminate\Database\Eloquent\Model { protected $guarded = []; protected $casts = ['expired_at' => 'datetime']; }
=> Illuminate\Database\Eloquent\Model@anonymous {#0000}

>>> $class->expired_at = \Illuminate\Support\Facades\Date::parse('2022-10-18T15:00:00.000000Z')->tz(config('app.timezone'))->format('Y-m-d H:i:s')
=> "2022-10-19 00:00:00"

>>> $class->expired_at
=> Illuminate\Support\Carbon @1666105200 {#0000
     date: 2022-10-19 00:00:00.0 Asia/Tokyo (+09:00),
   }

Discussion