この記事はLaravel Advent Calendar 2018 - Qiitaの21日目の記事です。
はじめに
みなさん、アプリケーションのレイヤー化してますか?
してますね?
Eloquent使ってますか?
使ってますね?
レイヤードアーキテクチャやクリーンアーキテクチャなどを学んでいると、こんなこと思いませんか?
Auth::user()でEntity返してえ〜〜
ということで
今日は Auth::user()
で独自クラスを返してみましょう!
※この記事中ではアーキテクチャの話は一切出てきません
目的
Auth::user()
の返り値を独自のクラスに変更することを通じてLaravelのことを知る
やること
-
Illuminate\Contracts\Auth\Authenticatable
を実装 -
Illuminate\Contracts\Auth\UserProvider
を実装 - 実装したクラスをProviderで登録
- configいじる
環境
- PHP7.2
- Laravel 5.7
- php artisan make:auth 済み
Auth::user()の挙動を調べる
と、その前に Auth::user()
は何をしているのか、調べてみましょう
/**
* 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->provider
が Illuminate\Contracts\Auth\UserProvider
のことで
$this->user
が Illuminate\Contracts\Auth\Authenticatable
なのです!
それでは始めましょう!
Illuminate\Contracts\Auth\Authenticatableを実装する
Authenticatableとは何なのか、確認してみましょう。
<?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
にします。
mkdir app/Auth
touch app/Auth/User.php
そして、作成したファイルにUserクラスを作ります。
<?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を実装する
<?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
の下にファイルを作ります
touch 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\EloquentUserProvider
や Illuminate\Auth\DatabaseUserProvider
の retrieveByCredentials
は
もうちょっと色々な処理をやっていますが、この記事の趣旨からすると少しノイズに感じたので多少シンプルにしてあります。
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します
use App\Auth\UserProvider;
boot
メソッドに追記します
/**
* 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');
ログイン後にアクセスしてみます
やったー!
おわりに
色々割愛したつもりでも、思ったより長くなってしまいました。
が、いかがだったでしょうか?
フレームワークの挙動を変えるのも意外と簡単ですよね。
Laravelのこういうところが好きです。
今回は説明の都合上、DRYになってなかったりしています。
みなさんが実際に実装する際は清いコードに書き直してください。
また、Eloquentを使わないという選択で失うものはとても多いので
Eloquentと仲良くできる要件なら仲良くした方が良いと思います!
宣伝 Laravel JP Conferenceやります!
だいたい2ヶ月後くらいの2019/2/16にLaravel JP Conferenceが開催されます!!
僕もスタッフとして微力ながらお手伝いさせていただいております!
面白いカンファレンスになるようがんばっておりますので、みなさま是非ご来場ください!!
もうそろそろチケット販売なども開始しますよー!!
https://conference2019.laravel.jp/
Twitter
おわり