🔑

LaravelのAuth Facadeのuser()は一体何をしているのか?が気になりすぎて夜しか眠れなかったのでコードを読んでみた。

2024/12/05に公開

はじめに

こんにちは、まさき。です。
この記事はNEアドベントカレンダー5日目の記事になります。
えっ?昨日もこの人の記事見た?ありがとうございます!

突然ですが、LaravelにはFacade(ファサード)という仕組みがあり、認証系はAuth Facadeを呼び出すことで簡単に実装することができます。
たとえば、Auth::user()を呼び出すとログインしているユーザーを取得することができます。
では、なぜこれでログイン中のユーザーが取得できるか考えたことはありますか?私は考えたことがなかったので、今回はコードを追いながらその仕組みを解明していきます!

本記事を読むことで、Auth::user()がどのように動作しているかを理解することができます。また、その過程でFacadeがどのように特定のクラスのメソッドを呼び出しているのかにも触れています。

きっかけ

私は、副業でPHPでのWebアプリケーション開発を教えています。誰かに教えることで自分の理解が深まったり、教えるためにはもっと知ってなきゃいけないなーとか、この人に教わりたいと思ってもらえるようなエンジニアにならなきゃなーとか思うようになったりと、思わぬ副産物が得られています。

先日、PHPのWebフレームワークであるLaravelを教えていて、Auth Facadeでログインしているユーザーを取得する処理の説明に詰まってしまいました。
この機会に、今までなんとなく使っていたAuth Facadeをコードを読んで理解してみることにしました。

コードリーディング

前提

今回のコードリーディングの目的は、Auth::user()の実装を理解することです。したがって、それ以外のことはあまり深追いしません。
今回は、Laravel 10.x系のコードを読みます。

Laravel

Laravelは、PHPのWebアプリケーションフレームワークです。
laravel/frameworkにコードがあるので、これをコードリーディングしていきます。

github.devでVSCodeで読めます!
https://github.dev/laravel/laravel/tree/10.x
https://github.dev/laravel/framework/blob/10.x/src/Illuminate/Support/Facades/Auth.php

Facadeとは?

【Laravel】ファサードとは?何が便利か?どういう仕組みか?

ファサードとは、
クラスをインスタンス化しなくてもstaticメソッドのように
メソッドを実行できるようにしてくれる機能のこと。

Facadeは、Laravelのサービスコンテナを通じて基盤クラスにアクセスするためのシンプルなインターフェースです。たとえば、DB::table('users')->get()でデータベースにアクセスできるのも、DBというFacadeが背後でサービスコンテナからデータベース接続を解決しているおかげです。この仕組みを理解することで、Facadeを使った簡潔なコードの背景にある強力な設計思想を学ぶことができます。

Auth::user()を紐解く その1 Facade

// ログインしているユーザーを取得する
$login_users = Auth::user();

このような処理でログインしているユーザーをどうやって取得しているのかを理解するためにコードを読んでいきます。

まず、Illuminate\Support\Facades\Authを見ていきます。
laravel/framework/src/Illuminate/Support/Facades/Auth.php

https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Support/Facades/Auth.php#L27-L28
とりあえず、AuthFacadeのファイルをuser()で検索してみると上記のような記述が見つかります。が、肝心のメソッドが見つかりません。

AuthクラスはFacadeを継承していて、getFacadeAccessorが以下のようにオーバーライドされていました。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Support/Facades/Auth.php#L72-L81

そこで、Facadeクラスの方を見てみます。このファイルの中でまず、user()がないかを見てみますが、やはり見当たりません。
が、一番下にマジックメソッド __callStaticが実装されてました。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Support/Facades/Facade.php#L342-L361

__callStatic() は、 アクセス不能メソッドをstatic メソッドとして呼び出した場合に起動します。
引数 $name は、 コールしようとしたメソッドの名前です。 引数 $arguments は配列で、メソッド $name に渡そうとしたパラメータが格納されます。
https://www.php.net/manual/ja/language.oop5.overloading.php#object.callstatic

AuthにもFacadeにもuser()というメソッドは定義されておらず、まさにアクセス不能なuserメソッドをStaticメソッドとして呼び出しています。(これに気づくのにだいぶ時間使った、、、のはココだけの話)

https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Support/Facades/Facade.php#L202-L211

https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Support/Facades/Facade.php#L224-L244

resolveFacadeInstanceの中でddでデバッグしたところ、static::$app[$name];が呼ばれてそうということがわかりました。$nameは、getFacadeAccessorで'auth'であることがわかります。

static::$appってなんだ?とコードジャンプすると、Containerインスタンスへのヘルパーメソッドだったことがわかります。引数の指定もないので、このContainerインスタンスは、\Illuminate\Foundation\Applicationであることがわかりました。(実際にtinkerで確認できた)
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Foundation/helpers.php#L108-L126

ちょっと邪道ですが、このApplication.phpの中で'auth'で検索してみると、以下のようにregisterCoreContainerAliasesの中で見つかります。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Foundation/Application.php#L1585-L1637

