Laravelでのマルチ認証でログイン後のリダイレクト先をroleで変更する方法と認証したユーザー情報にJOINしたテーブルのフィールドを追加する方法

# 概要
セッション衝突問題とかは置いておいてシンプルにroleを使ってログイン先を変更する話です。
Laravelデフォルトの認証を使います。

# UsersテーブルにRoleを追加
migrationファイルでroleを追加しておきまましょう。
roleは数値でもenumでも良いです。
今回は admin,teacher,parentを用意したと仮定して進めます。

# redirectTo()
Auth\LoginControllerにredirectToメソッドを定義してあげます。

そうすると既に定義してあるprotected $redirectToではなくredirectTo()の戻り地でredirect先が決定するようになります。

この辺りのことは\Illuminate\Foundation\Auth\RedirectsUsers.phpに書いてあります。

```php
trait RedirectsUsers
{
    /**
     * Get the post register / login redirect path.
     *
     * @return string
     */
    public function redirectPath()
    {
        if (method_exists($this, 'redirectTo')) {
            return $this->redirectTo();
        }

        return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
    }
}
```
traitがtrait先のコントローラーでメソッドかプロパティの定義を期待しているのはちょっと変わった実装ですね。


# LoginControllerにredirectTo()を実装


```php
public function redirectTo(){
    $role = $this->guard()->user()->role;
    if($role === 'admin'){
      return '/admin';
    }
    if($role === 'teacher'){
      return '/teacher';
    }
    if($role === 'parent'){
      return '/parent';
    }
    return '/';
}
```
roleに当てはまらなかったら例外投げても良いのですが今回は'/'にします。

# まとめ
これでログイン後のリダイレクト先を変更することが出来ました。
role別に認証や認可でアクセス先を制限するのは別な実装が必要です。

# Authで取得するuser()にJOINでパラメータを追加する場合

Provider/AuthUserProviderを作成

とりあえず下で実装するので空で良い

Provider/AuthServiceProviderに以下を追記

```php
public function register() {
	\Auth::provider('auth_ex', function($app){
		$model = $app['config']['auth.providers.users.model'];
		return new AuthUserProvider($app['hash'], $model);
	});
}

```

config/auth.phpを変更

```php
    'providers' => [
        'users' => [
            'driver' => 'auth_ex',
            'model' => App\User::class,
        ],
    ]
```

Provider/AuthUserProviderを実装

```php
class AuthUserProvider extends EloquentUserProvider{
	public function retrieveById($identifier) {
		$user = $this->createModel()->newQuery()->select(['users.role'])->where('users.id', $identifier)->first();
		if(empty($user)){
			$user = parent::retrieveById($identifier);
		}
		if($user->role == 'parent'){
			$result = $this->createModel()->newQuery()
				->leftJoin('parents', 'parents.user_id', '=', 'users.id')
				->select(['users.id', 'users.email', 'users.role', 'users.remember_token',
					'parents.id as parent_id', 'parents.classroom_code as classroom_code', 'parents.color as color'])
				->find($identifier);
			return $result;
		}
		if($user->role == 'teacher'){
			$result = $this->createModel()->newQuery()
				->leftJoin('teachers', 'teachers.user_id', '=', 'users.id')
				->select(['users.id', 'users.email', 'users.role', 'users.remember_token',
					'teachers.id as teacher_id', 'teachers.classroom_id as classroom_id', 'teachers.color as color', 'teachers.name as name'])
				->find($identifier);
			return $result;
		}
		return parent::retrieveById($identifier);
	}
}
```

retrieveById()をオーバーライドする形で内容を変えます。parentのケースとteaecherのケースではjoinを使って別テーブルのパラメータを取得してきます。こうすることでAuth::user()とか認証ユーザーの情報を取得するときにjoioした状態のテーブルのフィールドを加えることが出来ます。

retrieveById()はSessionGuardのuser()で主に利用されます。
デフォルトだとAuth::user()とやるとSessionGuardのuser()が呼び出されます。

# Seessionが一度切れてCookieから読みに行くときにエラーになる問題
上記のようにretrieveById()を使ってフィールドを加える場合、特定の条件下でretrieveById()が呼び出されず、joinしたフィールドを呼び出してエラーになってしまうときがあります。

SessionGuardのuser()のソースです。

```php
public function user()
{
	if ($this->loggedOut) {
		return;
	}

	if (! is_null($this->user)) {
		return $this->user;
	}

	$id = $this->session->get($this->getName());

	if (! is_null($id)) {
		if ($this->user = $this->provider->retrieveById($id)) {
			$this->fireAuthenticatedEvent($this->user);
		}
	}

	$recaller = $this->recaller();

	if (is_null($this->user) && ! is_null($recaller)) {
		$this->user = $this->userFromRecaller($recaller);

		if ($this->user) {
			$this->updateSession($this->user->getAuthIdentifier());

			$this->fireLoginEvent($this->user, true);
		}
	}

	return $this->user;
}

```

