👻

Laravel の Auth Facade の仕組みを理解する

2023/09/16に公開

先日、次の記事を投稿した。

https://zenn.dev/macloud/articles/1fbe600f8e72fd

この記事中に

Auth::login() を呼び出すと \Illuminate\Auth\SessionGuard::login() が呼び出される。

とあるが、公式ドキュメントの Facade Class Reference を読むと Auth ファサードは Illuminate\Auth\AuthManager に基づいているとあり、話が違うじゃないと思われるかもしれない。結論を言うと AuthManager を通して SessionGuard のメソッドを呼び出すのだが、本記事ではこれがどのように実現されているのか、 Laravel のソースコードを読んで理解する。

環境

  • PHP 8.2
  • Laravel 10.20.0

前提

公式ドキュメントの Facade のページの内容を前提知識とする。

https://laravel.com/docs/10.x/facades

Laravel のソースコードを読む

今回は公式ドキュメントに従って Laravel Sail の環境を構築し、 Laravel Breeze を使って認証機能の土台を追加した。

Auth ファサードと AuthManager の関係

まずは Auth ファサードと AuthManager の関係について理解するため、サービスコンテナへのバインディングの処理について調べていく。このセクションの内容は他のファサードクラスの仕組みを理解する上でも利用できる。

Auth ファサードは次のようになっている。

<?php

namespace Illuminate\Support\Facades;

// ...(中略)...

class Auth extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'auth';
    }

    // ...(中略)...
}

ファサードは基本的に getFacadeAccessor() で返却された文字列にバインドされたオブジェクトをサービスコンテナに解決させる。 Auth ファサードの場合、サービスコンテナは auth という名前でバインドされたオブジェクトがあれば、それを返す。このバインディングはサービスプロバイダを通して登録されており、それは config/app.php を辿って見つけることができる。

<?php

// ...(中略)...

return [
    // ...(中略)...

    'providers' => ServiceProvider::defaultProviders()->merge([
        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ])->toArray(),

    // ...(中略)...
];

ここで ServiceProvider::defaultProviders()DefaultProviders のインスタンスを単に生成して返却する。

<?php

namespace Illuminate\Support;

class DefaultProviders
{
    // ...(中略)...

    /**
     * Create a new default provider collection.
     *
     * @return void
     */
    public function __construct(?array $providers = null)
    {
        $this->providers = $providers ?: [
            \Illuminate\Auth\AuthServiceProvider::class,
            \Illuminate\Broadcasting\BroadcastServiceProvider::class,
            \Illuminate\Bus\BusServiceProvider::class,
            \Illuminate\Cache\CacheServiceProvider::class,
            \Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
            \Illuminate\Cookie\CookieServiceProvider::class,
            \Illuminate\Database\DatabaseServiceProvider::class,
            \Illuminate\Encryption\EncryptionServiceProvider::class,
            \Illuminate\Filesystem\FilesystemServiceProvider::class,
            \Illuminate\Foundation\Providers\FoundationServiceProvider::class,
            \Illuminate\Hashing\HashServiceProvider::class,
            \Illuminate\Mail\MailServiceProvider::class,
            \Illuminate\Notifications\NotificationServiceProvider::class,
            \Illuminate\Pagination\PaginationServiceProvider::class,
            \Illuminate\Pipeline\PipelineServiceProvider::class,
            \Illuminate\Queue\QueueServiceProvider::class,
            \Illuminate\Redis\RedisServiceProvider::class,
            \Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
            \Illuminate\Session\SessionServiceProvider::class,
            \Illuminate\Translation\TranslationServiceProvider::class,
            \Illuminate\Validation\ValidationServiceProvider::class,
            \Illuminate\View\ViewServiceProvider::class,
        ];
    }

    // ...(中略)...
}

さらに \Illuminate\Auth\AuthServiceProvider を見ると、 auth という名前でバインディングする処理を見つけることができる。

<?php

namespace Illuminate\Auth;

// ...(中略)...

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerAuthenticator();
        $this->registerUserResolver();
        $this->registerAccessGate();
        $this->registerRequirePassword();
        $this->registerRequestRebindHandler();
        $this->registerEventRebindHandler();
    }

    /**
     * Register the authenticator services.
     *
     * @return void
     */
    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', fn ($app) => new AuthManager($app));

        $this->app->singleton('auth.driver', fn ($app) => $app['auth']->guard());
    }

    // ...(中略)...
}

これを見ると、 auth という名前でバインディングされているのは Illuminate\Auth\AuthManager のインスタンスであることが分かる。

Auth ファサードと SessionGuard の関係

次に、 Auth::login() を呼び出すと \Illuminate\Auth\SessionGuard::login() が呼び出される点について調べる。 AuthManager のコードを読むと分かるが、このクラスには login() メソッドが定義されていない。そのため、 PHP のマジックメソッドである AuthManager::__call() メソッドが呼び出される。

<?php

namespace Illuminate\Auth;

// ...(中略)...

class AuthManager implements FactoryContract
{
    // ...(中略)...

    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->guard()->{$method}(...$parameters);
    }
}

このメソッドではまず guard() が呼び出され、さらにその返り値のインスタンスが持つ $method という名前のメソッドが呼び出されることが分かる。

/**
 * Attempt to get the guard from the local cache.
 *
 * @param  string|null  $name
 * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
 */
public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

guard() メソッド内では getDefaultDriver() によってガード名が取得されるが、これはデフォルトの config/auth.php の設定であれば web となり、 resolve() メソッドでこのガードを解決しようとする。

/**
 * Resolve the given guard.
 *
 * @param  string  $name
 * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
 *
 * @throws \InvalidArgumentException
 */
protected function resolve($name)
{
    $config = $this->getConfig($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
    }

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

    throw new InvalidArgumentException(
        "Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
    );
}

resolve() メソッド内では 'create'.ucfirst($config['driver']).'Driver' によって作成された名前のメソッドを呼び出して返り値を返している。 web ガードのドライバは session であるため、 createSessionDriver という文字列が組み立てられ、このメソッドが呼び出される。

/**
 * Create a session based authentication guard.
 *
 * @param  string  $name
 * @param  array  $config
 * @return \Illuminate\Auth\SessionGuard
 */
public function createSessionDriver($name, $config)
{
    $provider = $this->createUserProvider($config['provider'] ?? null);

    $guard = new SessionGuard(
        $name,
        $provider,
        $this->app['session.store'],
    );

    // ...(中略)...

    return $guard;
}

ここで SessionGuard のインスタンスを生成して返却する処理を見つけることができる。返却されたインスタンスは前述した __call() メソッド内の guard() の返り値になるため、 SessionGuard::login() メソッドが呼び出されることになる。

まとめ

サマリーすると次のようになる。

  • Auth ファサードは Illuminate\Auth\AuthManager に基づいている
  • login() メソッドのように Illuminate\Auth\AuthManager に定義が存在しない場合、 __call() メソッドが呼び出される
  • __call() メソッド内で認証ガードが解決されて SessionGuard インスタンスが生成され、 SessionGuard::login() メソッドが呼び出される。

Discussion