Laravel 6.x SessionGuardをカスタムする（Cookie jar has not been set の対処法）

#やりたいこと
Laravelでは「どのようにユーザを認証するか」ということをガード（Guard）という概念で実装している。つまり、認証に関してカスタムしたいことがあればガードを自作し、それに基づいて認証を行うように設定すれば良い。
Laravelにはデフォルトのガードが二つ存在し、一つは`SessionGuard`クラス、もう一つは`TokenGuard`クラスである。それぞれデフォルトの web 用のガード、api 用のガードとして実装されている。そこで、それらのサブクラスを実装してデフォルトのGuardと入れ替えればさくっと認証処理をカスタムできる。便利だ。

と思っていたら、意外なところでハマったので、この記事はそれに関するレポートである。より具体的には、

**Cookie jar has not been set**

というエラーが出た時の対処法である。

#結論
上記のエラーに当たったということは基本的なGuardのカスタムはできていて、いざ認証しようとした時（特にRememberMe機能を使用しようとした時）に上記のエラーに当たったものと思われるので、解決策を先に述べる。

おそらく`app/Providers/AuthServiceProvider.php`の`boot`メソッドを用いて自作のガード（`CustomGuard`クラスとする。このクラスは`SessionGuard`クラスを拡張しているものとする。）を以下のように登録していると考えられる。

```php
    public function boot()                                                                                                                                                         
    {                                                                                                                                                                              
        $this->registerPolicies();                                                                                                                                                 
                                                                                                                                                                                   
        // 自作ガードの登録
        Auth::extend('custom_guard', function($app, $name, array $config) {
            return new CustomGuard($name, Auth::createUserProvider($config['provider']), $app['session.store']);                                                                                                                                                     
        });                                                                                                                                                                        
    }                                                                                                                                                                              
```

そしてこの`custom_guard`を`config/auth.php`でガードのドライバーとして指定しているはずである。世の中のカスタムガード作成の指南書にはほぼこれに準ずることが書かれている。

この状態で問題の **Cookie jar has not been set** エラーが出た時には、`boot`メソッド中の自作ガードの登録部分を以下のように変更する。

```php
    public function boot()                                                                                                                                                         
    {                                                                                                                                                                              
        $this->registerPolicies();                                                                                                                                                 
                                                                                                                                                                                   
        // 自作ガードの登録
        Auth::extend('custom_guard', function($app, $name, array $config) {
            $guard = new CustomGuard($name, Auth::createUserProvider($config['provider']), $app['session.store'])

            if (method_exists($guard, 'setCookieJar')) {
                $guard->setCookieJar($app['cookie']);
            }

            if (method_exists($guard, 'setDispatcher')) {
                $guard->setDispatcher($app['events']);
            }

            if (method_exists($guard, 'setRequest')) {
                $guard->setRequest($app->refresh('request', $guard, 'setRequest'));
            }

            return $guard;
        });                                                                                                                                                                        
    }                                                                                                                                                                              
```

以上。

#やっていること
コードの中身を見てもらえれば一目瞭然であるが、変更前はただガードクラスを生成して返しているだけだが、変更後は生成したインスタンスの`setCookieJar`、`setDispatcher`、`setRequest`メソッドを呼び出してから返している。おそらく **Cookie jar has not been set** エラーは`setCookieJar`を呼び出すことによって解決する。

さて、この解決策がどこから来たかという話であるが、それはデフォルトの`SessionGuard`を登録してる`Illuminate\Auth\AuthManager`の`resolve`メソッド及びそこから呼び出される`createSessionDriver`メソッド、`callCustomCreator`メソッドを見ていくと解決する。

まず`resolve`メソッドであるが、これはメソッド名の通りガード名（上記の例で言えば`custom_guard`）からガードクラスのインスタンスを解決するためのメソッドである。その中身を見てみると、処理は

```php
if (isset($this->customCreators[$config['driver']])) {
   return $this->callCustomCreator($name, $config);
} 
```
という条件分岐によって、自作されたガードであるか否かで二つに分かれる。自作のガードであればメンバ変数`$customCreators`配列にその名前が登録されているため`callCustomCreator`メソッドが呼ばれ、デフォルトの`SessionGuard`や`TokenGuard`を使用している場合には、それぞれ`createSessionDriver`メソッドや`createTokenDriver`メソッドが呼ばれることになる。

`callCustomCreator`メソッドとこの`createSessionDriver`メソッドとを見比べてみると、前者は`Auth::extend`メソッドに渡したコールバックを呼び出す、つまり変更前のコードであれば単にカスタムガードクラスを生成して返すだけなのに対し、後者は

```php
    public function createSessionDriver($name, $config)
    {
        $provider = $this->createUserProvider($config['provider'] ?? null);

        $guard = new SessionGuard($name, $provider, $this->app['session.store']);

        // When using the remember me functionality of the authentication services we
        // will need to be set the encryption instance of the guard, which allows
        // secure, encrypted cookie values to get generated for those cookies.
        if (method_exists($guard, 'setCookieJar')) {
            $guard->setCookieJar($this->app['cookie']);
        }

        if (method_exists($guard, 'setDispatcher')) {
            $guard->setDispatcher($this->app['events']);
        }

        if (method_exists($guard, 'setRequest')) {
            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
        }

        return $guard;
    }
```
のように記述されており、すなわち変更後のコードと同様生成したインスタンスに対して三つのメソッドを呼び出している。（というより、変更後のコードは`AuthManager`のこのメソッドをコピーしただけである。）

三つのメソッドの中身は追いかけてはいないが、名前から察するに問題の **Cookie jar has not been set** エラーは`setCookieJar`メソッドの呼び出しに起因することは明らかである。

#まとめ
`SessionGuard`のカスタム時に陥りやすいと思われるエラーの対処方法について解説した。おそらく`TokenGurad`のカスタムも同じようなことが言えると思うが、どのようなエラーが出るのかは不明、対処方法は同じ（`createTokenDriver`の処理を参考にすればよい。）と思われる。

#参考資料
- [Laravel Document: Laravel6.x 認証 - カスタムガードの追加](https://readouble.com/laravel/6.x/ja/authentication.html?header=%25E3%2582%25AB%25E3%2582%25B9%25E3%2582%25BF%25E3%2583%25A0%25E3%2582%25AC%25E3%2583%25BC%25E3%2583%2589%25E3%2581%25AE%25E8%25BF%25BD%25E5%258A%25A0)
- [Laravel API](https://laravel.com/api/6.x/)