💡

Laravel Filamentで請求システム作る No5 権限機能を追加

2023/11/20に公開

前回に引き続きFilamentで請求システムを作っていきます。

ユーザー管理まで作ったら、やっぱり権限機能を作りたくなってきました。
別に僕一人で利用するシステムなので、必要ないっちゃないのですが、
今後Filamentで案件をこなす時は確実に必要な機能なので、
このタイミングを逃すのはもったいない。

LaraveとFilamentでの権限について

laravelにはgateとpolicyという権限機能が有ります。
https://reffect.co.jp/laravel/laravel-gate-policy-understand/
こちらの記事がものすごく解りやすかったので、読んでみてください。

要は、laravelとしてはgateとpolicyで画面やデータに対する権限を作ってね。
って事みたいですね。

そしてfilamentもplicyで自動的にリソースへのアクセスを自動で制御できるようになっているみたいです。
https://filamentphp.com/docs/3.x/panels/resources/getting-started#authorization

要は、laravelのpolicyを上手い事つくれたら、
後はfilamentが勝手にやってくれるみたいですね。

laravelのpolicyとFilamentリソースでの挙動

viewAny

viewAnyがTrueなら、
サイドバーのメニューにリソース名が表示される。
Falseならそのリソースへはアクセスできない。

view

Trueならば、レコードの表示が可能
Filamentでは、表示画面が表示できるという意味
編集画面が表示できるけど、更新ボタンが無いという意味ではない。

表示画面について
https://filamentphp.com/docs/3.x/panels/resources/viewing-records

create

Trueならば、新規作成ができる
Filamentでは、作成画面が表示でき、作成ボタンをクリックできるという意味

作成画面について
https://filamentphp.com/docs/3.x/panels/resources/creating-records

update

Trueならば、レコードの更新ができる
Filamentでは、作成画面が表示でき、作成ボタンをクリックできるという意味

delete

Trueならば、レコードの削除ができる

forceDelete

複数のレコードを一括削除できる

restore

復元できる


ここからはFilament独自のplicy
正直使いどころを理解できていないので、さらっと行きます。

reorder

並び替えができる

という事で実験

手動でUserモデルに対してPolicyを作ってみる。

Userモデルはusersテーブルを操作するクラスなので、
命名規則はUserPolicyになる。

という事で、以下のコマンドを実行する

./vendor/bin/sail artisan make:policy UserPolicy

でけた。

たとえば、
UserPolicyviewAny関数の返り値がfalseの場合は
サイドバーに表示されないそうなので、そんなソースを書いてみた。

app\Policies\UserPolicy.php
namespace App\Policies;

use App\Models\User;

class UserPolicy
{
    public function viewAny()
    {
        return false;
    }
}

お~!表示しない!

Trueだと表示する

app\Policies\UserPolicy.php
    public function viewAny()
    {
        return false;
    }

同じように、createとviewとupdateとdeleteも全部Falseにした。

app\Policies\UserPolicy.php
class UserPolicy
{
    public function viewAny()
    {
        return true;
    }

    public function create()
    {
        return false;
    }

    public function view()
    {
        return false;
    }

    public function update()
    {
        return false;
    }

    public function delete()
    {
        return false;
    }
}

するとこんな感じで一覧だけ表示してその他の操作は全部できなくなった。

URLを直接叩いてみる。
updateがtrueならば、http://localhost/admin/users/1/editで編集画面が表示するはずだけど、
updateがfalseならば、ちゃんと403が帰ってくる。

これをDBで管理しちゃえばいいんだね。

DB構造を考える

ちょっと調べたけど、policyをDBで管理する方法を検討してる記事が無かったので、
考えてみる。

  1. user_has_policysテーブルを用意する
  2. user_has_policysでは、ユーザーID・モデル名(英語)・viewAny・view・create・update・deleteのカラムが有る。
  3. usersから見たuser_has_policysはhasManyの関係
  4. UserModelにhasPolicyという関数を作り、モデル名(英語)と権限名(viewAnyという文字列)を送ると良い感じにBooleanを返してくれる。
  5. 4の関数をpolicyで利用するようにする。

画面は別で考える

じゃあとりあえず、データとDBだけ作って行こう。

マイグレーションとモデルを作る

./vendor/bin/sail artisan make:model UserHasPolicy -m

でuser_has_policyのモデルとマイグレーションを一気に作る

マイグレーションファイルを作る

database\migrations\2023_11_20_124840_create_user_has_policies_table.php
public function up(): void
    {
        Schema::create('user_has_policies', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id');
            $table->string('model_name', 50)->comment('モデル名');
            $table->boolean('view_any')->comment('一覧表示が可能か');
            $table->boolean('create')->comment('作成が可能か');
            $table->boolean('update')->comment('更新が可能か');
            $table->boolean('delete')->comment('削除が可能か');
            $table->boolean('view')->comment('詳細表示が可能か');
            $table->timestamps();
        });
    }

とりあえずこんな感じ。

マイグレーションを実行する

./vendor/bin/sail artisan migrate

テーブルができたので、適当にデータを登録する。

laravelのboolean型はmysqlではtinyintになるので、0or1で登録する。

モデルを作る

UserHasPolicyモデル

fillableを書いて。
マイグレーションでbooleanとしたカラムをcastして0 or 1からtrue or falseに変換できるようにしておく。

app\Models\UserHasPolicy.php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class UserHasPolicy extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'model_name',
        'view_any',
        'create',
        'update',
        'delete',
        'view',
    ];

    protected $casts = [
        'view_any' => 'boolean',
        'create' => 'boolean',
        'update' => 'boolean',
        'delete' => 'boolean',
        'view' => 'boolean',
     ];
}

Userモデル

UserHasPolicyとhasManyのリレーションを作るり、
hasPolicyというメソッドを作る

app\Models\User.php
use Illuminate\Database\Eloquent\SoftDeletes;
+ use App\Models\UserHasPolicy;

class User extends Authenticatable
{
//省略
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

+    public function hasPolicy($modelName, $policyName)
+    {
+        $userHasPolicy = UserHasPolicy::where('user_id', $this->id)
+            ->where('model_name', $modelName)
+            ->first();
+        if ($userHasPolicy) {
+            return $userHasPolicy->$policyName;
+        }
+        return false;
+    }

+    public function userHasPolicy()
+    {
+        return $this->hasMany(UserHasPolicy::class);
+    }
}

policyで利用する

app\Policies\UserPolicy.php
namespace App\Policies;

use App\Models\User;

class UserPolicy
{
+    private $modelName = 'user';

-    public function viewAny()
+    public function viewAny(User $user)
    {
-        return true;
+        return $user->hasPolicy($this->modelName, 'view_any');
    }

-    public function create()
+    public function create(User $user)
    {
-        return false;
+        return $user->hasPolicy($this->modelName, 'create');
    }

こんな感じにした。

DBの登録状況は
viewAnyを1(True)でその他は0(false)

となって、一覧画面を表示できた。

DBの登録状況は全部1

で全部の処理が可能になった。

次回

DBのレコードを利用したPolicyの動作について理解できたので、
次回はリファクタリングをしながら他のモデルのPolicyも作っていきます。

Laravel Filamentで請求システム作る No6 Policyをリファクタリング

Discussion