Carbon::parse で不正な日時も取得する

## 24時超えの終業時刻ってｗ

担当した勤怠管理プロジェクトにて、24時超えの終業時刻（どころか始業すら）が当たり前のようにあった。
フォームからの入力値を正しい日時オブジェクトに変換するためにも Carbon::parse を用いたかったが、残念ながらエラーで取得できない。

24時超えや31日超えの不正な日時でも取得可能なのは、Carbon::create だけのようだ。

ということで、Carbon::parse を拡張してみた。

## Carbon::parse の拡張

<a href="https://qiita.com/mikomey/items/ac6cbeb989deb3a8b5b7">前の記事</a>で `App\Helpers\Carbon` を作成しているので、そこにメソッドを加える。


__app/Helpers/Carbon.php__
```php
<?php

namespace App\Helpers;

use Carbon\Carbon as BaseCarbon;
use Carbon\CarbonImmutable as BaseCarbonImmutable;

class Carbon extends BaseCarbonImmutable
{
    /**
     * {@inheritdoc}
     */
    public static function setTestNow($testNow = null)
    {
        BaseCarbon::setTestNow($testNow);
        BaseCarbonImmutable::setTestNow($testNow);
    }

    /**
     * Create a carbon instance from a string.
     *
     * @param string|DateTimeInterface|null $time
     * @return static
     */
    public static function parse($time = null, $tz = null)
    {
        try {
            // '-1 month' などが扱えるよう、まずは parse してみる
            return parent::parse($time, $tz);
        } catch (\Exception $e) {
            // 24時超えや31日超えでエラーとなった場合
            if (preg_match('/^(\\d+?)[\\/-](\\d+?)[\\/-](\\d+) (\\d+):(\\d+):?(\\d*)$/', $time, $m)) {
                // 2021-10-10 10:10:00 または 2021/10/10 10:10:00
                return parent::create((int) $m[1], (int) $m[2], (int) $m[3], (int) $m[4], (int) $m[5], $m[6] ?? 0, $tz);
            } elseif (preg_match('/^(\\d+?)[\\/-](\\d+?)[\\/-](\\d+)$/', $time, $m)) {
                // 2021-10-10 または 2021/10/10
                return parent::create((int) $m[1], (int) $m[2], (int) $m[3], 0, 0, 0, $tz);
            } elseif (preg_match('/^(\\d+):(\\d+):?(\\d*)$/', $time, $m)) {
                // 10:10:00
                $now = now();
                return parent::create($now->year, $now->month, $now->day, (int) $m[1], (int) $m[2], (int) $m[3] ?? 0, $tz);
            }
        }
        return null;
    }
}
```

## 動作確認

比較のため strtotime の結果も記す。
オリジナルカーボンである Carbon\Carbon と、今回置き換えた App\Helpers\Carbon との結果の違いを見てほしい。

「2月30日」はすべて３月に繰り越せる。

```php
>>> date('Y-m-d', strtotime('2022-02-30'))
=> "2022-03-02"
>>> Carbon\Carbon::parse('2022-02-30')
=> Carbon\Carbon @1646146800 {#4429
     date: 2022-03-02 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> Carbon::parse('2022-02-30')
=> App\Helpers\Carbon @1646146800 {#4416
     date: 2022-03-02 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> 
```

31日超えがエラーにならないのは App\Helpers\Carbon だけ。

```php
>>> date('Y-m-d', strtotime('2022-02-32'))
=> "1970-01-01"
>>> Carbon\Carbon::parse('2022-02-32')
Carbon\Exceptions\InvalidFormatException with message 'Could not parse '2022-02-32': DateTime::__construct(): Failed to parse time string (2022-02-32) at position 9 (2): Unexpected character'
>>> Carbon::parse('2022-02-32')
=> App\Helpers\Carbon @1646319600 {#4323
     date: 2022-03-04 00:00:00.0 Asia/Tokyo (+09:00),
   }
```

時刻として「10:10」はすべて読み取れる。日付部分は「今日」が基準となる。

```php
>>> date('Y-m-d H:i:s', strtotime('10:10'))
=> "2022-05-19 10:10:00"
>>> Carbon\Carbon::parse('10:10')
=> Carbon\Carbon @1652922600 {#4440
     date: 2022-05-19 10:10:00.0 Asia/Tokyo (+09:00),
   }
>>> Carbon::parse('10:10')
=> App\Helpers\Carbon @1652922600 {#4323
     date: 2022-05-19 10:10:00.0 Asia/Tokyo (+09:00),
   }
```

24時超えの時刻が読み取れるのは App\Helpers\Carbon だけ。

```php
>>> date('Y-m-d H:i:s', strtotime('25:10'))
=> "1970-01-01 09:00:00"
>>> Carbon\Carbon::parse('25:10')
Carbon\Exceptions\InvalidFormatException with message 'Could not parse '25:10': DateTime::__construct(): Failed to parse time string (25:10) at position 0 (2): Unexpected character'
>>> Carbon::parse('25:10')
=> App\Helpers\Carbon @1652976600 {#4290
     date: 2022-05-20 01:10:00.0 Asia/Tokyo (+09:00),
   }
```

Eloquent の日時はすでに Carbon だが、再パースしてもOK。

```php
>>> $date = User::find(1)->created_at
=> App\Helpers\Carbon @1652765323 {#4319
     date: 2022-05-17 14:28:43.0 Asia/Tokyo (+09:00),
   }
>>> Carbon::parse($date)
=> App\Helpers\Carbon @1652765323 {#4407
     date: 2022-05-17 14:28:43.0 Asia/Tokyo (+09:00),
   }
```