以下のメソッドでは、authというキーでAuthManager::classを呼び出せるようにエイリアスを設定しています。

    /**
     * Alias a type to a different name.
     *
     * @param  string  $abstract
     * @param  string  $alias
     * @return void
     *
     * @throws \LogicException
     */
    public function alias($abstract, $alias)
    {
        if ($alias === $abstract) {
            throw new LogicException("[{$abstract}] is aliased to itself.");
        }

        $this->aliases[$alias] = $abstract; // '\Illuminate\Auth\AuthManager::class' => 'auth' 

        $this->abstractAliases[$abstract][] = $alias; // 'auth' => [\Illuminate\Auth\AuthManager::class] 
    }

Applicationのコンストラクタでこのメソッドが呼ばれていることも確認できました。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Foundation/Application.php#L211-L226

ということで、AuthFacadeの正体は、\Illuminate\Auth\AuthManagerであることが判明しました。
つまり、Facadeでメソッドを呼び出しをすると、Laravelのサービスコンテナ(app)を通じて、AuthManagerのような実装クラスを取得し、そのクラスのメソッドを呼び出す仕組みになっています。

'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],

で、これは、LaravelのFacadeの説明のところに書いてあるものと一致します。コード読む必要なかった。
FacadeClass
https://laravel.com/docs/10.x/facades#facade-class-reference

Auth::user()を紐解く その2 AuthManager

https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php
AuthManagerクラスにきっとuser()はいるはず!ということでuser()を探しましたが、やはり見つかりませんでした。

そして案の定、マジックメソッド__call()が定義されていました。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php#L332-L343

__call() は、 アクセス不能メソッドをオブジェクトのコンテキストで実行したときに起動します。https://www.php.net/manual/ja/language.oop5.overloading.php#object.call

https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php#L60-L72

引数なしでguard()がコールされているのでgetDefaultDriver()から$nameを取得します。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php#L192-L201

ちなみに、取得される値は以下の通り。App\Models\User::classがここで登場することがわかります。
https://github.com/laravel/laravel/blob/7effeb99ec1568f006ed95e60d3e7dfeed1b8160/config/auth.php#L16-L20
https://github.com/laravel/laravel/blob/7effeb99ec1568f006ed95e60d3e7dfeed1b8160/config/auth.php#L38-L44
https://github.com/laravel/laravel/blob/7effeb99ec1568f006ed95e60d3e7dfeed1b8160/config/auth.php#L62-L73

特にCustomCreatorsを定義してないので、configのdriverに従い、createSessionDriverメソッドを呼び出します。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php#L73-L103

guardの正体がSessionGuardであることが判明しました。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/AuthManager.php#L116-L154

Auth::user()を紐解く その3 SessionGuard

user()はSessionGuardに実装されていたようです。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/SessionGuard.php#L147-L188

このメソッドに渡ってきたLaravelのSessionをdd()で確認したものが以下の図になります。これによるとlogin_xxxxx => 1がattributesにあることが確認できます。この1こそが、現在ログイン中のuserのidであり、最終的に返却されるApp\Models\Userのidです。
Session

SessionGuardインスタンスに渡されているProviderは、AuthManagerのcreateSessionDriverで、以下のcreateUserProvider()によって返却されています。configにある通り、driverはEloquentで、modelはApp\Models\Userが指定されています。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/CreatesUserProviders.php#L16-L44
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/CreatesUserProviders.php#L71-L81

これにより、以下のretrieveByIdより、Userモデルが取得されるわけですね。
https://github.com/laravel/framework/blob/7db7ea950d3df663391718730f4490a4e189234a/src/Illuminate/Auth/EloquentUserProvider.php#L47-L61

Auth::user()を紐解く まとめ

  1. Auth::user()呼び出し → Facadeの__callStaticに転送
  2. getFacadeAccessorでauthを取得 → サービスコンテナからAuthManagerを解決
  3. AuthManagerがSessionGuardを生成 → SessionGuardのuserメソッドを実行
  4. SessionGuardがセッション情報を使い、EloquentUserProviderからUserモデルを取得

Auth::user()でログイン中のユーザーが取得できるのは、セッション情報にログイン中のuser_idが保持されており、そのuser_idからApp\Models\Userを取得しているからと言えそうですね。

まとめ

この記事では、LaravelのAuth Facadeのコードリーディングを行いました。最終的に、Auth::user()がいかにしてAuthenticatableなApp\Models\Userを返却するのかをコードを提示しながら説明しました。

LaravelのFacadeはマジックメソッドを活用することで、Staticメソッドとして気軽に呼び出せるという利点を持ちつつ、内部的には動的に依存関係を解決することができ、高い拡張性やテストのしやすさを実現していると考えられます。

Facadeの仕組みを理解することにより、オリジナルなFacadeを定義できるようになったり、configを通じてAuthの挙動を変更したりすることができるようになります。

この記事がFacadeやAuthFacadeを理解する一助になれば幸いです。

NE株式会社の開発ブログ

Discussion