Result/Row/ViewModel三段構成がしっくりきた話
一覧画面を作るたびに、「どの層で何を持つか」がぼやけがちでした。
Eloquentのまま渡すと表示ロジックが散るし、DTOに寄せるとVMっぽく太る。
そこで今回、Result / Row / ViewModel をきっちり分けてみたら、想像以上に見通しが良くなりました。
最終的に落ち着いたのがこの構成です。
目次
まずは全体の構成
流れをざっくりと。ファイルは膨らみましたが、
責務がはっきりして混乱が減り、改善運用もやりやすくなりました。
Controller
↓
UseCase
↓
Result (Row[] + ページ情報)
↓
ViewModelFactory
↓
ItemViewModel[] ← Formatter(日付・価格・Tel)
↓
ListViewModel(paginator/toArray など)
↓
Blade(出すだけ)Row:1件分の“生データ”を固定する(SpotListItemRow)
SpotListItemRow は readonly & 非null のDTO。
検索結果1件分をありのまま運ぶ役で、表示の整形は一切しない。
- 住所・電話・辞書名など、一覧に必要な最小限を持つ
- 価格系は
monthlyPriceMin/Maxと 状態フラグ(forCheckMonthly,isPricingConsul)で明示 - 画像や状態アイコンもそのまま(
statusIconsは{type_id,status}の配列)
良かった点:
「非null+フラグ」で状態が明示されるので、上位でissetを書かなくて済む。Result:束ねて運ぶ“台車”(
SpotListResult)
SpotListResultはRow[] + ページ情報のまとまり。
UseCaseからController/VMへ渡す“契約”になりました。
itemsにSpotListItemRow[]total / perPage / currentPageをセットで保持- ここでも整形はしない。あくまで運搬役
良かった点:
ページング情報の責務が固定されて、「どこから取る?」迷いが消えた。
ViewModel:見せ方の最終形(SpotListItemViewModel)
SpotListItemViewModel::fromRow() が唯一の変換入口。
ここで Formatter に委譲しながら表示用に整えます。
SpotFormatter:住所結合 / 価格レンジ / 画像URLTelFormatter:表示用・数字のみ の両方を生成statusIconsは辞書解決済みの最小構造を渡して、そのままループ可能に
ねらい:
Bladeは$vm->monthlyPriceや$vm->fullAddressを出すだけ。
“どこで整形するか”を固定できたので、画面ごとのブレがなくなりました。一覧全体のVM:道具が揃ってる(
SpotListViewModel)
SpotListViewModel は 一覧全体のVM。fromResult() で Row[] → ItemVM[] に変換して、下記の“便利関数”でViewを支えます。
totalPages / hasNextPage / hasPreviousPage / from / topaginator():LengthAwarePaginatorに変換(クエリ付与対応)toArray():API/Blade共有用の軽量シリアライズ
良かった点:
ページング周りの微妙な計算をここに隠せるので、BladeもControllerも静か。
Factoryで“辞書解決”を外出し(SpotListViewModelFactory)
SpotListItemViewModel::fromRow() は整形の入口に特化させて、
アイコンの辞書解決だけは SpotIconsViewModelFactory に分離しました。
statusIconsの{type_id,status}生配列 → 表示可能なVM配列へ- その後に
SpotListItemViewModel::fromRow($row, $statusIconViewModels)で組み立て
良かった点:
辞書解決の責務がVMから抜けたことで、VMが“見せ方”だけに集中できた。
使ってみてわかった三段構成の効きどころ
責務の棚卸しが進む
Row=生, Result=束ねる, VM=見せる の三語だけで合意できる。
レビュー基準が明快
「それはRowに入れる?VMでやる?」の会話が早い。
テストの層が分かれる
Row/Resultは型の保証、VMは表示の期待値、Factoryは差し替えを検証。
ページングの匂いを封じ込める
paginator() と totalPages() 系をVMに寄せたら、Controller/Bladeが一気に静かに。
まとめ
Resultが運んで、Rowが持って、ViewModelが見せるこの三段で並べ直したら、
データの旅路がはっきり見えるようになりました。
- Row:検索結果1行の事実
- Result:その束とページの文脈
- ViewModel:画面での語り方
ファイルは増えるけど、迷いが減る。
最終的に、BladeもControllerもスッキリしましたね。
今回は以上です!