前回の記事でPolicyを使った認可チェックの実装方法について紹介しました。その中でGateファサードのメソッドを使うことで色々応用が効く点に触れました。今回はその辺を少し掘り下げてどんなケースで応用が効くのか紹介したいと思います。

不認可の理由を表示したい

時としてユーザの操作が制限されている(つまり特定のアクションが不認可である)場合にその理由を表示したいケースがあります。例えば以下の様な商品一覧ページがあったとして、何かしらの理由で商品が購入不可の場合に購入ボタンを無効とし、その理由を赤字で表示したい場合など。もちろん、このケースでもPolicyが適用できます。

こちらのページのPolicyの実装は以下のようになります。認可がdenyされるケースが複数あり、それぞれ異なるメッセージが返されます。


namespace App\Policies;

use App\Models\User;
use App\Models\Product;
use Illuminate\Auth\Access\Response;

class ProductPolicy
{
    public function available(?User $user, Product $product): Response
    {
        $err = match (true) {
            ($product->vip_only === 'Y' && $user?->vip_flag !== 'Y') => 'VIPのみ購入可能な商品です',
            $user?->balance < $product->price => '残高が不足しています',
            default => '',
        };

        return empty($err)
            ? Response::allow()
            : Response::deny($err);
    }
}

さて、これらのメッセージを先の画面のように表示するにはどうすればよいでしょうか?

前回の記事において、Policyを使った判別処理の実装方法をblade側、controller側、middleware側に分けて紹介しました。それらの中でcontroller側のauthorize()やmiddleware側のcan()ではdenyの時にResponse::deny()に指定したエラー文言を表示する事ができます。しかし、403ページへ強制的にリダイレクトされてからの表示です。403ページへのリダイレクト無しにエラー文言だけ取得して表示に利用することができないでしょうか?

Gate::inspect()を使ってエラー表示

Gate::inspect()を使うとPolicyを介して認可チェックを行った際に返されるレスポンスを取得する事ができます。そちらからResponse::deny()に指定したエラー文言を以下のように取得する事ができます。

> $response = Gate::inspect('available', Product::first())
= Illuminate\Auth\Access\Response {#6114}

// allowか判別
> $response->allowed()
= false

// denyか判別
> $response->denied()
= true

// denyのメッセージ取得
> $response->message();
= "残高が不足しています"

// Response自体を文字列にキャストしてもメッセージが取得できる
> (string) $response
= "残高が不足しています"

ということで、以下のようにbladeにGate::inspect()を埋め込むと、PolicyがResponse::deny()を返した場合に冒頭の商品ページのエラー表示とすることができます。(購入ボタンの部分のみ抜粋しています)

...
<div class="mt-2">
    @can('available', $product)
        <button class="button is-small is-success">
            購入する
        </button>
    @else
        <button class="button is-small is-success" disabled>
            購入する
        </button>
        <p class="has-text-weight-bold has-text-danger">{{ Gate::inspect('available', $product)->message() }}</p>
    @endcan
</div>
...

@reason

先のようなエラー表示のケースは良く使われそうですがパッと見て何を表示しているのか分かりづらいと思いませんか?そんな時こんな記事を見つけました。@reasonというディレクティブを追加するというものです。AppServiceProviderのboot()に以下を追加してみましょう。

...
    public function boot(): void
    {
        // @reason を追加
        Blade::directive('reason', function ($expression) {
            return "<?php echo Gate::inspect($expression)->message() ?>";
        });
    }
...

すると先ほどのblade側でGate::inspect()をcallしていた部分を次のように書き換えられます。

...
<div class="mt-2">
    @can('available', $product)
        <button class="button is-small is-success">
            購入する
        </button>
    @else
        <button class="button is-small is-success" disabled>
            購入する
        </button>
        <p class="has-text-weight-bold has-text-danger">@reason('available', $product)</p>
    @endcan
</div>
...

より分かりやすくなりましたね。

By hikaru