Auth::user()で独自クラスを返却する

この記事は[Laravel Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/laravel)の21日目の記事です。

# はじめに

みなさん、アプリケーションのレイヤー化してますか？

してますね？

Eloquent使ってますか？

使ってますね？

レイヤードアーキテクチャやクリーンアーキテクチャなどを学んでいると、こんなこと思いませんか？


__Auth::user()でEntity返してえ〜〜__

ということで
今日は `Auth::user()` で独自クラスを返してみましょう！

※この記事中ではアーキテクチャの話は一切出てきません

## 目的
`Auth::user()` の返り値を独自のクラスに変更することを通じてLaravelのことを知る


## やること

1. `Illuminate\Contracts\Auth\Authenticatable` を実装
1. `Illuminate\Contracts\Auth\UserProvider` を実装
1. 実装したクラスをProviderで登録
1. configいじる

## 環境
- PHP7.2
- Laravel 5.7
- php artisan make:auth 済み
 


# Auth::user()の挙動を調べる
と、その前に `Auth::user()` は何をしているのか、調べてみましょう  

```php:vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if ($this->loggedOut) {
            return;
        }

        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }

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

        // First we will try to load the user using the identifier in the session if
        // one exists. Otherwise we will check for a "remember me" cookie in this
        // request, and if one exists, attempt to retrieve the user using that.
        if (! is_null($id)) {
            if ($this->user = $this->provider->retrieveById($id)) {
                $this->fireAuthenticatedEvent($this->user);
            }
        }

        // If the user is null, but we decrypt a "recaller" cookie we can attempt to
        // pull the user data on that cookie which serves as a remember cookie on
        // the application. Once we have a user we can return it to the caller.
        $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;
    }
```
なんかなげえ！
ざっくり流れを追いましょう！

```php
if ($this->loggedOut) {
    return;
}
```
ログアウトしたことになっていたらnullを返して終了してますね。

```php
if (! is_null($this->user)) {
    return $this->user;
}
```
userという値がnull以外であれば、その値をそのまま返す

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

if (! is_null($id)) {
    if ($this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }
}
```
セッションからidを取得することが出来れば  
`$this->provider->retrieveById($id)` でproviderがユーザーを取得して来て `$this->user` にセットしてる感じ

```php
$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);
    }
}
```
細かい説明は割愛しつつ・・・。
`$this->userFromRecaller($recaller)` はクッキーにログイン情報のトークンが存在してたら
その情報を元にユーザーを探してきて `$this->user` にセットしているようです


```php
return $this->user;
```
それで最終的にセッションかクッキーのどっちかからユーザーを取得できていればそれを返して
何も取得できていなければnullですね


以上を踏まえると

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

if (! is_null($id)) {
    if ($this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }
}
```
この部分の `$this->provider->retrieveById($id)` を何とかできれば何とかなるんです！！

そして、この `$this->provider` が `Illuminate\Contracts\Auth\UserProvider` のことで  
`$this->user` が `Illuminate\Contracts\Auth\Authenticatable` なのです！

それでは始めましょう！

# Illuminate\Contracts\Auth\Authenticatableを実装する

Authenticatableとは何なのか、確認してみましょう。

```php:vendor/laravel/framework/src/Illuminate/Contracts/Auth/Authenticatable.php
<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName();

    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier();

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword();

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken();

    /**
     * Set the token value for the "remember me" session.
     *
     * @param  string  $value
     * @return void
     */
    public function setRememberToken($value);

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName();
}
```
正体はただのインターフェースですね。  
これらのメソッドを実装したクラスを作成すれば良さそうです。  

そうとわかれば作りましょう！  

## Userクラスを作成する
`app` ディレクトリの下に `Auth` ディレクトリを作成し、そこにクラスを置いていくことにします。  
また、 `Authenticatable` を実装するためのクラスは `App\Auth\User` にします。

```bash:bash
mkdir app/Auth
touch app/Auth/User.php
```

そして、作成したファイルにUserクラスを作ります。

