Eloquentモデルのアクセサを使って時刻フォーマットを統一する

Laravel を学習していて、時間を管理するフィールドを特定の時刻フォーマットで取得したいことがありました。
Eloquent モデルの[アクセサ](https://readouble.com/laravel/9.x/ja/eloquent-mutators.html)を活用したら便利だったので備忘録として残しておきます。

- PHPのバージョン : 8.1.14
- Laravelのバージョン : 9.42.2

まずはデータベースのテーブル構造から。
今回は例として、ユーザ情報を管理する以下のようなテーブルがあるとします。

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('first_name', 100);
            $table->string('last_name', 100);
            $table->timestamp('logined_at');
            $table->timestamp('created_at')->useCurrent();
            $table->timestamp('updated_at')->useCurrent();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
};
```

また、このテーブルには以下のレコードが登録されているとします。

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('users')->insert([
            [
                'first_name' => '佐藤',
                'last_name' => '一郎',
                'logined_at' => Carbon::now()
            ]
        ]);
    }
}
```

それでは artisan コマンドでユーザ情報のモデルを作成します。
作成直後は以下のようなコードになっているかと思います。

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasFactory;
}
```

この状態で `logined_at` の値を取得した場合 `Y-m-d H:i:s` 形式の文字列となります。

```php
User::findOrFail(1)->logined_at
// 2023-01-21 15:56:17
```

これを上記の形式ではなく `Y/m/d H:i:s` 形式の文字列で取り扱いたい場合、一案として以下のような方法もありますが、変換ロジックが方々に散らばることになるので保守性の悪化に繋がります。

```php
Carbon::createFromFormat(
    'Y-m-d H:i:s',
    User::findOrFail(1)->logined_at
)->format('Y/m/d H:i:s');
// 2023/01/21 15:56:17
```

それならば変換ロジックを関数化して DRY にすれば...ということも考えられますが、ここでアクセサの出番です。

> アクセサは、Eloquentの属性値にアクセスが合った時に、その値を変換するものです。アクセサを定義するには、アクセス可能な属性を表すprotectedなメソッドをモデル上に作成します。このメソッド名は、裏に存在するモデル属性やデータベースカラムの「キャメルケース」表現に対応させる必要があります。

引用文の通り、アクセサはモデルのフィールドにアクセスした時に、その値を変換するための機能です。
本記事の例で言うと「`logined_at` フィールドの値を取得した側が変換を頑張るんじゃなくて、時刻フォーマットの変換はアクセサに任せて、取得した時には要件通りの時刻フォーマットにしておきましょう。」といった考え方になります。

ここまでを基に `logined_at` フィールドを `Y/m/d H:i:s` 形式の文字列で取得するためのアクセサを実装した場合、以下のようになります。
なお、本記事のアクセサの書き方は Laravel 9 で追加されたものになり、[Laravel 8 以前では異なる書き方](https://readouble.com/laravel/8.x/ja/eloquent-mutators.html)をする必要がある点に注意してください。

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;

class User extends Model
{
    use HasFactory;

    /**
     * `Y/m/d H:i:s`形式のログイン日時を取得
     *
     * @return Attribute
     */
    protected function loginedAt(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => Carbon::parse($value)->format('Y/m/d H:i:s')
        );
    }
}
```

```php
User::findOrFail(1)->logined_at
// 2023/01/21 15:56:17
```

ちなみにアクセサはモデルクラスのアクセス可能なプロパティであれば、データベースに実在するフィールドに限らないので、以下のような使い方も可能です。

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;

class User extends Model
{
    use HasFactory;

    /**
     * `Y/m/d H:i:s`形式のログイン日時を取得
     *
     * @return Attribute
     */
    protected function loginedAt(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => Carbon::parse($value)->format('Y/m/d H:i:s')
        );
    }

    /**
     * ユーザのフルネームを取得
     * 
     * フルネームは『ユーザ姓』『半角スペース』『ユーザ名』の順に結合した文字列とする。
     *
     * @return Attribute
     */
    protected function userName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->first_name} {$this->last_name}"
        );
    }
}
```

```php
User::findOrFail(1)->userName;
// 佐藤 一郎
```

記事のタイトルからは少しそれてしまいましたが、アクセサを活用することで変換ロジックを DRY に保てて保守性も高まると思いますので上手に利用していきたいところです。