[Laravel5.4] 認可をコントローラー毎に複数権限でやりたい

よく一緒に語られること多いけど認証と認可は別物だよ。

# 前提

* Laravel 5.4
* PHP7.1
* ロールは複数（管理者、一般ユーザー1、一般ユーザー2、一般ユーザー3・・・etc）
* ロールは特に継承関係は持ってなく単独
* Laravel のポリシーによる認可は今回は使わない

# 解決したいこと

* コントローラー毎にアクセス可能なロールを指定したい
* アクセス可能なロールをAND条件じゃなくてORで複数指定したい（管理者 or 一般ユーザー1ならOK等）

# 解決方法

* アクセス可能なロールの指定方法をパイプ区切りで複数指定する
* `AuthServiceProvider`でパイプを再帰的に処理

### AuthServiceProviderを作成

`$user->isなんとか()`はやっつけ

```php:app/Providers/AuthServiceProvider
<?php
namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // ロール毎に Gate にクロージャーを定義
        Gate::define('admin', function (\App\User $user) {
            return $user->isAdmin();
        });

        Gate::define('general1', function (\App\User $user) {
            return $user->isGeneral1();
        });
        
        Gate::define('general2', function (\App\User $user) {
            return $user->isGeneral2();
        });
        
        // $ablities をパイプ区切りで複数判定
        // ⇒ $abilities は 'admin|general1' といったような形式を想定
        Gate::before(function (\App\User $user, $abilities, $arguments = []) { 
            if (strpos($abilities, '|') === false) {
                return;
            }

            $abilities = array_filter(explode('|', $abilities));
            if (! count($abilities)) {
                return;
            }

            foreach ($abilities as $ability) {
                // 再度Gate::beforeクロージャーが呼ばれるが、
                // 次に渡されるときは | がないのでGate::define 側で定義したメソッドがよばれる
                if ($user->can($ability, $arguments)) {
                    return true;
                }
            }
            return false;
        });
    }
}
```

`Gate::before`で呼び出してるクロージャーがミソで、$user->can を呼び出すとパイプで区切られた値にて再度このクロージャーが呼び出される。クロージャー内で何も返さずに return すると今度はロール毎にGateで定義したクロージャーが呼び出される。

### AuthServiceProviderの登録

まぁスケルトン使ってると勝手に登録されてたりする。
`AuthServiceProvider`自体も最初からあったり。

```php:config/app.php
return [
    // ~~~ 他色々 ~~~
    'providers' => [
        /*
         * Application Service Providers...
         */
        App\Providers\AuthServiceProvider::class,
    ],

    // ~~~ 他色々 ~~~
];
```

### コントローラーのmiddleware

各コントローラーのプロパティの文字列いじるだけで簡単に定義できるようにしたいので、基底クラス側で middleware を設定

```php:App/Http/Controllers/Controller.php
<?php

namespace App\Http\Controllers\Controller;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
    
    public static $can = '';

    public function __construct()
    {
        if (strlen(static::$can)) {
            // @see \App\Providers\AuthServiceProvider
            $this->middleware('can:' . static::$can);
        }
    }
}
```

# 使う

コントローラー

```php
<?php
namespace App\Http\Controllers;

class HogeController extends Controller
{
    // 管理者と一般ユーザー1ならOK
    public static $can = 'admin|general1';
}
```

blade

```
@can(App\Http\Controllers\HogeController::$can)
    <a href="{{ route('hoge.index') }}">ほげ</a>
@endcan
```

# 所感

これがベストプラクティスなのか分からん／(^o^)＼
blade の判定部分もうちょっとすっきりさせたい・・・