```php:app/Auth/User.php
<?php
namespace App\Auth;

use Illuminate\Contracts\Auth\Authenticatable;

class User implements Authenticatable
{
    private $id;
    private $name;
    private $email;
    private $password;
    private $rememberToken;

    public function __construct(
        int $id,
        string $name,
        string $email,
        string $password,
        ?string $rememberToken
    ) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
        $this->rememberToken = $rememberToken;
    }
}
```

デフォルトのusersテーブルにはもう少し色々なカラムが設定されてましたが、とりあえず必要そうなやつだけ使うことにします。  
最高ですね。  
では、一つ一つメソッドを実装していきます

### getAuthIdentifierName
```php
    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }
```

このメソッドは、ユーザーを一意に特定できるカラム名を返せば良いので、今回は `id` としておきます。  
普通ですね。

### getAuthIdentifier
```php
    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->id;
    }
```

IDの実際の値を返せばいいので、そうします。  
普通です

### getAuthPassword
```php
    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        return $this->password;
    }
```
こちらはパスワードをそのまま返せばいいですね  
普通

### getRememberToken
```php
    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken()
    {
        return $this->rememberToken;
    }
```
`ログイン情報を保持する` ってやつですね、トークンをそのまま返せばよし  
普通〜〜〜〜〜

### getAuthPassword
```php
    /**
     * Set the token value for the "remember me" session.
     *
     * @param  string  $value
     * @return void
     */
    public function setRememberToken($value)
    {
        $this->remember_token = $value;
    }
```
新しいトークンが渡されたときの処理ですね
アァ〜〜〜〜〜

### getRememberTokenName
```php
    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        return 'remember_token';
    }
```
トークンを保存しているカラム名を返せば良しです  
これで終わり！！！！

普通なことをしていたら完成してしまいました  

次行こう！！！

<br>

# Illuminate\Contracts\Auth\UserProviderを実装する  

```php:vendor/laravel/framework/src/Illuminate/Contracts/Auth/UserProvider.php
<?php

namespace Illuminate\Contracts\Auth;

interface UserProvider
{
    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier);

    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @param  mixed   $identifier
     * @param  string  $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token);

    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  string  $token
     * @return void
     */
    public function updateRememberToken(Authenticatable $user, $token);

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials);

    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials);
}
```

これまたインターフェースですね。  
`Auth::user()` の中で使用されていた `retrieveById` というメソッドもバッチリ定義されています。  
  
これが今回の山場です！！！といっても大したことないけど！！

## UserProviderクラスを作る

とりあえず `app/Auth` の下にファイルを作ります  

```bash:bash
touch app/Auth/UserProvider.php
```

ファイルを作ったら、中身はとりあえずこんな感じで。

```php:app/Auth/UserProvider.php
<?php
namespace App\Auth;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Str;

class UserProvider implements \Illuminate\Contracts\Auth\UserProvider
{
    private $conn;
    private $hasher;
    private $table;

    public function __construct(ConnectionInterface $conn, Hasher $hasher, string $table)
    {
        $this->conn = $conn;
        $this->hasher = $hasher;
        $this->table = $table;
    }
}
```

今回はDBからユーザーを引っ張ってくるので、DBとのやりとり用に `ConnectionInterface` を  
パスワードのチェック用に `Hasher` を  
テーブル名もコンストラクタで受けるようにします

よし、1つ1つ実装していくぞ！！！

### retrieveById

```php
    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        $user = $this->conn->table($this->table)->find($identifier);

        if (is_null($user)) {
            return null;
        }

        return new User(
            $user->id,
            $user->name,
            $user->email,
            $user->password,
            $user->remember_token
        );
    }
```

`retrieveById` はidが引数で与えられるので、ユーザーが見つかれば `Authenticatable` を、見つからなければ `null` を返せばいいです

### retrieveByToken

```php
    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @param  mixed   $identifier
     * @param  string  $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token)
    {
        $user = $this->conn->table($this->table)->find($identifier);

        if (is_null($user)
            || empty($user->remember_token)
            || ! hash_equals($user->remember_token, $token)
        ) {
            return null;
        }

        return new User(
            $user->id,
            $user->name,
            $user->email,
            $user->password,
            $user->remember_token
        );
    }
```

idとトークンが引数で与えられるので、ユーザーが見つかった && トークンがマッチしていたら `Authenticatable` を返し、それ以外であれば `null` を返します

