PHP
laravel

続・Laravel で ViewModel を使ってみる

この記事について

Laravel Meetup Okinawa 第5回の発表資料です。

以前に以下の記事を書いたんですが、もうちょっと改良できそうだなーと思っていながら放置してしまっていたので、今回は API(JSONを返すController)からでも使える ViewModel のあり方について探ってみました。

LaravelでViewModelを使ってみる - Qiita


概要


環境

  • PHP 7.1.12
  • Laravel 5.5.28

前回のモデルの問題点

ParkingLotController.php
class ParkingLotController extends Controller
{
    public function show(ShowParkingLotViewModel $view, ParkingLot $parkingLot)
    {
        $view->user = Auth::user();
        $view->parkingLot = $parkingLot;

        return $view->render();
    }
}
  • ViewModel が明示的に現れる
  • プロパティ方式

改良版

ParkingLotController.php
class ParkingLotController extends Controller
{
    public function show(ParkingLot $parkingLot)
    {
        return view('parkingLot.show.json', compact('parkingLot'));
    }
}
  • Client (Controller) から ViewModel を意識する必要がない
  • 必要なデータは配列でまとめて渡す

解説


view() ヘルパー関数の向こう側に ViewModel を隠蔽する


diff --git a/config/app.php b/config/app.php
index 0e4ebed..8608394 100644
--- a/config/app.php
+++ b/config/app.php
@@ -162,7 +162,7 @@ return [
         Illuminate\Session\SessionServiceProvider::class,
         Illuminate\Translation\TranslationServiceProvider::class,
         Illuminate\Validation\ValidationServiceProvider::class,
-        Illuminate\View\ViewServiceProvider::class,
+        App\Providers\ViewServiceProvider::class,

ViewServiceProvider を自前で拡張する


ViewServiceProvider.php
<?php
declare(strict_types=1);

namespace App\Providers;

use Illuminate\View\ViewServiceProvider as Base;
use App\Factories\ViewFactory;

class ViewServiceProvider extends Base
{
    protected function createFactory($resolver, $finder, $events)
    {
        return new ViewFactory($resolver, $finder, $events);
    }    
}

自前の ViewFactory を返すようにする


ViewFactory.php
<?php
declare(strict_types=1);

namespace App\Factories;

use Illuminate\Support\Str;
use Illuminate\View\Factory;

class ViewFactory extends Factory
{
    protected $type = 'html';

    public function make($view, $data = [], $mergeData = [])
    {
        $viewModel = $this->viewModelInstance($view);
        if ($viewModel) {
            $data = $viewModel->apply($data);
        }
        if ($this->type === 'json') {
            return array_merge($data, $mergeData);
        }

        return parent::make($view, $data, $mergeData);
    }

    protected function viewModelInstance(string $view)
    {
        if (Str::endsWith($view, '.json')) {
            $this->type = 'json';
            $view = Str::replaceLast('.json', '', $view);
        }
        $namespace = "App\\Http\\ViewModels\\";
        $classPath = $namespace . $this->parseViewName($view) . 'ViewModel';
        if (!class_exists($classPath)) {
            return null;
        }

        return new $classPath($view);
    }

    protected function parseViewName(string $view)
    {
        return implode("\\", array_map(function ($name) {
            return ucfirst($name);
        }, explode('.', $view)));
    }
}

ViewFactory を自前で拡張する


命名規約

  • view 名はドット区切りのパス
  • .json で終わる名前の場合は JSON で返す
return view('hoge.fuga'); // <-- テンプレートを返す
return view('hoge.fuga.json'); // <-- JSONを返す

で、App\Http\ViewModels\Hoge\FugaViewModel があれば、そのクラスを「通って」データが渡される(JSON形式の場合は"返される")。


その結果、


ViewModel.php
<?php
declare(strict_types=1);

namespace App\Http\ViewModels;

interface ViewModel
{
    public function apply(array $data): array;
}

めっちゃシンプルになった(以前のコードはこちら


実装クラスも、

ShowViewModel.php
<?php

namespace App\Http\ViewModels\ParkingLot;

use App\Http\ViewModels\ViewModel;

class ShowViewModel implements ViewModel
{
    public function apply(array $data): array
    {
        $parkingLot = $data['parkingLot'];
        // 値を書き換えたり
        $parkingLot->name .= '(ViewModel版)';
        // 値を加えたり
        return ['message' => 'ViewModelで作成'] + $data;
    }
}

データの変更だけ意識すればよくなった


まとめ

入出力インタフェースが array になってしまうので、型宣言が使いたい、という向きには敬遠されそうですが、どのみちビューには配列で渡すんだし、未定義とかが怖い、ということであれば、内部で Collection とか Optional とか使えばある程度は安全に書けるとは思うし、試してみる価値はありそう、というかんじはしています。

  • Client(Controller)から ViewModel を意識する必要がなくなった
  • ViewModel ではデータの変更だけ意識すればよくなった

まだまだ改良の余地がありそうな気がしていますが、本日のところはここまでとさせていただきます。
ご意見などお待ちしております :bow: