【Laravel】Eloquentのキャスト・アクセサ・ミューテタの使い分け

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はソフトデリートによる安全な削除の仕組みを解説しました。

今回は、Eloquentモデルの値を「取得するとき」「保存するとき」に自動加工できる3つの機能、キャストアクセサミューテタを扱います。

似たような役割に見えて実は用途が違うこの3つを、整理して使い分けられるようになりましょう。

3つの機能の全体像

それぞれの役割

まずは3つの機能を一言でまとめておきます。

機能役割主な用途
キャスト(Cast)データベースの値をPHPの型に自動変換する真偽値・日付・配列(JSON)などの型変換
アクセサ(Accessor)値を取得するときに加工する表示用に整形する(氏名の結合、金額のフォーマット等)
ミューテタ(Mutator)値を保存するときに加工する保存前の正規化(ハッシュ化、トリム、暗号化等)

「型を変換したいだけ」ならキャスト、「取得時に加工したい」ならアクセサ、「保存前に加工したい」ならミューテタ、というのが基本の判断軸です。

似ているようで役割が明確に違うため、目的に合わないものを選ぶと余計なコードになりがちです。

なぜこの3つが必要なのか

データベースに保存されている値は、そのままだと扱いにくいことがよくあります。

たとえば、真偽値のつもりで保存したカラムがtinyintとして扱われ、PHP側では"1"という文字列で返ってくる、といったズレが典型例です。

また、「姓」と「名」を別カラムで持っているのに、画面には「フルネーム」で表示したい、というケースも頻繁にあります。

こうした「DBの値とアプリで扱いたい値のギャップ」を埋めるのが、この3つの機能の共通の目的です。

キャスト:型変換を自動化する

基本の書き方

キャストは、モデルにcasts()メソッド(Laravel 11以降の書き方)を定義することで設定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected function casts(): array
    {
        return [
            'is_published' => 'boolean',
            'published_at' => 'datetime',
            'options' => 'array',
        ];
    }
}

この設定をしておくと、$post->is_publishedは自動的にPHPの真偽値(true/false)として扱われます。

published_atはCarbonインスタンスとして取得できるため、$post->published_at->format('Y年m月d日')のような日付操作もそのまま可能です。

optionsカラムにJSON文字列を保存していても、arrayキャストを指定すれば取得時に自動でPHPの配列に変換されます。

よく使うキャストの種類

代表的なキャストの種類を挙げておきます。

<?php

protected function casts(): array
{
    return [
        'is_active' => 'boolean',   // 真偽値
        'price' => 'integer',       // 整数
        'rating' => 'float',        // 小数
        'settings' => 'array',      // JSON⇔配列
        'published_at' => 'datetime', // 日時(Carbon)
        'metadata' => 'collection', // JSON⇔Laravelコレクション
    ];
}

DBのカラム型とPHPで扱いたい型が食い違う場面では、まずキャストで解決できないかを検討するのが基本です。

アクセサ:取得時に値を加工する

Attributeクラスを使った書き方

Laravel 9以降では、Illuminate\Database\Eloquent\Casts\Attributeクラスを使ってアクセサ・ミューテタをまとめて定義するのが標準的な書き方です。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->last_name} {$this->first_name}",
        );
    }
}

last_namefirst_nameという2つのカラムから、full_nameという存在しないカラムを仮想的に作り出しています。

<?php

$member = Member::find(1);
echo $member->full_name; // "田中 太郎" のように結合されて表示される

メソッド名はキャメルケース(fullName)で定義しますが、実際にアクセスするときはスネークケース(full_name)になる点に注意してください。

アクセサの使いどころ

アクセサは「DBには生の値を保存しつつ、画面表示のときだけ加工したい」場面で活躍します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected function priceWithTax(): Attribute
    {
        return Attribute::make(
            get: fn () => number_format($this->price * 1.1),
        );
    }
}

priceカラムには税抜き価格をそのまま保存しておき、税込み表示が必要な場面だけ$product->price_with_taxを使う、という使い分けができます。

「保存する値」と「表示する値」を分けて考えられるようになると、DB設計もシンプルに保てます。

ミューテタ:保存時に値を加工する

Attributeクラスでのset定義

ミューテタは、同じAttribute::make()set引数で定義します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    protected function email(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => strtolower(trim($value)),
        );
    }
}

この設定をしておくと、$member->email = ' Taro@Example.com 'のように前後に空白や大文字が混じった値を代入しても、保存時には自動的にtaro@example.comのような整った形式に変換されます。

ユーザー入力をそのままDBに保存すると表記ゆれが起きやすいメールアドレスのようなカラムには、ミューテタでの正規化がとても有効です。

つまずきやすいポイント:アクセサとミューテタを混同する

私が実際に最初につまずいたのが、「保存時に加工したいのに、アクセサ(get)に処理を書いてしまう」というミスでした。

❌ Before:パスワードのハッシュ化をアクセサに書いてしまう

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;

class Member extends Model
{
    protected function password(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => Hash::make($value), // 取得のたびにハッシュ化してしまう
        );
    }
}

この書き方だと、$member->password取得するたびにハッシュ化処理が実行されてしまいます。

そのため、DBに保存されている値がハッシュ化されないまま平文で残ってしまい、ログイン認証が通らないという不具合が起きました。

「取得時に加工=get」「保存時に加工=set」という基本を混同していたのが原因です。

✅ After:ハッシュ化はミューテタ(set)に書く

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;

class Member extends Model
{
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => Hash::make($value), // 保存時に一度だけハッシュ化する
        );
    }
}

setに処理を書くことで、$member->password = '生パスワード';と代入した時点で一度だけハッシュ化され、DBにはハッシュ化済みの値が保存されるようになります。

「取得時に毎回加工されるのか」「保存時に一度だけ加工されるのか」を意識してgetsetを選ぶことが、この機能を正しく使う一番のポイントです。

応用:キャスト・アクセサ・ミューテタの組み合わせ

独自のキャストクラスを作る

複雑な変換ロジックが必要な場合は、独自のキャストクラスを作ることもできます。

<?php

namespace App\Models;

use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected function casts(): array
    {
        return [
            'total_amount' => MoneyCast::class,
        ];
    }
}

Illuminate\Contracts\Database\Eloquent\CastsAttributesインターフェースを実装したクラスを作れば、金額を「円単位の整数で保存しつつ、取得時は通貨オブジェクトとして扱う」といった、アクセサ・ミューテタでは表現しきれない複雑な変換も一箇所にまとめられます。

同じ変換ロジックを複数のモデルで使い回したい場合は、アクセサ・ミューテタよりも独自キャストクラスの方が適しています。

まとめ

この記事のポイント

  • キャストは型変換、アクセサは取得時の加工、ミューテタは保存時の加工という役割分担で使い分ける
  • Laravel 9以降はAttribute::make(get: ..., set: ...)の書き方に統一されている
  • アクセサ・ミューテタのメソッド名はキャメルケース、実際にアクセスするときはスネークケースになる
  • パスワードのハッシュ化のように「保存時に一度だけ」加工したい処理をgetに書くと事故につながるため注意する
  • 複雑な変換ロジックを使い回したい場合は独自のキャストクラスを検討する

次に読むべき記事

データベース・ORM関連の基礎が一通り身についたところで、次は認証機能の選び方を整理していきましょう。

→ 次の記事:Laravel Breeze・Jetstream・Fortify・Sanctumの違いを整理する:認証機能はどれを選ぶべき?

タイトルとURLをコピーしました