Laravel 5.7 + laravel-permission + laravel-adminで、ユーザー編集画面で権限も編集可能にする

「laravel-adminで、追加パラメータのあるピボットテーブルをコーディングなしで保存する。」
「assign()の第2パラメータなしでピボットテーブルの追加パラメータを扱う」
「モデルにbelongsToManyピボットテーブルの追加パラメータを指定して保存まで自動化する」
といったタイトルでもいいですね。

※ここで出てくる「ユーザー」は、管理画面ではなくフロント側のユーザーです。

Laravel 5.7を導入して、``laravel-permission`` + ``laravel-admin``で快適開発！と思ってたら、ユーザーのRoleとPermission編集画面作成でハマった話。日本語でも英語でもあまり情報がなかったのでコツだけ紹介。

# 普通に作るとエラーが出る。

[この記事](https://tac-blog.tech/index.php/2018/10/02/laravel-admin-belongstomany/)などを参考にしながら進めてみる。
``laravel-permission`` + ``laravel-admin``を導入して、ユーザーモデルの``User.php``に``laravel-permission``を割り当て。さらに``role()``と``permission()``を実装して、

```php:app/User.php
...
    use HasRoles;
...
    public function role()
    {
        return $this->belongsToMany('Spatie\Permission\Models\Role', 'model_has_roles', 'model_id', 'role_id');
    }

    public function permission()
    {
        return $this->belongsToMany('Spatie\Permission\Models\Permission', 'model_has_permissions', 'model_id', 'permission_id');
    }
...
```

※もし[こちらの記事](https://qiita.com/hitotch/items/9e22921209eda18d326b)でマルチテナント化してる場合は以下のようにモデルを変更してください。

```php:app/User.php
    public function role(){
        return $this->belongsToMany('App\Role', 'model_has_roles', 'model_id', 'role_id');
    }

    public function permission(){
        return $this->belongsToMany('App\Permission', 'model_has_permissions', 'model_id', 'permission_id');
    }
```

``app/Admin/Controllers/UserController.php``をコマンドで作って、``grid()``にユーザーのRoleとPermissionが表示されるように、``form()``にユーザーのRoleとPermissionが編集できるように記述。

```
php artisan admin:make UserController --model=App\User
```

```php:app/Admin/Controllers/UserController.php
...
    protected function grid()
    {
        $grid = new Grid(new User);
...
        $grid->role()->display(function ($role) {
            $role = array_map(function ($role) {
                return "<span class='label label-success'>{$role['name']}</span>";
            }, $role);
            return join('&nbsp;', $role);
        });
        $grid->permission()->display(function ($permission) {
            $permission = array_map(function ($permission) {
                return "<span class='label label-success'>{$permission['name']}</span>";
            }, $permission);
            return join('&nbsp;', $permission);
        });
...
        return $grid;
    }
...
    protected function form()
    {
        $roles = Role::pluck('name', 'id');
        $permissions = Permission::pluck('name', 'id');

        $form = new Form(new User);
...
        $form->multipleSelect('role')->options($roles);
        $form->multipleSelect('permission')->options($permissions);
...
        return $form;
    }
}
```

あとはRoute通して、メニューに登録して。

```php:app/Admin/routes.php
    $router->resource('users', UserController::class);
```

表示できるかなー？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/315589/69dcf03c-632d-bc72-29bc-81e551c55579.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/315589/2bbc2e6f-49a9-483b-a8cb-e9bddd054618.png)

ここまでは楽勝。で、試しに``user`` Roleを追加して、保存。（デフォルトの状態だとパスワード入力も必須。）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/315589/9c5d3b48-79eb-0762-ee6d-d32d09c47ac4.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/315589/750ec121-4289-3afd-236a-2da99f43457c.png)

``model_type``にデフォルト値がありません。

# エラーの理由の解析

ですよねー。Laravelのピボットテーブルの設計でいうと

- user_roleテーブル（名前も重要）
 - id (プライマリキー)
 - user_id
 - role_id

という設計であるべきところが、laravel-permissionのピボットテーブルの設計って、

- model_has_roleテーブル
 - role_id (プライマリキー)
 - model_type
 - model_id (プライマリキー)

みたいになってて、``model_type``に``App\User``などとモデル名を入れるという仕様なわけで、このmodel_typeはもちろんデフォルト値がないし、Laravelもそんな設計知りません。
　本来であれば、そもそもlaravel-permissionをコールしてRole設定すべきなんでしょうが、めんどくさい！（←重要）
　``app/Admin/Controllers/UserController.php``に``update()``を新設していろいろ書くのすらめんどくさい！（←重要）

ということで、問題は2つあります。

- 一覧表示できてたRoleってそもそも``model_type``考慮してないよね？
- 保存時に**自動で**``model_type``取り扱ってよ！

# 解決方法

問題を順番に解決していきます。

## 一覧表示で``model_type``考慮

ユーザーモデルでは、ピボットテーブルの``model_type``が``App\User``だけになるように絞り込む。

```php:app/User.php
...
    public function role(){
        return $this->belongsToMany('App\Role', 'model_has_roles', 'model_id', 'role_id')
            ->wherePivot('model_type', 'App\User');
    }

    public function permission(){
        return $this->belongsToMany('App\Permission', 'model_has_permissions', 'model_id', 'permission_id')
            ->wherePivot('model_type', 'App\User');
    }
...
```

これで一覧表はOKだし、編集画面を立ち上げた瞬間もOKになるのですが、保存でコケる。。。

## 保存にも有効な``withPivotValue``を使う！

今年[ここ](https://github.com/laravel/framework/pull/22867)で議論の末にLaravel 5.6から実装されたのが``withPivotValue``！仕様は[こちら](https://laravel.com/api/5.6/Illuminate/Database/Eloquent/Relations/BelongsToMany.html)（本家の説明が手抜き過ぎて用途がわからないけど・・・）

```php:app/User.php
...
    public function role(){
        return $this->belongsToMany('App\Role', 'model_has_roles', 'model_id', 'role_id')
            ->withPivotValue('model_type', 'App\User');
    }

    public function permission(){
        return $this->belongsToMany('App\Permission', 'model_has_permissions', 'model_id', 'permission_id')
            ->withPivotValue('model_type', 'App\User');
    }
...
```

実行。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/315589/0c53c29c-2920-6c87-96af-907db07ba685.png)

``user`` Roleも保存できた！

しかも実はlaravel-adminがLaravel 5.5しかサポートしてないのにうまく動作するという快挙。

**こんな重要な機能、なんでLaravel公式の説明に書いてないの。(´;ω;｀)**

# 追記

上記の方法では、``laravel-permission`` のキャッシュがクリアされず、即時反映されないことがわかりました。
即時反映のためには、permissionやroleのつながりを変更するformの保存後にキャッシュをフラッシュする必要があります。コントローラーに以下のコードを入れれば、解決します。

```php:app/Admin/Controllers/UserController.php
...
    protected function form()
    {
...
        $form->saved(function (Form $form) {
            app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
        });
...
    }
...
```

キャッシュについては、以下のページに詳細があります。
https://docs.spatie.be/laravel-permission/v3/advanced-usage/cache/

:thumbsup:※随時お仕事募集中。ご相談ください。:thumbsup: