🔍

Laravelのサービスコンテナについて内部構造を理解する(part1 コンテナ基本処理と依存解決編)

2023/01/16に公開約19,800字

はじめに

N番煎じですが、自分の理解をまとめるために書き記しておきます。
laravelのコードリーディングをする際にコンテナの動きを知らないとあやふやな部分があったのでメモ

Part1の目標

app()->make("keyword")のapp部分は何なのか理解する

コンテナのinstance,singleton,aliasの違いを理解する

コンテナによる依存解決はどうやって行われているかを調べる

サービスコンテナとは

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

1.app()->make("keyword")のapp部分は何なのか理解する

app()のなかみ

まずappが何なのかを探ると下記のようになっています。

  vendor/laravel/framework/src/Illuminate/Foundation/helper.php
  
  function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }

        return Container::getInstance()->make($abstract, $parameters);
    }
    
 public static function getInstance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }

        return static::$instance;
    }

なので、Container->makeを見ればよさそうですが、
Containerの $instanceはいつセットされているのでしょうか。

これはlaravelがスタートしたところから始めないといけません。

laravelのスタート

まずはブラウザでサイトにアクセスするとpublic以下のindex.phpが読み込まれるのでそこから行きます。

public/index.php

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

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

bootstrap/app.php

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

上記のようになっているので、Illuminate\Foundation\Applicationが具体的なクラスですね。
このApplicationは/Illuminate/Container/Containerを継承しています。

Applicationのコンストラクタを見てみます。

 Illuminate\Foundation\Application.php
 
 public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }

        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

上記のregister系でapp()->make()で呼び出す具体的なクラスを登録しています。

 Illuminate\Foundation\Application.php
 
 protected function registerBaseBindings()
    {
        static::setInstance($this);

        $this->instance('app', $this);

        $this->instance(Container::class, $this);
        $this->singleton(Mix::class);

        $this->singleton(PackageManifest::class, function () {
            return new PackageManifest(
                new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
            );
        });
    }

setInstanceはApplicationではなくContainerクラスにあります。


/Illuminate/Container/Container.php
public static function setInstance(ContainerContract $container = null)
    {
        return static::$instance = $container;
    }

なのでContainerの$instanceはApplicationということになります。
よって、app()はApplicationを見ればよいことがわかります。

setInstanceだけではなく、this->instanceとthis->singletonはクラス登録メソッドで、Applicationではなく、Containerクラスに存在します。

2.コンテナのinstance,bind,singleton,aliasの違いを理解する

app()が何なのか分かったところで、app()->instace達を見ていきましょう。
これらはコンテナへ依存関係を登録するものですが、どんな違いがあるのでしょうか。
Containerクラスを見てみましょう。

instance

まずはinstanceから。

/Illuminate/Container/Container.php

 public function instance($abstract, $instance)
    {
        $this->removeAbstractAlias($abstract);

        $isBound = $this->bound($abstract);

        unset($this->aliases[$abstract]);

        $this->instances[$abstract] = $instance;

        if ($isBound) {
            $this->rebound($abstract);
        }

        return $instance;
    }

処理の本命は this->instances[abstract] = $instance;ですね。
あとはaliasesをunsetしているのでaliasとinstanceは同じkeyは使えなさそうです。

singleton,bind

singleton

/Illuminate/Container/Container.php

public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }
public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);
	
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

      
        if (! $concrete instanceof Closure) {
            ...

            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

         
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

 protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

bindとsingletonの違いはbindの第三引数sharedがsingletonではtrue,bindではfalseになっている点ですね。

singleton,bindを使うとinstancesではなくbindingsに保存されるようです。
dropStaleInstancesを見るとbindはinstances,aliasesでは同じkeyは使えなさそうです。

singletonが何故singletonと名前がついているのかはmakeで実際に呼び出すところで見ましょう。

this->bindings[$abstract] = compact('concrete', 'shared');
とありますが、compactでは下記のように配列を作っています。
[
"concrete" => $concrete,
"shared" => $shared,
]

alias

/Illuminate/Container/Container.php

public function alias($abstract, $alias)
    {
        ...
        $this->aliases[$alias] = $abstract;

        $this->abstractAliases[$abstract][] = $alias;
    }

aliasはただaliasesとabstractAliasesに登録しているだけです。

bind,singleton vs instance

instanceは既存のインスタンス、bind,singletonはClosure,stringのみ
bind,singletonで既存のインスタンスを使うと下記でエラー

 if (! $concrete instanceof Closure) {
            if (! is_string($concrete)) {
                throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
            }

            $concrete = $this->getClosure($abstract, $concrete);
        }

instanceでClosureを入れるとinstances[abstarct]でClosureが返ってくるだけなのでエラーは出ないけど、インスタンスは返ってこないので想定する使い方とは違ってしまいます。

bind vs singleton

bindは毎回違うインスタンスが返って来て、singletonは毎回同じインスタンスが返ってきます。

登録系を見終わったところで、makeでどう登録された奴が解決されるか見ていきましょう。

make

/Illuminate/Container/Container.php

 public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

 protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

        ...

        $concrete = $this->getContextualConcrete($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

        $this->with[] = $parameters;

        if (is_null($concrete)) {
            $concrete = $this->getConcrete($abstract);
        }

       
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }
        
	...
        return $object;
    }

make + instance

instance(TestInterface::class,new Test())と呼んで、
instances[TestInterface::class] = new Test()の状態で、
app()->make(TestInterface::class)と呼んでみましょう。

 protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

        ...
    $concrete = $this->getContextualConcrete($abstract);
        $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
	
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }
    }
    
 public function getAlias($abstract)
    {
        return isset($this->aliases[$abstract])
                    ? $this->getAlias($this->aliases[$abstract])
                    : $abstract;
    }
    
    
  protected function getContextualConcrete($abstract)
    {
        if (! is_null($binding = $this->findInContextualBindings($abstract))) {
            return $binding;
        }
        
	...
        if (empty($this->abstractAliases[$abstract])) {
            return;
        }

        foreach ($this->abstractAliases[$abstract] as $alias) {
            if (! is_null($binding = $this->findInContextualBindings($alias))) {
                return $binding;
            }
        }
    }
    
  protected function findInContextualBindings($abstract)
    {
        return $this->contextual[end($this->buildStack)][$abstract] ?? null;
    }

getAliasは$abstract = TestInterface::classを返します。
getContextualConcreteはnullを返します。(aliasを呼んでない、contextualに値が入ってないので)
needsContextualBuildはfalseです。

今回あらかじめinstaceを呼んでinstancesにセットしているので、
if (isset(this->instances[abstract]) = true && ! $needsContextualBuild) となり、

return this->instances[abstract]となって具体クラスのTestが返ります。

make + bind(singleton)

app()->bind(TestInterface::class,function(){
return new Test();
});
と設定します。
この時、
bindings[TestInterface::class] = [
"concrete" => function(){
return new Test();
},
"shared" => false,
]

 protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

        ...
    $concrete = $this->getContextualConcrete($abstract);
        $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
	
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }
	
	...
    }

