Livewireのエラー処理をカスタマイズする

投稿日 最終更新日

2024年10月30日

Livewireのエラー処理について理解していないところが多かったので。

Livewireに対する通常のレスポンス

Livewireは通常、非同期でサーバーと通信をする。
つまり、ブラウザのバックグラウンドでJavaScriptがサーバーとHTTP通信をしているということになる。

サーバーからのHTTPレスポンスを見てみると

{
	"components": [
		{
			"snapshot": ...
			"effects": {
				"returns": [
					null
				],
				"html": ...
			}
		}
	],
	"assets": []
}

のようにJSON形式になっている。
LivewireはこのJSON形式のレスポンスを取得し、再描画している。

Laravelのエラー

Laravel内でエラーが発生すると、専用のエラー画面が表示される。
あれってつまり、LaravelがLaravel内で発生したエラーをキャッチしてHTMLにしてHTTPレスポンスで返しているということになる。

そのため、$this->authorize('delete', $post);とかでエラーになると、Livewire的には先のJSON形式のレスポンスを期待しているのに、HTML形式でレスポンスが返ってくることになる。

結果、 モーダルでエラーが表示されている画像 のようにiframeのモーダルでエラーが表示される。

これは、Livewireの粋な計らいで、HTML形式のレスポンスならiframeで埋め込んで表示するようになっているみたい。

簡易的な実装ならこれでもいいんだけど、ほとんどの開発者にとってはこの表示は避けたいはず。

このモーダルは前に私が投稿したちゃんとしたモーダルをLaravel+Alpine.js+TailwindCSSで作成するに反していて、ユーザーに優しくないし、iframeで1画面に2個同じサイトがあるのはちょっと異質。

Livewireのモーダルエラー表示をカスタマイズする

これ、一応カスタマイズできなくはない。

公式ドキュメントにある通り、Requestの結果をフックに処理が実行できて、preventDefault()を付けることでモーダル表示も無くなる。

つまり、

document.addEventListener('livewire:init', () => {
        Livewire.hook('request', ({ fail }) => {
            fail(({ status, preventDefault }) => {
                if (status === 403) {
                    alert('この処理は許可されていません。')

                    preventDefault()
                }
            })
        })
    })

のようにしてあげると alertでエラーが表示されている画像 みたいに任意の処理ができる。

JSON形式のエラーで独自のエラーメッセージを返す

先の方法でも大体は問題ないと思うんだけど、JSON形式でエラーを返してあげると更にカスタマイズの幅が広がる。

JSON形式でエラーを返す方法は色々あるだろうけど、ここではカスタム例外を作る方法を紹介する。

先ず、オリジナルの例外を作る

php artisan make:exception Custom403LivewireException

中身は以下のような感じにしてみる

<?php

namespace App\Exceptions;

use Exception;

class Custom403LivewireException extends Exception
{
    public function render($request)
    {
        if ($request->hasHeader('x-livewire')) {
            return response()->json([
                "message" => $this->getMessage()
            ], 403);
        } else {
            abort(403);
        }
    }
}

ここでは、リクエストのHeaderにx-livewireが付いているなら、JSON形式でメッセージを返す感じ。

次に認可処理部分を

-        $this->authorize('update', $this->post);
+        if (Gate::denies("update", $this->post)) {
+            throw new Custom403LivewireException("認証エラーだよ");
+        }

のようにカスタム例外を返す形に。

そして、さっきのjsを

<script>
    document.addEventListener('livewire:init', () => {
        Livewire.hook('request', ({ fail }) => {
            fail(({ status, content, preventDefault }) => {
                if (status === 403) {
                    preventDefault();
                    try {
                        jsonContent = JSON.parse(content);
                        errorMessage = jsonContent.message || "エラーが発生しました";
                        alert(errorMessage)
                    } catch (e) {
                        alert("エラーが発生しました");
                    }
                }
            })
        })
    })
</script>

のようにすれば、独自のエラーメッセージを表示できるようになった。

独自のエラーメッセージが表示されている画像

Livewireは非同期処理をしてるんだからJSON形式を期待するし、違ったら独自の処理をするっていうのが言われれば当たり前なんだけど、私はなかなか気づけなかった。