Laravel／PHPの日付で起こり得るバグ4択！何個解けますか？

## あるある！日付計算がうまく行かないバグ

さっそくですが、このコードの中のバグが分かりますか？
```
public function searchBookingsInWeek(CarbonInterface $start)
{
    return BookingModel::query()
        ->whereBetween('date', [$start->startOfWeek(), $start->endOfWeek()]);
}
```
### 答え

**※1問目だけはネタバレ対策させていただきます。**
次からはスクロールの調整でご協力お願いします。バグ解決を検索している方になるべく不便なく届くため次から答えを隠しません。


<details>
<summary>クリックで正解を表示</summary>
正解は、Carbonのオブジェクトはミュータブルなせいで探してる期間が実質<code>$start->endOfWeek()</code>から<code>$start->endOfWeek()</code>になっちゃいます。
    
修正としては簡単です。

    public function searchBookingsInWeek(CarbonInterface $start)
    {
        return BookingModel::query()
            ->whereBetween('date', [$start->startOfWeek(), (clone $start)->endOfWeek()]);
    }

Carbonで多くの日付計算をしている時は毎回<code>clone</code>を使っておけば大丈夫かと思います。
</details>

これはおそらく一般的にCarbon使用に置いて一番目に出会うバグ種です。この調子でどんどん他のPHPの日付バグを紹介してまいります。日付バグで困っていない方も謎解き感覚でご覧になっていただければと思います。

## たまに起きるaddMonth()がズレるバグ
以下のコードには特定な時にしか現れないバグあります。どの時か分かりますか？

```
public function extendSubscription(CustomerModel $customer)
{
    return $customer->update([
        'subscription_expires_at' => now()->addMonths($customer->plan->months)->endOfMonth()
    ]);
}
```
### 答え
正解は、実行時が29日以降で、本来のプランの期限月がそれ以下の日数がある時です。

Carbonの`addMonth()`の処理としては、月の値を足しているだけで日の値を変更していないので、例えば`2024-01-31`日に1ヶ月を足すとまず`2024-02-31`になります。こういう時、**PHP日付はオーバーフローしちゃう**ので`2024-02-31`が`2024-03-02`になって最終的に顧客さんが1ヶ月無料延長もらっちゃうことになります。

修正版
```
public function extendSubscription(CustomerModel $customer)
{
    return $customer->update([
        'subscription_expires_at' => now()->startOfMonth()->addMonths($customer->plan->months)->endOfMonth()
    ]);
}
```
`addMonths()`を使う前に毎回`startOfMonth()`使って1日に変更しておけば大丈夫です。

## ハイフン抜きの日付パースが正しくないバグ
以下のコードの`parseDate`を実行して、何の結果になると思いますか？
```
public function parseDate()
{
    return Carbon::createFromFormat('Ynj', '2024912');
}
```

### 答え
正解は、2031-07-02です！2024年のつもりで入力したものは7年後になっちゃいます。
原因は、月のフォーマットを`n`で指定して「1~12」の値だけ切り取られると思ったら、必ず91月だと認知されます。そこからお馴染みのオーバーフロー処理が働いて7年7ヶ月足さられてこの不思議な結果になります。

修正版
```
public function getDateParsedWithoutSeparator()
{
    return $this->parseDateWithoutSeparator('2024912');
}

public function parseDateWithoutSeparator(string $dateStr)
{
    $year = (int) mb_substr($dateStr, 0, 4);
    $monthDay = mb_substr($dateStr, 4);
    $monthDigits = (mb_strlen($monthDay) === 2 || (int) mb_substr($monthDay, 0, 2) > 12) ? 1 : 2;
    $month = (int) mb_substr($monthDay, 0, $monthDigits);
    $day = (int) mb_substr($monthDay, $monthDigits);

    return Carbon::create($year, $month, $day);
}
```

## たまに起きるMySQL検索バグ
さて、最終問題になりますが以下のコードのバグはどの時に起きますか？
```
public function searchBookingsInMonth(int $month, int $year)
{
    return BookingModel::query()
        ->whereBetween('date', ["$year-$month-01", "$year-$month-31"])
        ->get();
}
```

### 答え

正解は、**31日がない月でMySQLバージョンが8.0+** の時です。
正確に言うとMySQL5.7から8.0にマイグレーションを行った時に存在しない日付を使っちゃってたけど結果をちゃんと返しているコードが何も返さなくなった現象あります。MySQL8.0＋だけかどうかは正直検証しておりません。

修正版
```
public function searchBookingsInMonth(int $month, int $year)
{
    return BookingModel::query()
        ->whereBetween('date', [
            Carbon::create($year, $month)->startOfMonth(),
            Carbon::create($year, $month)->endOfMonth()
        ])
        ->get();
}
```

## 最後に
以上今まで出会ったLaravel日付バグです！他のLaravel日付バグご経験ありましたらぜひご共有をお楽しみにしています！


## 一緒に働く仲間を募集しています！
株式会社コネクター・ジャパンでは一緒に働いてくれる仲間を募集しています！

事業拡大に伴い、エンジニアを大募集しています。
興味のある方は下記リンクから弊社のことをぜひ知っていただき応募してもらえると嬉しいです。
▼会社について
https://www.wantedly.com/companies/cnctor/about
▼代表メッセージ
https://cnctor.jp/10years-anniversary/
▼応募はこちら
https://www.wantedly.com/companies/cnctor/projects