Laravelで和暦を扱う

和暦を扱う必要があったので，その実装をまとめてみました

# 環境
- Laravel 5.8

# 作るもの
- 年号を管理するテーブル(era_names)
- 和暦クラス
- 和暦と西暦の変換処理

# 年号テーブルの作成
年号を管理するテーブルには

- 年号の名前
- 略称
- 開始日
- 終了日

の4つのカラムを作成します．テーブル名を```era_names```とし，マイグレーションファイルは次のようになります．

```php
public function up()
{
    Schema::create('era_names', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('short_name');
        $table->date('start_at');
        $table->date('end_at');
        $table->timestamps();
    });
}
```

作成したマイグレーションを実行しテーブルを作成したら，モデルを作成します．

```php:app\Models\EraName.php
class EraName extends Model
{
    /** @var string */
    protected $table = 'era_names';

    /** @var array */
    protected $dates = [
        'start_at',
        'end_at',
    ];

    /** @var array */
    protected $guarded = [
        'id',
    ];
}
```

最後にデータを追加します．追加する方法はシーダやtinkerを用いてください．

| name | short_name | start_at | end_at |
| --- | --- | --- | --- |
| 昭和 | S | 1926-12-25 | 1989-01-07 |
| 平成 | H | 1989-01-08 | 2019-04-30 |
| 令和 | R | 2019-05-01 | 2100-01-01 |

# 和暦-西暦変換処理の作成
プロジェクトの方針にしたがって，和暦-西暦の変換処理を作成します．ここでは和暦クラスを作成し，Carbon(CarbonImmutable)と相互変換する形で実装します．他の実装として変換処理をすべてServiceに書き，ファサードやヘルパを通して変換することなどが考えられます．

Carbonに和暦への変換メソッドを実装するために，次のようなサービスプロバイダを用意します．

```php:app\Providers\DateServiceProvider
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Date;
use App\Models\EraName;
use App\Models\Wareki;
use Carbon\CarbonImmutable;

class DateServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        Date::use(CarbonImmutable::class);  // 日付を扱うクラスをCarbonImmutableに変更

        Date::macro('toWareki', function () {  // 日付を扱うクラスにtoWarekiメソッドを追加
            return new Wareki($this);
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Wareki::setEraNames();
    }
}
```

``Date::macro(...)``を用いることで，日付オブジェクトにメソッドを追加することができます．ここでは```toWareki```メソッドを追加しています．

```php
Wareki::setEraNames();
```

の部分では下で定義する``Wareki``クラスに``static``プロパティとして``EraName::all()``で取得できる年号テーブルの一覧をセットしています．``static``として年号一覧を持つことで，処理の途中でera_namesテーブルの内容が変更されても，処理開始時の年号データが使われるようになります．また，年号を取得するSQLが1度しか実行されなくなるなどの利点があります．

次に和暦クラスを実装します．場所はプロジェクトの方針にしたがいましょう．ここでは```app\Models```下に作成します．

```php:app\Models\Wareki.php
<?php

namespace App\Models;

use Illuminate\Support\Facades\Date;
use App\Models\EraName;

class Wareki
{
    /** @var collection */
    private static $eraNames;

    /** @var int */
    public $year;  // 年
    public $fiscalYear;  // 年度
    public $month;
    public $day;

    /** @var EraName */
    public $eraName;

    public static function setEraNames() {
        if (is_null(self::$eraNames)) self::$eraNames = EraName::all();
    }

    /**
     * @param Carbon|CarbonImmutable
     */
    public function __construct($ymd) {
        $this->eraName = self::$eraNames->first(function ($eraName) use ($ymd) {
            return $eraName->start_at->startOfDay() <= $ymd && $ymd <= $eraName->end_at->endOfDay();
        });

        $this->year = $ymd->year - $this->eraName->start_at->year + 1;
        $this->fiscalYear = $this->year - ((1 <= $ymd->month && $ymd->month <= 3) ? 1 : 0);
        $this->month = $ymd->month;
        $this->day = $ymd->day;
    }

    /**
     * @return Carbon|CarbonImmutable
     */
    public function toYmd() {
        return Date::createMidnightDate(
            $this->eraName->start_at->year + $this->year - 1,
            $this->month ?? 1,
            $this->day ?? 1,
        );
    }

    /**
     * @param string $format
     * @return string
     */
    public function format(string $format) {
        $ret = '';
        foreach (mb_str_split($format) as $str) {
            if ($str === 'Y') $ret .= sprintf('%s%02d', $this->eraName->name, $this->year);
            else if ($str === 'S') $ret .= sprintf('%s%02d', $this->eraName->short_name, $this->year);
            else if ($str === 'y') $ret .= sprintf('%s%02d', $this->eraName->name, $this->fiscalYear);
            else if ($str === 's') $ret .= sprintf('%s%02d', $this->eraName->short_name, $this->fiscalYear);
            else if ($str === 'm') $ret .= sprintf('%02d', $this->month);
            else if ($str === 'd') $ret .= sprintf('%02d', $this->day);
            else $ret .= $str;
        }

        return $ret;
    }
}
```

和暦-西暦の変換処理は和暦クラスにまとめることにしてあります．コンストラクタで西暦->和暦の変換を行い，```toYmd```メソッドで和暦->西暦の変換を行います．```format```メソッドでは和暦を文字列に変換します．例えば

```php
$ymd = Date::parse('2019-03-01');  \\ 平成年31年3月1日

$ymd->toWareki()->format('Y年');  \\ 平成31年
$ymd->toWareki()->format('S');  \\ H31
$ymd->toWareki()->format('y年度');  \\ 平成30年度
$ymd->toWareki()->format('s');  \\ H30
$ymd->toWareki()->format('m月d日');  \\ 03月01日
```

このようになっています．この和暦クラスでは時分秒を扱わない形で実装しています．


# まとめ
Laravelで和暦を扱ってみました．年号データをDBではなくconfigなどに設定する場合も，``setEraNames``の部分を調整することで実装することができます．また，``Data::macro``を用いて年度初めを取得する処理などを追加してもいいかもしれません．