abstract,concreteの処理はinstanceの時と同じで、
$abstract=TestInterface::class
$concrete=nullです。
ただthis->instancesは空なので、isset(this->instances[$abstract])はfalseなのでresolveの先の処理に進んで行くことになります。

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        ...

        if (is_null($concrete)) {
            $concrete = $this->getConcrete($abstract);
        }

      
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        ...

        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }
}

 protected function getConcrete($abstract)
    {
        if (isset($this->bindings[$abstract])) {
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;
    }
    
  protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

$concrete=nullなのでgetConcreteよりbindingsから値をとってきます。
今回は function(){return new Test();}ですね。
$concrete = function(){return new Test();}となります。

isBuildableの条件を満たし、$this->buildに進みます。

下記では依存解決をしています。詳しくは3.でやりましょう。

  public function build($concrete)
    {
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        try {
            $reflector = new ReflectionClass($concrete);
        } catch (ReflectionException $e) {
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
        }
	
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();
	
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        try {
            $instances = $this->resolveDependencies($dependencies);
        } catch (BindingResolutionException $e) {
            array_pop($this->buildStack);

            throw $e;
        }

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

今回$concreteはClosureなので下記でreturnされて終わりです。
if ($concrete instanceof Closure) {
return concrete(this, $this->getLastParameterOverride());}

function(){return new Test();}なのでnew Test();ですね。

buildが終わってresolveに戻ると下記のようになっています。

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
   {
       
   $object = new Test();

       ...

       if ($this->isShared($abstract) && ! $needsContextualBuild) {
           $this->instances[$abstract] = $object;
       }
   
   return $object;
  }

  public function isShared($abstract)
   {
       return isset($this->instances[$abstract]) ||
              (isset($this->bindings[$abstract]['shared']) &&
              $this->bindings[$abstract]['shared'] === true);
   }

ここでbindとsingltonの違いが明らかになります。
singletonの場合shared=trueだったので、
this->instances[abstract] = $objectとなります。
instancesに値がある場合、即returnしたのを覚えているでしょうか。
singletonにすれば、instancesに値が入ることで次回以降も毎回同じインスタンスが返ることにな ります。

bindの場合instancesに値が入らないので、次回以降も$this->buildだったりを経由して毎回違うインスタンスをClosureから生成することになります。

ここまでinstance,bind,singletonを見てきました。
ではaliasとは何なのでしょうか?aliasに値を入れてmakeを見てみましょう。

make+alias

例えば、app()->bind("test",function(){
return new Test();
});
と登録して、app()->make("test")だけではなく、app()->make("alias_test")でもTestを呼びたくなったとします。

そんな時にapp()->alias("test","alias_test")とすれば上記が可能になります。コードを見てみましょう。

/Illuminate/Container/Container.php

public function alias($abstract, $alias)
    {
        ...
        $this->aliases[$alias] = $abstract;

        $this->abstractAliases[$abstract][] = $alias;
    }

となっているので冒頭の操作をしたとすると、
bindings["test"] = function(){return new Test();}
aliases["alias_test"] = "test"
abstractAliases["test"] = ["alias_test"]
となります。

ここでmake("alias_test")を呼んで上手くnew Testまでつながるかを試します。

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

     ...
	
    }
    
   public function getAlias($abstract)
    {
        return isset($this->aliases[$abstract])
                    ? $this->getAlias($this->aliases[$abstract])
                    : $abstract;
    }

aliases["alias_test"] = "test"だったので、
$this->getAlias("alias_test")から $this->getAlias("test")を呼ぶことになります。
aliasesに"test"はないので、結果としてgetAliaseからはtestが返ります。
あとはbindで見た処理と同じ流れですね。

3.コンテナによる依存解決はどうやって行われているかを調べる

2.で放置していたbuildのところを確認しましょう。

  public function build($concrete)
    {
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        try {
            $reflector = new ReflectionClass($concrete);
        } catch (ReflectionException $e) {
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
        }
        
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();
        
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        try {
            $instances = $this->resolveDependencies($dependencies);
        } catch (BindingResolutionException $e) {
            array_pop($this->buildStack);

            throw $e;
        }

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

コード内にちらほらあるようにリフレクションを使っていますね。
リフレクションはphp自体にある機能で、クラスのコンストラクタの情報だったりメソッドの情報をとってこれます。
例として下記のようなクラスを作ってリフレクションを使ってみましょう。

class Test
{
    public function __construct(User $user)
    {   
    }
}
$reflector = new ReflectionClass(Test::class);
$constructor = $reflector->getConstructor();
$dependencies = $constructor->getParameters();

結果

constructor

ReflectionMethod {
  +name: "__construct"
  +class: "App\Models\Test"
  parameters: {
    $user: ReflectionParameter {
      +name: "user"
      position: 0
      typeHint: "App\Models\User"
    }
  }
}

dependencies

array:1 [
  0 => ReflectionParameter {
    +name: "user"
    position: 0
    typeHint: "App\Models\User"
  }
]

以上のようになります。
依存を解決する方法は、
1.リフレクションを使って、クラスのコンストラクタの情報を取る
2.コンストラクタから引数の情報を取る
3.引数がクラスだったら1に戻る...
といった感じでどんどんトップレベルから依存解決していく感じですね。

今回の例でいえばTest -> Userと解決していく感じです。

流れをつかんだところで具体的なbuild部分を見ていきましょう。
app()->bind("test",Test::class)としたとします。

  public function build($concrete)
    {
       ...
        $reflector = new ReflectionClass($concrete);
     

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();
        
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();


        $instances = $this->resolveDependencies($dependencies);
     
        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

reflectorとconstructor、$dependenciesはさっきみた通りですね。
resolveDependenciesを見ましょう。

 protected function resolveDependencies(array $dependencies)
   {
       $results = [];

       foreach ($dependencies as $dependency) {
     
           ...
       
           $result = is_null(Util::getParameterClassName($dependency))
                           ? $this->resolvePrimitive($dependency)
                           : $this->resolveClass($dependency);

           if ($dependency->isVariadic()) {
               $results = array_merge($results, $result);
           } else {
               $results[] = $result;
           }
       }
       return $results;
   }

resolveDependenciesはコンストラクタの引数を順番に依存解決していっています。

$result = is_null(Util::getParameterClassName($dependency))
                           ? $this->resolvePrimitive($dependency)
                           : $this->resolveClass($dependency);

上記はもしコンストラクタの引数がクラスならresolveClass,それ以外ならresolvePrimitiveとしています。

resolvePrimitiveは階層が進むことなく打ち止めなので、resolveClassを見ます。

 protected function resolveClass(ReflectionParameter $parameter)
   {
    return $parameter->isVariadic()
                       ? $this->resolveVariadicClass($parameter)
                       : $this->make(Util::getParameterClassName($parameter));
    }

Variadicは引数が可変かどうかを聞いているので今回はUserなので$this->makeの方ですね。

さて、ここで$this->makeが出てきました。
このmakeで今度はUserの依存解決を行っています。

app()->make("test")
app()->make(User::class)
といった感じですね。

ここでUser::classは具体クラスなのであらかじめbindで登録しておかなくても大丈夫です。
というのももしmakeで具体クラスを指定すると、

 protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

        $concrete = $this->getContextualConcrete($abstract);

        if (is_null($concrete)) {
            $concrete = $this->getConcrete($abstract);
        }

        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
      

        return $object;
    }

当然何の登録も行ってないのでaliasesにもinstancesにもbindingsにも引っ掛かりません。
なのでconcrete=abstract = User::classとすすみisBuildable=trueとなります。
そして$this->buildにUser::classが渡されるわけですが、
リフレクションは具体クラスならOKなのですが、interfaceだったりクラス名じゃない"test"とかはうまく情報を取ってこれません。
interfaceにはコンストラクタがなく、"test"はそもそもファイル名がどこかわかりません。
なのでinterfaceや"test"とかは先に登録して、具体クラスに変換する必要があります。

makeのネストが終わって依存解決が終わるとbuildの下記に戻ってきます。

  public function build($concrete)
    {
        ...

        $instances = $this->resolveDependencies($dependencies);
        
	...

        return $reflector->newInstanceArgs($instances);
    }

newInstanceArgsでリフレクションを使って依存解決済みのパラメータとともに、
コンストラクタを起動します。

これで依存解決の話は終わりです。

おわりに

ここまで長々とコンテナの内部機能について書いてきました。
お付き合いいただきありがとうございます。
省いた部分もありますがコンテナの理解としてはこのくらいで良さそうな気がしています(適当)

Part2ではlaravelのコードリーディング頻出であるapp()->make("config")とかauth()は何やってるのか調べます。
コードリーディングをこれからしようとしている人、しているけど躓いている人、一回読んだけど忘れちゃった人の理解の助けになればうれしいです。

Discussion

ログインするとコメントできます