⚙️

Laravelのサービスコンテナについて内部構造を理解する(part2 コードリーディング頻出の処理編)

2023/01/16に公開

はじめに

N番煎じですが、自分の理解をまとめるために書き記しておきます。
laravelのコードリーディングをする際にコンテナ絡みでこのconfigとかauthってどうコンテナで登録されてるのだろうと迷子になったのでメモ。

前回

https://zenn.dev/cube/articles/f88c5b6654e729

Part2の目標

laravelのコードリーディングで頻出する、app()->make("keyword")の"keyword"部分に対応する具体的なクラスはどこで登録するか調べる、例として今回はauthとconfigを調べる。

サービスコンテナとは

サービスコンテナ自体の説明は下記がとてもわかりやすいです。
https://qiita.com/minato-naka/items/afa4b930a2afac23261b
https://reffect.co.jp/laravel/laravel-service-container-understand

authの本体

auth()->user()のauth()は何をやっているのでしょうか。
auth()の定義を調べると下記です。

vendor/laravel/framework/src/Illuminate/Foundation/helper.php

use Illuminate\Contracts\Auth\Factory as AuthFactory;
function auth($guard = null)
    {
        if (is_null($guard)) {
            return app(AuthFactory::class);
        }

        return app(AuthFactory::class)->guard($guard);
    }

とあるのでAuthFactory=Auth\Factoryを指定してその中身を調べればよさそうですね。

app()がApplicationを指すことがわかったので、引き続きregisterを見ていきます。

Applicationの_constructor内のregisterCoreContainerAliasesを見ます。

  public function registerCoreContainerAliases()
    {
        foreach ([
           ...
            'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
           ...
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

part1で見たaliasが出てきましたね。

Containerのaliasesは下記のようになります。
aliases[\Illuminate\Auth\AuthManager::class] = "auth"
aliases[\Illuminate\Contracts\Auth\Factory::class] = "auth"

abstractAliases["auth"] = [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class]

なので\Illuminate\Contracts\Auth\Factory::class => "auth"と繋がるので後はauthを登録しているところを探しましょう。
Part1で触れたregisterをたくさんしているところから探せて、AuthServiceProviderのregisterAuthenticatorでやっています。

Illuminate\Auth\AuthServiceProvider.php
protected function registerAuthenticator()
    {
        $this->app->singleton('auth', fn ($app) => new AuthManager($app));

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

なのでauth()はAuthManagerを呼んでいることがわかります。

configの本体

コードリーディングをしていると、$this->app['config']['auth.defaults.guard']みたいなコードに出くわすことがあります。
内容的にconfigファイルを読み込んでApplicationに登録していそうです。
この辺りはどう登録しているか調べましょう。

Applicationのregister系にはセットしているところはなかったので、index.phpまで戻ります。

public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();

このKernel::classはIlluminate\Contracts\Http\Kernel.phpでinterfaceなので本体を探す必要があります。

bootstrap/app.php
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

となるので、先に進むためにはApp\Http\Kernel->handleを見ればいいことになります。
kernel->handleではrouting関連やmiddlewareを扱っているのですが、今回はconfigをセットしているところだけ追っていきます。

Illuminate\Contracts\Http\Kernel.phpはHttpKernelとして下記を継承していて、HttpKernelにhandleメソッドがあります。
use Illuminate\Foundation\Http\Kernel as HttpKernel

Illuminate\Foundation\Http\Kernel.php

public function handle($request)
    {
       ...

        try {
            ...
            $response = $this->sendRequestThroughRouter($request);
        } catch (Throwable $e) {
          ...
        }

       ...

        return $response;
    }
    
protected function sendRequestThroughRouter($request)
    {
        ...

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

sendRequestThroughRouterのbootstrapを見ていきます。
bootstrapの後のreturnでmiddlewareとroutingを扱っていますが今回は省略。

Illuminate\Foundation\Http\Kernel.php

 public function bootstrap()
    {
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }
    }
 protected function bootstrappers()
    {
        return $this->bootstrappers;
    }
 protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];

となっていて$bootsrappersのLoadConfigurationでconfigをロードしていそうです。

$this->appは Illuminate\Foundation\Applicationです。
なのでIlluminate\Foundation\Application->bootstrapWithを見ましょう。

    Illuminate\Foundation\Application.php
    
    public function bootstrapWith(array $bootstrappers)
    {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            ...

            $this->make($bootstrapper)->bootstrap($this);

           ...
        }
    }

となっているので、LoadConfigurationのbootstrapを見ます。

Illuminate\Foundation\Bootstrap\LoadConfiguration.php

public function bootstrap(Application $app)
    {
        $items = [];

        ...
	
        $app->instance('config', $config = new Repository($items));

        if (! isset($loadedFromCache)) {
            $this->loadConfigurationFiles($app, $config);
        }

        ...
    }

ここでapp["config"]とすれば、\Illuminate\Config\Repository::classが返ってくるようになっています。
loadConfigurationFilesでRepositoryの$itemsに値をセットしています。

Illuminate\Foundation\Bootstrap\LoadConfiguration.php

protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
    {
        $files = $this->getConfigurationFiles($app);

        ...

        foreach ($files as $key => $path) {
            $repository->set($key, require $path);
        }
    }
protected function getConfigurationFiles(Application $app)
    {
        $files = [];

        $configPath = realpath($app->configPath());

        foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) {
            $directory = $this->getNestedDirectory($file, $configPath);

            $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
        }

        ksort($files, SORT_NATURAL);

        return $files;
    }

config/以下のphpファイルの情報を取って来て、repositoryの$itemsにセットしています。
例えば、config/app.phpだったら
$repository->set("app",require config/app.phpのフルパス)
config/app.phpのフルパスはrealpathで取得し,requireしているわけですが、ファイルの先頭以外でrequireを使っていて変な感じですね。

requireの返り値は対象のファイルにreturnがあればそれを、なければ1を返します。
ここでapp.phpを見てみると下記のようにreturnがありますね。これがitemsのvalueになるわけです。

config/app.php

return [
     ...
    'name' => env('APP_NAME', 'Laravel'),
     ...
     ];

よってRepositoryの$items["app"] = [
"name" => env('APP_NAME', 'Laravel')
]
という関係です。

実際にアクセスするときはapp["config"]['auth.defaults.guard']となるわけですが、
これはapp[config]がRepositoryで、Repository['auth.defaults.guard']となります。

クラスに対して配列みたいにアクセスしています。
これはクラスがInterfaceであるArrayAccessを実装しているからできることです。
Class["key"]とすると、Class.offsetGet("key")が呼ばれ、
Class["key"] = valueとするとClass.offsetSet(key,value)が呼ばれます。

ArrayAccessの参考
https://qiita.com/ktplato/items/0cc978dffd40dc2ec939

 Illuminate\Config\Repository.php
 
 public function offsetGet($key): mixed
    {
        return $this->get($key);
    }
    
 public function get($key, $default = null)
    {
        if (is_array($key)) {
            return $this->getMany($key);
        }

        return Arr::get($this->items, $key, $default);
    }

Arr::getはドット区切りでアクセスできるようになるメソッドで、
第一引数に$keyをドットで区切って順々にアクセスします。
例えばitems['auth.defaults.guard']だったら、items["auth"]["defaults"]["guard"]という感じです。

おわりに

以上でコードリーディングで頻出する際に結構困るauthとconfigの登録箇所を読みました。
Part1と比べるととても短い分量になりましたが、auth,config以外の他のところも同じように呼んでいけば見つかるはずです。
ちょくちょく読んではは処理内容忘れるので良かったら皆様もメモ代わりに使ってやってください。

Discussion