retrieveById()を呼び出さずユーザーをreturnしている箇所があります。2つめのif分です。
節のタイトル通り、Sessionが切れてCookieから読み直す場合、そこの分岐にたどり着く場合があります。

Authファサードのguard()を呼び出したときCookieから読み取りユーザー情報をセットされてしまいます。
そのユーザー情報にはjoinしたフィールドは含まれません。

ではどうしたら良いか？


ちなみにAuthファサードのguard()は Illuminate\Auth\AuthManagerクラスのメソッドを呼び出しています。


### SessionGuardを継承したCustomGuardを用意する。


#### 自作のGuardを作る

user()の内容を変えます。
以下、CustomGuardの実装です。

```php

class CustomGuard extends SessionGuard {
	private function retrieve(){
		$id = $this->session->get($this->getName());
		if (! is_null($id)) {
			if ($this->user = $this->provider->retrieveById($id)) {
				$this->fireAuthenticatedEvent($this->user);
			}
		}
		return $this->user;
	}
	public function user()
	{
		if ($this->loggedOut) {
			return;
		}

		if (! is_null($this->user)) {
			if($this->user->role === 'parent'){
				if($this->user->classroom_code === null){
					$this->user = $this->retrieve();
				}
			}
			if($this->user->role === 'teacher'){
				if($this->user->classroom_id === null){
					$this->user = $this->retrieve();
				}
			}
			return $this->user;
		}

		$id = $this->session->get($this->getName());

		if (! is_null($id)) {
			if ($this->user = $this->provider->retrieveById($id)) {
				$this->fireAuthenticatedEvent($this->user);
			}
		}

		$recaller = $this->recaller();

		if (is_null($this->user) && ! is_null($recaller)) {
			$this->user = $this->userFromRecaller($recaller);

			if ($this->user) {
				$this->updateSession($this->user->getAuthIdentifier());

				$this->fireLoginEvent($this->user, true);
			}
		}

		return $this->user;
	}
}
```

roleで分岐を行い、join対象のフィールドがnullであったらretrieveById()を呼び出してユーザー情報を再加工しています。

次にSessionGuardからCustomGuardに切り替える設定を行います。

AuthServiceProviderクラスのboot()でAuth::extendをします。

```php
public function boot()
{
	$this->registerPolicies();

	Auth::extend('custom', function($app, $name, array $config) {
		$guard = new CustomGuard($name,Auth::createUserProvider($config['provider']), $app["session.store"]);
		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;
	});
}

```

まず、前提知識です。自作Guardの作成は２種類方法があります。
上記のextend()を使ったやり方とAuthManagerを継承してメソッドを追加する方法です。


少しソースの説明をします。

CustomGuardを生成した後の３つのmethod_existsですが通常のSessionGuardを利用するときに呼び出されるものです。
AuthManagerクラスのcreateSessionDriver()に記述されています。

createSessionDriver()の呼び出し方が可変関数を使って呼び出されています。

このような感じです。

```php
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
    return $this->{$driverMethod}($name, $config);
}
```
extendを使うとcallCustomCreator()が先に呼び出されてその行にたどり着けません。

```php
if (isset($this->customCreators[$config['driver']])) {
	return $this->callCustomCreator($name, $config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
    return $this->{$driverMethod}($name, $config);
}
```

このような順番になっているためextendのクロージャーを呼び出してメソッドから抜け出してしまいます。
そのため、createSessionDriver()で記述してある、3つのmethod_existsが実行されなくなります。

CustomGuardの生成後にすぐにreturnしていないのはそのためです。


#### 設定ファイル変更
config/auth.phpを変更します

```php
'guards' => [
    'web' => [
        'driver' => 'custom',
        'provider' => 'users',
    ],
```


これでCustomGuardが代わりに使われるようになります。
目的達成です。



# 参考
>Laravelで複数テーブルを使ったログイン認証を実装する
>https://qiita.com/sakuraya/items/248d1bed5857c69e44c4
>【Laravel】Laravel5.5でユーザーと管理者の認証（MultiAuth）を実装する（マルチログイン実装時のセッション分割対応）
>https://qiita.com/sola-msr/items/65634826bcedf3ea4ca4
>やはりお前らのMulti-Auth は間違っている
>https://qiita.com/h1na/items/bbe4d17af21860de1930
>Laravel5.5　カスタムGuardでログイン時にEgerLoadする
>https://nextat.co.jp/staff/archives/195