PHP の日付操作ライブラリ Carbon の問題
プロジェクトのコーディング規約で、私は PHP のdate 関数と strtotime 関数、及び MySql の NOW 関数の使用禁止を第一に謳う。
日付操作のロジックを可視化するには Carbon を使用するべきであり、Sql クエリ内の固定された NOW はユニットテストの障害となるから。
しかし、Carbon を使用さえすれば良いかといえば、そうはいかない。
初期状態の Carbon には、プログラムコードにバグを潜らせる問題点が2つある。
-
Carbon オブジェクトは値が変動する
$today = Carbon::now(); $tomorrow = $today->addDay();
のようなコードは書きがちであるが、もとの変数も変動してしまう。これを避けるにはCarbonImmutable
を意識して宣言する必要がある。 -
1月末の1ヶ月後が2月末にならない
これを回避するためにaddMonth
ではなくaddMonthNoOverflow
を使用しろと言うのは無理がありすぎる。
ということで、Laravel で Carbon を使用するに当たり、CarbonImmutable と NoOverflow をデフォルトに 設定する。
必要な設定は次の3項目だ。
- 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 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),
}
1月末から1ヶ月後は3月に繰り越す。
そして、もとの変数 $now
まで変化してしまう。
>>> 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
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
がデフォルトとなる。
// use Carbon\Carbon; // 一般的な use 宣言
// use Illuminate\Support\Carbon; // Laravel における use 宣言
use App\Helpers\Carbon;
config でエイリアスを指定することで、use 宣言なし(代わりに \Carbon
指定)で呼び出すことも可能。
config/app.php
'aliases' => [
~~ (省略)~~
'Carbon' => App\Helpers\Carbon::class,
],
now(), today() ヘルパー
大抵は、use 宣言をせずに使用できる now()
ヘルパーを常用するだろう。
ヘルパーが参照するクラスが App\Helper\Carbon
となるように、AppServiceProvider
で定義することができる。
ついでに、ここで noOverflow
を初期値に設定する。
app/Providers/AppServiceProvider.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 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),
}
1月末の1ヶ月後は2月末であり、もとの変数 $now
は維持されるようになった。
>>> 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
に変更になっている。
>>> 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),
}