はじめに
Laravel(Carbon)で、検索フォームなどで、2025-02のような「年月」形式の文字列を受け取り、その月の範囲(月初から月末まで)をテーブルなどから取得するケースがある。
これまでは、「日付オーバーフロー問題(いわゆる31日問題)」の回避策として、文字列操作を用いた実装を行っていた。しかし、フォーマット指定子を工夫するだけでよりスマートに記述できることを最近知ったので紹介する。
従来の方法
年月指定の文字列からCarbonインスタンスを生成する場合、以下のように文字列連結で明示的に「1日」を付与してパースしていた。
$date = '2025-02'; // 入力値
// 文字列結合で '-01' を付与して Y-m-d 形式にする
$start = Carbon::createFromFormat('Y-m-d', $date . '-01')->startOfMonth();
$end = Carbon::createFromFormat('Y-m-d', $date . '-01')->endOfMonth();
なぜこうしていたか
簡単に説明すると、
現在日が「1月31日」の場合、内部的に2025-02-31が生成され、2月には31日がないためオーバーフローして2025-03-03になってしまう。
詳細は31日問題とかで調べてください
スマートな方法
フォーマット文字列の先頭に!を付与することで、文字列連結を行わずにこの問題を解決できる。
$date = '2025-02';
// ! を付けることでパース可能
$start = Carbon::createFromFormat('!Y-m', $date);
$end = Carbon::createFromFormat('!Y-m', $date)->endOfMonth();
これだけで、現在日に依存せず2025-02-01 00:00:00が取得できる。
仕組み
実際は、これはLaravelやCarbon独自の機能ではなく、PHPのDateTimeクラスの仕様である。
全てのフィールドは現在の日付/時刻で初期化されます。 ほとんどの場合、これらの値を "ゼロ" (Unix タイムのエポック 1970-01-01 00:00:00 UTC) でリセットしたいはずです。 これは、format の最初の文字に ! を入れるか、 最後の文字に | を入れることで実現できます
DateTimeImmutable::createFromFormat
今回は!で説明したが、|でも似た挙動になる。
ただし、リセットされるタイミングが異なるため注意が必要。
!の定義
すべてのフィールド (年、月、日、時、分、秒、マイクロ秒およびタイムゾーン情報) を ゼロ相当の値 (時、分、秒、マイクロ秒については 0。 月、日については 1。 年については 1970。 タイムゾーンについては UTC) にリセットします。
|の定義
まだパースされていないすべてのフィールド (年、月、日、時、分、秒、マイクロ秒およびタイムゾーン情報) を ゼロ相当の値にリセットします。