CarbonImmutable、NoOverflow をデフォルトに

## PHP の日付操作ライブラリ Carbon の問題

プロジェクトのコーディング規約で、私は PHP のdate 関数と strtotime 関数、及び MySql の NOW 関数の使用禁止を第一に謳う。
日付操作のロジックを可視化するには Carbon を使用するべきであり、Sql クエリ内の固定された NOW はユニットテストの障害となるから。

しかし、Carbon を使用さえすれば良いかといえば、そうはいかない。
初期状態の Carbon には、プログラムコードにバグを潜らせる問題点が２つある。

1. __Carbon オブジェクトは値が変動する__
`$today = Carbon::now(); $tomorrow = $today->addDay();` のようなコードは書きがちであるが、もとの変数も変動してしまう。これを避けるには `CarbonImmutable` を意識して宣言する必要がある。
2. __１月末の１ヶ月後が２月末にならない__
これを回避するために `addMonth` ではなく `addMonthNoOverflow` を使用しろと言うのは無理がありすぎる。

ということで、__Laravel で Carbon を使用するに当たり、CarbonImmutable と NoOverflow をデフォルトに__ 設定する。

必要な設定は次の３項目だ。

- src/Illuminate/Support/Carbon.php を複写して app/Helpers/Carbon.php を作成し、継承元を Carbon\CarbonImutable に差し替える。
- config/app.php に App\Helpers\Carbon のエイリアスを追加する。
- app/Providers/AppServiceProvider.php で、noOverflow を初期値設定と now ヘルパーの使用クラス設定を追加する。

## Laravel の初期状態を確認

now() ヘルパーは、`Carbon\Carbon` ではなく `Illuminate\Support\Carbon`を参照している。
ただし、これは `Carbon\Carbon` のラッパーでしかない。

```php
$ php artisan tinker
Psy Shell v0.11.2 (PHP 8.0.15 — cli) by Justin Hileman
>>> now()
=> Illuminate\Support\Carbon @1652861045 {#4296
     date: 2022-05-18 17:04:05.830169 Asia/Tokyo (+09:00),
   }
```
１月末から１ヶ月後は３月に繰り越す。
そして、もとの変数 `$now` まで変化してしまう。
```php
>>> Illuminate\Support\Carbon::setTestNow('2020-01-31')
=> null
>>> $now = now()
=> Illuminate\Support\Carbon @1580396400 {#4300
     date: 2020-01-31 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> $next = $now->addMonth()
=> Illuminate\Support\Carbon @1583074800 {#4300
     date: 2020-03-02 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> $now
=> Illuminate\Support\Carbon @1583074800 {#4300
     date: 2020-03-02 00:00:00.0 Asia/Tokyo (+09:00),
   }
```

## app/Helpers/Carbon.php の作成

`src/Illuminate/Support/Carbon.php` を複写して `app/Helpers/Carbon.php` を作成する。
継承元を Carbon から CarbonImmutable に変えただけだ。


```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);
    }
}

```

Carbon を使用するときの use 宣言は次のようにすることになる。
これで、もとの変数は変化しない `CarbonImmutable` がデフォルトとなる。


```php
// use Carbon\Carbon;              // 一般的な use 宣言
// use Illuminate\Support\Carbon;  // Laravel における use 宣言
use App\Helpers\Carbon;
```

config でエイリアスを指定することで、use 宣言なし（代わりに `\Carbon` 指定）で呼び出すことも可能。

__config/app.php__

```php
    'aliases' => [
        ~~ （省略）~~

        'Carbon' => App\Helpers\Carbon::class,
    ],
```

## now(), today() ヘルパー

大抵は、use 宣言をせずに使用できる `now()` ヘルパーを常用するだろう。
ヘルパーが参照するクラスが `App\Helper\Carbon` となるように、`AppServiceProvider` で定義することができる。
ついでに、ここで `noOverflow` を初期値に設定する。

__app/Providers/AppServiceProvider.php__

```php
<?php

namespace App\Providers;

use App\Helper\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // noOverflowを初期値に設定
        Carbon::useMonthsOverflow(false);
        Carbon::useYearsOverflow(false);
        // now(), today() の使用クラスを変更
        Date::use(Carbon::class);
    }
```

## 変更後の確認

`now()` 関数の参照クラスは `App\Helpers\Carbon` となった。

```php
$ php artisan tinker
Psy Shell v0.11.2 (PHP 8.0.15 — cli) by Justin Hileman
>>> now()
=> App\Helpers\Carbon @1652861045 {#4296
     date: 2022-05-18 17:04:05.830169 Asia/Tokyo (+09:00),
   }
```
１月末の１ヶ月後は２月末であり、もとの変数 `$now` は維持されるようになった。
```php
>>> Carbon::setTestNow('2020-01-31')
=> null
>>> $now = now()
=> App\Helpers\Carbon @1580396400 {#4300
     date: 2020-01-31 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> $next = $now->addMonth()
=> App\Helpers\Carbon @1583074800 {#4300
     date: 2020-02-28 00:00:00.0 Asia/Tokyo (+09:00),
   }
>>> $now
=> App\Helpers\Carbon @1583074800 {#4300
     date: 2020-01-31 00:00:00.0 Asia/Tokyo (+09:00),
   }
```

Eloquent の日時オブジェクトも `App\Helpers\Carbon` に変更になっている。

```php
>>> User::find(1)->created_at
[!] Aliasing 'User' to 'App\Models\Eloquent\User' for this Tinker session.
=> App\Helpers\Carbon @1652765323 {#4320
     date: 2022-05-17 14:28:43.0 Asia/Tokyo (+09:00),
   }
```