### updateRememberToken

```php
    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  string  $token
     * @return void
     */
    public function updateRememberToken(Authenticatable $user, $token)
    {
        $this->conn->table($this->table)
                   ->where($user->getAuthIdentifierName(), $user->getAuthIdentifier())
                   ->update([$user->getRememberTokenName() => $token]);
    }
```

tokenを更新する際の処理を書きます。  
普通にidの一致をwhere句で指定して、remember_tokenを引数のtokenの値に更新しているだけですね


### retrieveByCredentials

```php
    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials)
            || (count($credentials) === 1 && array_key_exists('password', $credentials))
        ) {
            return null;
        }

        $query = $this->conn->table($this->table);

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            $query->where($key, $value);
        }

        $user = $query->first();

        if (is_null($user)) {
            return null;
        }

        return new User(
            $user->id,
            $user->name,
            $user->email,
            $user->password,
            $user->remember_token
        );
    }
```

こちらは渡された配列の値で検索する感じで。
`$credential`にpasswordだけが渡された場合はnullを返却します。そうしないと誰かしらが引っかかるようになってしまうので。
ちなみに `Illuminate\Auth\EloquentUserProvider` や `Illuminate\Auth\DatabaseUserProvider` の `retrieveByCredentials` は
もうちょっと色々な処理をやっていますが、この記事の趣旨からすると少しノイズに感じたので多少シンプルにしてあります。  


### validateCredentials

```php
    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return $this->hasher->check(
            $credentials['password'], $user->getAuthPassword()
        );
    }
```

単純に、平文のパスワードとハッシュ化されているパスワードを比較しているだけですね

これでUserProviderも完成です！！！！！


さー、あともうちょっとです！

# 実装したUserProviderをLaravelに教える

`app/Providers/AuthServiceProvider.php` にちょこっと追記すればokです


UserProviderをuseします


```php:app/Providers/AuthServiceProvider.php
use App\Auth\UserProvider;
```

`boot` メソッドに追記します


```php:app/Providers/AuthServiceProvider.php
    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        // ここから追記
        $this->app['auth']->provider(
            'my_provider',
            function (Application $application, array $config) {
                return new UserProvider(
                    $application['db']->connection(),
                    $application['hash'],
                    $config['table']
                );
            }
        );
    }
```

これでOK！

最後にconfigを書き換えて終わり！！

# configの書き換え

`config/auth.php` を書き換えます

```php

return [
    
    // 省略

    'providers' => [
         'users' => [
             // ここを書き換え
             'driver' => 'my_provider',
             'table' => 'users',
         ],
    ],

    // 省略
];
```

`driver`の値をAuthServiceProviderでLaravelに教えた値に書き換えればokです



# いざ、確認

こんな感じでRoutingして

```php
Route::get('/test', function() {
    $user = \Auth::user();
    // クラス名とインスタンス
    dd(get_class($user), $user);
})->middleware('auth');
```

ログイン後にアクセスしてみます

![dd](https://qiita-image-store.s3.amazonaws.com/0/144502/2bd40cb8-e137-2499-b349-8abef7330e7c.png)


やったー！


# おわりに
色々割愛したつもりでも、思ったより長くなってしまいました。
が、いかがだったでしょうか？
フレームワークの挙動を変えるのも意外と簡単ですよね。
Laravelのこういうところが好きです。

今回は説明の都合上、DRYになってなかったりしています。
みなさんが実際に実装する際は清いコードに書き直してください。

また、Eloquentを使わないという選択で失うものはとても多いので
Eloquentと仲良くできる要件なら仲良くした方が良いと思います！

# 宣伝　Laravel JP Conferenceやります！

だいたい2ヶ月後くらいの2019/2/16に[Laravel JP Conference](https://conference2019.laravel.jp/)が開催されます！！
僕もスタッフとして微力ながらお手伝いさせていただいております！
面白いカンファレンスになるようがんばっておりますので、みなさま是非ご来場ください！！
もうそろそろチケット販売なども開始しますよー！！

https://conference2019.laravel.jp/
[Twitter](https://twitter.com/laraveljpcon)

おわり