ViewModelって何?なんで必要?

2025.10.10 09:00
2025.10.21 20:47
ViewModelって何?なんで必要?

Bladeを書いているときに、
「ここでnumber_formatするの、なんか気持ち悪いな…」
って思うことありませんか?

最初のうちはぜんぜん気にならなかったんですけど、
ページが増えてくると、

  • 同じ整形を何回も書いてる
  • ちょっとした文言がコントローラに出てくる
  • nullチェックがちらばる
    みたいな、地味だけどモヤモヤする状況が増えてきて。

調べていくうちに、「あ、これってViewModelで解決できるのか」と腑に落ちました。

ViewModelってなんだろう

ざっくり言うと、
ViewModelは「画面で使う形」にデータを整えるクラス

DTOが「運ぶための箱」なら、
ViewModelは「見せるための形」にする人。
Bladeに渡す前に、

  • テキストの合成
  • ラベル名の変換
  • フォーマット(日付、価格、状態など)

といった表示の最終整形を全部ここに集めてつかうようですね。

なんで必要なのか

最初は「Bladeでformatすればいいじゃん」と思ってたんですよ。
でもプロジェクトが大きくなるにつれて、
だんだん「どこで整形してるか分からない」問題が出てきました。

同じような表示ロジックがControllerにも、Resourceにも、Bladeにも点在してて、
修正するときにあちこち探す羽目になる。

それで「表示用の責務をまとめる場所」が欲しくなった。
そこにぴったりハマったのがViewModelでした。

自分が理解した、DTOとの違い

種類役割ロジック
DTO層と層のあいだでデータを運ぶ整形しない(生データ)
ViewModelViewで使いやすい形に整える表示整形(フォーマット・ラベルなど)

DTOは生データ、ViewModelは見せ方。
この2つを分けると、コードの責務が一気に見えやすくなりました。

たとえばこんな感じ

final readonly class SpotListItemViewModel
{
    public function __construct(
        public int $id,
        public string $title,
        public string $priceRangeLabel,
        public string $locationLabel,
        public string $publishedLabel,
    ) {}

    public static function fromRow(SpotListItemRow $r): self
    {
        $date = DateFormatter::ymdWithJpWeekday(CarbonImmutable::parse($r->createdAtIso));
        $loc  = $r->prefectureName . $r->cityName;
        $price = self::formatPrice($r->hasPrice, $r->monthlyPriceMin, $r->monthlyPriceMax);

        return new self(
            id: $r->id,
            title: $r->name,
            priceRangeLabel: $price,
            locationLabel: $loc,
            publishedLabel: $date,
        );
    }

    private static function formatPrice(bool $has, int $min, int $max): string
    {
        if (!$has) return '—';
        if ($min > 0 && $max > 0) return number_format($min).'〜'.number_format($max).'円';
        if ($min > 0) return number_format($min).'円〜';
        if ($max > 0) return '〜'.number_format($max).'円';
        return '—';
    }
}

ViewModelは、
DTO(=生データ)を受け取って、
Bladeで使いやすい形に変えるクラスです。

フォーマット処理をここに閉じ込めておくと、
Bladeがとても静かになります。

Bladeは「出すだけ」でいい

@foreach ($vm->items as $it)
  <article class="c-card">
    <h3>{{ $it->title }}</h3>
    <p>{{ $it->locationLabel }} / {{ $it->publishedLabel }} / {{ $it->priceRangeLabel }}</p>
  </article>
@endforeach

数値整形も日付もここでは一切しません。
VMがもう「表示用の最終形」なので、
Bladeは値を出すだけ

この状態になると、Bladeが一気に読みやすくなるし、
デザイナーや他の人が見ても壊しにくくなります。

ViewModelに入れていいもの・入れないもの

いろいろ調べた中で、ここは線を引いておくとラクだなと思いました。

入れていい

  • 文字列の合成(都道府県+市区名など)
  • 状態の変換(0→「非公開」など)
  • ラベル化(null→「—」、false→「未設定」など)
  • 単純なフォーマット(日付、価格など)

入れない

  • データ取得(DBアクセス)
  • 業務ロジック(料金計算や判定)
  • 外部サービス呼び出し

VMは“見せ方”の責任だけを持つ。
「どう出すか」まではやるけど、「何を出すか」はやらない。
この線を引いておくと、迷いが減ります。

名前の付け方で迷った話

最初、「SpotListItem」って名前のままDTOとVMを両方置いてたんですが、
どっちがどっちか分からなくなったので、
最終的にこう落ち着きました。

DTO:     SpotListItemRow
Result:  SpotListResult
VM:      SpotListItemViewModel

命名で責務を分けておくと、
IDEでファイルを一覧しただけで
「この層で何をしてるか」がわかって地味に便利です。

変換ポイントは1か所だけにする

最初のうちは「DTO→VMの変換」を
Controllerとかで直接書いてたんですが、
それもだんだん散らかる。

今はViewModelの中に静的ファクトリを1つだけ作って、
そこを唯一の入口にしています。

$vm = SpotListItemViewModel::fromRow($dto);

変換ポイントが固定されるだけで、
責務の境界がはっきりしてテストもしやすくなりました。

「ViewModelがあると気持ちが落ち着く」ってこういうことかも

使ってみて思ったのは、
ViewModelって“便利なクラス”というより
整形ロジックをしまっておく安全な棚みたいな存在なんですよね。

Bladeがすっきりして、
Controllerが軽くなって、
結果的に「どの層で何をしてるか」が明確になる。

それだけなんですけど、地味に開発体験が変わります。

自分の運用ルール(現時点)

  • ViewModel::fromRow() でDTOから生成する
  • nullはできるだけ使わず、「—」や既定値で表示する
  • Bladeでは整形しない
  • 複数データを扱うときは 〇〇ViewModel〇〇ListViewModel に分ける
  • 共通整形は Formatter クラスへ

まだ完璧じゃないけど、このルールで混乱はだいぶ減りました。

まとめ:ViewModelは「見た目の責務を閉じる場所」

調べていくうちにわかったのは、
ViewModelはMVCの「V」ではなく、
「Viewをシンプルに保つための中間層」ってこと。

BladeにあったロジックをViewModelに逃がすだけで、
コードがぐっと整理される。

言葉で聞くと小さな変化だけど、
体感では「設計の手触り」がかなり変わります。

今回は以上です!