61
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LaravelAdvent Calendar 2018

Day 21

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

Last updated at Posted at 2018-12-20

この記事はLaravel Advent Calendar 2018 - Qiitaの21日目の記事です。

はじめに

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

してますね?

Eloquent使ってますか?

使ってますね?

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

Auth::user()でEntity返してえ〜〜

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

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

目的

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

やること

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

環境

  • PHP7.2
  • Laravel 5.7
  • php artisan make:auth 済み

Auth::user()の挙動を調べる

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

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;
    }

なんかなげえ!
ざっくり流れを追いましょう!

if ($this->loggedOut) {
    return;
}

ログアウトしたことになっていたらnullを返して終了してますね。

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

userという値がnull以外であれば、その値をそのまま返す

$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 にセットしてる感じ

$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 にセットしているようです

return $this->user;

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

以上を踏まえると

$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->providerIlluminate\Contracts\Auth\UserProvider のことで
$this->userIlluminate\Contracts\Auth\Authenticatable なのです!

それでは始めましょう!

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

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

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
mkdir app/Auth
touch app/Auth/User.php

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

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

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

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

getAuthIdentifier

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

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

getAuthPassword

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        return $this->password;
    }

こちらはパスワードをそのまま返せばいいですね
普通

getRememberToken

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken()
    {
        return $this->rememberToken;
    }

ログイン情報を保持する ってやつですね、トークンをそのまま返せばよし
普通〜〜〜〜〜

getAuthPassword

    /**
     * Set the token value for the "remember me" session.
     *
     * @param  string  $value
     * @return void
     */
    public function setRememberToken($value)
    {
        $this->remember_token = $value;
    }

新しいトークンが渡されたときの処理ですね
アァ〜〜〜〜〜

getRememberTokenName

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        return 'remember_token';
    }

トークンを保存しているカラム名を返せば良しです
これで終わり!!!!

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

次行こう!!!


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

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
touch app/Auth/UserProvider.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

    /**
     * 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

    /**
     * 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

    /**
     * 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

    /**
     * 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\EloquentUserProviderIlluminate\Auth\DatabaseUserProviderretrieveByCredentials
もうちょっと色々な処理をやっていますが、この記事の趣旨からすると少しノイズに感じたので多少シンプルにしてあります。

validateCredentials

    /**
     * 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します

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

boot メソッドに追記します

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 を書き換えます


return [
    
    // 省略

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

    // 省略
];

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

いざ、確認

こんな感じでRoutingして

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

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

dd

やったー!

おわりに

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

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

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

宣伝 Laravel JP Conferenceやります!

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

https://conference2019.laravel.jp/
Twitter

おわり

61
56
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?