ViewModelの使いどころ

View (Blade) 内に複雑な条件分岐を書いていませんか?それ、ViewModelを使うと、View (Blade) 内が簡潔になります。

ViewModelの実装例

まずは ViewModel の実装例を示します。

namespace App\ViewModels\UserRegistration;

use App\Enums\ProductTypeEnum;

/**
 * ユーザー登録フォーム用 ViewModel
 */
final readonly class UserRegistrationFormModel
{
    /**
     * @param  ProductTypeEnum  $productType 製品種類
     * @param  ?string  $serialNumber 製品シリアル番号
     * @param  string  $productName 製品名
     */
    public function __construct(
        public ProductTypeEnum $productType,
        public ?string $serialNumber,
        public string $productName,
    ) {
    }

    /**
     * 写真表示部分の CSS クラス名を得る
     *
     * @return string [] 製品種類に応じた CSS クラス名
     */
    public function getProductPhotoCssClasses(): array
    {
        $classes = [
            'photo-frame',
            'product',
        ];

        $classes[] = match ($this->productType) {
            ProductTypeEnum::STATIONERY => 'stationery',
            ProductTypeEnum::KITCHEN    => 'kitchen',
            ProductTypeEnum::EXTERIOR   => 'exterior',
        };

        return $classes;
    }
}

ControllerからViewに値を渡す

そしてControllerからViewに対して多くの変数を渡すよりも、ViewModelインスタンスひとつにまとめるのです。その実装例がこちらです。

class UserRegistrationFormController extends Controller
{
    public function __invoke(
        UserRegistraionRequest $requestData,
        GetProductByProductTypeService $getProductByProductType,
    ) {
        $product = $getProductByProductType->execute(
            productType: $requestData->productType,
        );

        return view('components.user-registration.form', [
            'viewModel' => new UserRegistrationFormModel( // 👈 ここです
                productType: $requestData->productType,
                serialNumber: $requestData->serialNumber,
                productName: $product->name,
            ),
        ]);
    }
}

この ViewModel は Blade テンプレートから Data Transfer Object のように扱えます。

@php
    /** @var App\ViewModels\UserRegistration\UserRegistrationFormModel $viewModel */ // 👈 このphpdocをお忘れなく
@endphp

<form method="post">
    @csrf
    <div class="form-inner">
        <div class="row">
            // 👇 CSSクラス名はViewModelで計算していますので、Bladeテンプレート内での条件分岐がなくなり簡潔になります 👇
            <div class="{!! implode(' ', $viewModel->getProductPhotoCssClasses()) !!}">
                <img src="...">
            </div>
        </div>

        @if(null !== $viewModel->serialNumber)
            <div class="row">
                {{ $viewModel->serialNumber }} // 👈 フィールドが定義されているので typo が起きにくい
            </div>
        @endif

        <div class="row">
            {{ $viewModel->productName }} // 👈 フィールドが定義されているので typo が起きにくい
        </div>

        <div class="row">
            <input type="text" name="name">
        </div>

        <div class="row">
            <input type="email" name="email" >
        </div>

        ...
    </div>
</form>
  • CSSクラス名はViewModelで計算していますので、Bladeテンプレート内での条件分岐がなくなり簡潔になります。
  • 各フィールドを参照するとき、定義済みのフィールドなので圧倒的に typo が起きにくいです。