Laravel Livewire を使うときの Controller の扱い

こんにちは!むちょこです。

最近 Laravel Livewire を始めてみたんですが、 Controller の扱いはどうしたらいいのか混乱してしまいまして。自分なりに調べて結論が出たので共有します!

(前置きが長いので、結論だけ知りたい方は目次から飛んでください)

Laravel Livewire とは

Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving the comfort of Laravel.

https://laravel-livewire.com/

Laravel で動的インターフェイスを簡単に実装するためのフレームワークです。Laravel 7 以上から使えます。

私の個人的な認識としては「できれば JavaScript を書かずに済ませたい、バックエンド寄りのフルスタックエンジニアにおすすめしたいフレームワーク」です。

公式ドキュメント

https://laravel-livewire.com/

混乱しました

Livewire の公式ドキュメントを眺めていると、今まで Controller で行っていた validate や save 、redirect の実行などが Livewire の Component クラスで実装されています。

「あれっ? そこまでやっちゃうと Controller の立場が……」

疑問を抱えながら次に見たのが PHPerKaigi のこちらの動画。

この動画の中で、 localdiskさんは 「Controller は使わないことを決断した」と仰っています。

この動画で言われている通り、 Controller を使わなくても全然実装できちゃいそうなんですよね。

Controller を使わないパターンはシンプルなアプリケーションならコード量少なくすっきりと書けそうですし、私は嫌いじゃないです。

「なるほど。Controller は要らんのね」と納得。

しかしその後勉強会等で Livewire の Controller の扱いを相談してみると Controller は必要では?という意見もちらほら。

ここで混乱してきました。

「Controller は何するんだ?モデルオブジェクトの取得か? Component に渡すのは必要最低限のパラメータ or モデルオブジェクト丸ごとどっち?表示や操作の権限はどこで確認するのがベストなのか????」

Laravel Jetstream のコードを調査してみた

迷ったら公式に頼る怠惰な人間なので、 Laravel Jetstream の Livewire 版ではどのように実装されているのか調べてみました。

Laravel JetwtreamのGitリポジトリ

https://github.com/laravel/jetstream

チーム機能のコードをみていきます。

まずはルーティング。

routes/livewire.php
Route::group(['middleware' => config('jetstream.middleware', ['web'])], function () {
    Route::group(['middleware' => ['auth', 'verified']], function () {
        Route::get('/teams/{team}', [TeamController::class, 'show'])->name('teams.show');
    }
}

TeamController クラスの show() メソッドを呼び出しています。

src/Http/Controllers/Livewire/TeamController.php
public function show(Request $request, $teamId)
{
    $team = Jetstream::newTeamModel()->findOrFail($teamId);

    if (Gate::denies('view', $team)) {
        abort(403);
    }

    return view('teams.show', [
        'user' => $request->user(),
        'team' => $team,
    ]);
}

show() メソッドでは team オブジェクトを取得し、表示する権限があるのか確認した上で blade テンプレートを指定して出力しています。

stubs/livewire/resources/views/teams/show.blade.php
@livewire('teams.update-team-name-form', ['team' => $team])
@livewire('teams.team-member-manager', ['team' => $team])

バーツごとに分かれた各 livewire コンポーネントに team オブジェクトを渡して呼び出し。

src/Http/Livewire/UpdateTeamNameForm.php
public function mount($team)
{
    $this->team = $team;

    $this->state = $team->withoutRelations()->toArray();
}
public function render()
{
    return view('teams.update-team-name-form');
}

Component クラスでは受け取ったデータをテンプレートファイルでも扱えるようにしてから表示しています。

stubs/livewire/resources/views/teams/update-team-name-form.blade.php
 @if (Gate::check('update', $team))
    <x-slot name="actions">
        <x-jet-action-message class="mr-3" on="saved">
            {{ __('Saved.') }}
        </x-jet-action-message>

        <x-jet-button>
            {{ __('Save') }}
        </x-jet-button>
    </x-slot>
@endif

ユーザの権限によって表示したくないところは、テンプレートファイルの方で表示・非表示を切り替えています。

src/Http/Livewire/UpdateTeamNameForm.php
public function updateTeamName(UpdatesTeamNames $updater)
{
    $this->resetErrorBag();

    $updater->update($this->user, $this->team, $this->state);

    $this->emit('saved');

    $this->emit('refresh-navigation-menu');
}

Component クラスの更新用メソッドでは素直にそのまま更新メソッドを実行。

stubs/app/Actions/Jetstream/UpdateTeamName.php
public function update($user, $team, array $input)
{
    Gate::forUser($user)->authorize('update', $team);

    Validator::make($input, [
        'name' => ['required', 'string', 'max:255'],
    ])->validateWithBag('updateTeamName');

    $team->forceFill([
        'name' => $input['name'],
    ])->save();
}

更新権限の確認やバリデーションは、実際に更新を行う Action クラスの方で行っていました。

結論

Jetstream の流れをざっくり図にすると、こんな感じでした。

ポイント

Controller: ページへのアクセス権限の確認、view()メソッドの実行

Livewire Component: なるべく小さい単位で、パーツごとにコンポーネントを作る。表示とアクション要求の受け取りを行う

テンプレート: 各要素別の表示権限の確認はテンプレート内で行う

Action: 操作権限の確認やバリデーションをしてから実際の処理を実行する

Jetstream の実装方法は責務がかなりはっきりしていて、規模が大きくなってきても整然とした状態を保てそうです。

アプリケーションの特性にもよりますが、これをベースに取捨選択していくと混乱しなさそうと思いました:)

つよつよエンジニアさんへ

「解釈が間違っている」「もっとこうするのがオススメ!」などのご意見がございましたら、ぜひ@aya_lachelier宛に教えていただけると嬉しいです!

読んでいただきありがとうございます!