53
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LaravelAdvent Calendar 2020

Day 10

Laravelのサービスコンテナのバインドと解決の仕組みが知りたい!

Last updated at Posted at 2020-01-26

はじめに

 Laravelについて勉強しているなか、自分はふと
「Laravelでは、bindとかmakeを使って手軽にインスタンスの受け渡しやインスタンスの利用ができて便利だけど、その仕組みはどうなっているんだろ?」
と感じました。
そこで、実際にbindなどのバインドの仕組みとmakeなどの解決の仕組みについてLaravelのコードを見て調べてみました!

本題

環境

ツール バージョン
PHP 7.4.8
Laravel 8.14.0

バインドと解決の大まかな仕組み(というかこの記事の結論)

 バインド(というかbindメソッド)の大まかな仕組みは、
bindメソッドの第一引数をキーとして、第二引数をクラスのインスタンスやクロージャとなるキーの値としてLravelであらかじめ用意された配列にセットするというものです。
そして解決(というかmakeメソッド)の大まかな仕組みは、
bindメソッドでセットした配列のキーと同じ値がmakeメソッドの第一引数にセットされたとき、そのキーに対応するインスタンスやクロージャを返すというものです。
コンテナのバインドと解決の流れを図にするとこんな感じです。
Containerクラス紹介.jpg

 今度は実際のLaravelのコードを見て、バインドの仕組みについてみていきたいと思います。

バインドの仕組みについて

 実際のbindメソッドのコードはこのようになっています。ちなみにbindメソッドは、Laravelのサービスコンテナの実態であるApplication.phpの親クラスであるContainer.phpにあります。

bindメソッド

Container.php
public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract); //.....(1)

        if (is_null($concrete)) {
            $concrete = $abstract;
        }                                    

        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);
        }                                                        //.....(2)

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

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

ポイントを(1)~(4)に分けて説明します。

(1)では、bindメソッドの第一引数が、同クラス内のinstanceメソッドやaliasメソッドなどによりセットされた配列のキーとして含まれていた場合、そのキーの要素を消去します。

(2)では、もし$concreteの値がクロージャでなかった場合(クラスである場合)、同クラス内のgetClousureメソッドによりその値はインスタンス化されます。(getClousureメソッドはメソッド内のbuildメソッドやmakeメソッドによりクラスをインスタンス化していますが、これらのメソッドについては後程取り上げるので説明は省略)
もし$concreteの値が文字列データ出ない場合はこの時点で例外処理が行われます。

(3)がこのbindメソッドでやる最も重要なことかなと思います。なぜなら、このひとつ前の章(バインドと解決の大まかな仕組み)で言っていた、あらかじめ用意された配列とは$this->bindingsのことだからです。
この配列にbindメソッドの$abstractの値をキーとして、$concreteの値をインスタンスやクロージャとしてセットします。
($sharedはデフォルトではfalseとなり特に何の効果も生みませんが、singletonメソッドでバインドされた際はtrueとなり、ある効果を生み出しますが本筋と離れるので詳しい説明は省きます。また、PHPのマジックメソッドであるcompactメソッドは変数名をキー、値をそのキーの値として配列を作ります。詳しくはこちら。)

<追記 今回説明しきれなかった、singletonメソッドについての記事上げました。こちらも是非!
Laravelのsingletonメソッドの機能とその仕組みについて>

(4)では、もし$this->bindingsのキーに対応するインスタンス又はクロージャが解決済みの場合(resolveメソッドにより解決済みかどうかが判断できる)、同クラス内のrebindingメソッドによりセットされた配列の同名のキーの値である関数が行われる。
(いつrebindingメソッドが行われるか、なぜこのメソッドが必要なのかわかっていません。わかる方、コメントで教えてください!)

解決(makeメソッド)の仕組みについて

 解決の流れはLaravelのサービスコンテナの実態であるApplication.phpのmakeメソッドを経て親クラスのContainer.phpのmakeメソッドが実行されますが、Application.phpのmakeメソッドについて説明すると本筋と離れるのでこの記事ではContainer.phpの方だけを取り扱います。

makeメソッド

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

makeメソッドの第一引数に解決したい値、第二引数にその値のメソッドの引数(なければnull)を入れ、それらの引数を基に同クラス内のresolveメソッドが動きます。
単純な作りで良いですね。

resolveメソッド

 解決の機能の大部分を担っているのがresolveメソッドです。

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

        $concrete = $this->getContextualConcrete($abstract); //...(2)

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

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

        $this->with[] = $parameters;  //.....(5)

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

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

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

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

        if ($raiseEvents) {
          $this->fireResolvingCallbacks($abstract, $object); 
        }   //.....(10)

        $this->resolved[$abstract] = true; //.....(11)

        array_pop($this->with);

        return $object;
    }

 resolveメソッドの機能は、このメソッドの第一引数である$abstractの値とbindメソッドで作成した$this->bindingsのキーの値が同じとき、そのキーに対応したインスタンスやクロージャが返される、というものです。でもこれを言うだけじゃ全コード載せた意味がないので、(1)~(10)に分けてもう少し細かく見ていきます。

(1)では、$abstractの値がaliasメソッドなどでセットされる$this->alaiasesという配列のキーに含まれていない場合、$abstractの値はそのままです。もし含まれていた場合はエラーを吐きます。

(2)$abstractの値が同クラス内のaddContextualBindingメソッドでセットされる$this->contextualのキーとしてセットされている場合は$concreteにその値が入れられ、それ以外はnullが入れられます。

(3)では、$parametersの値が存在しないかつ$concreteの値がnullの時以外は$needsContextualBuildの値はtrueになります。(たいていはfalseになります。というかなぜ$this->contextualが存在するのか自分では理解していません。わかる方はぜひ教えてほしいです。)

(4)についてですが、ここの機能はsingletonメソッドを使用する際に使われるですので今回は説明を省きます。

(5)では、$parametersの値が$this->withという配列にセットします。this->withはbuildメソッドで使います。そしてresolveメソッドの最後に$this->withの値はリセットされます。

(6)では、(2)の結果がnullの場合、$concreteに同クラス内のgetConcreteメソッドの結果をセットします。getConcreteメソッドの機能は、resolveメソッド内の$abstractの値と同じ$this->bindingsのキーのインスタンス又はクロージャを返す、というものです。また、それができなければgetConcreteメソッドの引数(ここでは$abstract)の値をそのまま返します。

(7)では、まず同クラス内のisBuildableメソッドによって処理の分岐が行われます。isBuildableでは、もし$abstract$concreteの値が同じか$concreteの値がクロージャであった場合、tureを返しそうでない場合はfalseを返します。
もしtrueであった場合は$objectの値がbuildメソッドの結果となります(buildメソッドについては後程取り上げます)。
もしfalseであった場合は第一引数を$concreteの値にしてresolveメソッド(makeメソッドはresolveメソッドを引き起こしているだけ)を行います。これにより今度は$abstractの値と$concreteの値が同じになり、trueの場合と同じことが行われます。

(8)では、もし同クラス内のextendメソッドによりセットされた$this->extendersの配列のキーと$abstractの値が同じ場合は、そのキーに対応したクロージャを$objectの値にセットします(extendメソッドの使われ方は理解できていません。わかる方教えてください)。

(9)は、singletonメソッドに関係するところなので、説明を省きます。

(10)では、同クラス内のresolvingメソッドで$this->globalResolvingCallbacksにクロージャがセットされていたらそのクロージャを実行します。セットされていなければ何もしません。

(11)では、$this->resolvedの配列の$abstractの値がキーとなっている値にtrueをセットします。これにより、$abstractの値を解決しようとしたときにすでにその値が解決済みかどうかがわかります。

最後に$this->bindings[$abstract]に対応したインスタンスかクロージャを返します。

buildメソッド

buildメソッドはbindメソッドでセットした$this->bindingのキーに対するクロージャやインスタンスを用意してくれる、サービスコンテナの中で重要な役割を担うメソッドです。
これがなければ解決どころかバインドすらも成立しません。

Container.php
public function build($concrete)
    {
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }                                                              //.....(1)

        try {
          $reflector = new ReflectionClass($concrete); 
        } catch (ReflectionException $e) {
          throw new BindingResolutionException("Target class [$concrete] 
            does not exist.", 0, $e);
        }  //.....(2)

        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }                                            //.....(3)

        $this->buildStack[] = $concrete; //.....(4)

        $constructor = $reflector->getConstructor(); //.....(5)

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

            return new $concrete;
        }                                //.....(6)

        $dependencies = $constructor->getParameters(); //.....(7)

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

          throw $e;
        }  //.....(8)

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances); //.....(9)
    }

buildメソッドは渡される引数によって中で起こることが変わります。

(1)は、もしbuildメソッドに渡された引数がクロージャである行われます。というかクロージャの場合は(1)で終了です。クロージャが返されるときに引数にとる$this->getLastParameterOverride()の値はmakeメソッドやresolveメソッドの第二引数であるarray $parametersの値となります。なお、返り値のクロージャの第一引数は絶対$this(Applicationクラスをインスタンスにとるオブジェクト。大抵は$app)となるので、もしクロージャに値を受け渡したいときは、bindメソッドでセットするクロージャの第二引数に受け渡したい値をセットしましょう。

(2)のReflectionClassはPHP5以降からPHPにあらかじめセットされたクラスで、使用したいクラスにuse ReflectionClass;と入力することで使えるようになります(詳しくはこちら)。
ReflectionClassに組み込まれているメソッドを使うことで、引数に指定されているクラスの様々な情報が分かったり、そのクラスの数値を取り出すこともできます。もしこの時にクラスが存在しないなどのエラーが出た場合は例外処理が行われます。

(3)ではReflectionClassの引数のクラス(これからは$concrete)がインスタンス化できるものかを調べ、できなければエラーを吐きます。

(4)の$this->buildStackは、buildメソッドが行われている間$concreteの値を他のメソッドに受け渡す際に使われます。buildメソッドの終了時に$this->buildStackの値はリセットされます。

(5)では、もし$concreteがコンストラクトメソッドを含む場合に、それを取得します。(詳しく言うと、$constructorReflectionMethodのオブジェクトをセットします。もしコンストラクトメソッドがなければnullをセットします。ReflectionMethodReflectionClassと同じようにPHP5以降から元からセットされたクラスで、$concreteのメソッドについて調べます。詳しくはこちら。)

(6)ではもし$concreteにコンストラクトメソッドがなければ、$concreteをインスタンスとして返します。

(7)では、$concreteのコンストラクトメソッドの引数の値を$dependenciesにセットします。
(ここでも詳しく言うと、ここでは$dependenciesReflectionParameterのインスタンスをセットします。ReflectionParameterReflectionClassと同じように以下略。$concreteのメソッドの引数について調べます。詳しくはこちら。)

(8)では、同クラス内のresolveDependenciesメソッドを使用し、もし調べる対象の$concreteのメソッドに値入りの引数があればその値を、タイプヒント付きの引数があればタイプヒントに表記されたクラスのインスタンスを$instancesにセットします。(そのとき事前にタイプヒントとして表記されたクラスがバインドされていなければならない)もしここでエラーが発生した場合は$this->buildStackを解放したうえで、例外処理が行われます。

(9)では$concreteにコンストラクトメソッドに入る引数(なければ空)をセットし、$concreteのインスタンスを返します。
(newInstanceArgsメソッドはReflectionClassに組み込まれたメソッドです。詳しくはこちら。)

実際に作って動かしてみよう!

まず作ろう

ここまで調べてくると、今まで調べたクラス、メソッドをコピペし、うまく組み合わせればフレームワーク要らずでバインドと解決の機能を再現できるんじゃね、と思い作ってみました。
実際に(コピペして)作ったコードがこちら

Application.php
<?php
namespace App; //ここはどうでも良いよ

use ReflectionClass;
use Closure;
use ReflectionParameter;
/**
 *
 */
class Application
{
  protected $bindings = [];
  protected $instances = [];
  protected $resolved = [];
  protected $abstractAliases = [];
  protected $contextual = [];
  protected $buildStack = [];
  protected $with;


  public function bind($abstract, $concrete = null, $shared = false)
  {
    if (is_null($concrete)) {
      $concrete = $abstract;
    }

    if (! $concrete instanceof Closure) {
      $concrete = $this->getClosure($abstract, $concrete);
    }

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

  protected function getClosure($abstract, $concrete)
  {
    return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
  }

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

  public function resolve($abstract, $parameters = [])
  {
    $needsContextualBuild = ! empty($parameters);

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

    $this->with[] = $parameters;

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

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
  }

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

        $reflector = new ReflectionClass($concrete);

        if (! $reflector->isInstantiable()) {
            //return $this->notInstantiable();
            echo "Error";
        }

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

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

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

 protected function getLastParameterOverride()
 {
     return count($this->with) ? end($this->with) : [];
 }

 protected function notInstantiable()
 {
   $message = 'Error!! This Class is not instantiable.';
   throw new Exception($message);
 }

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

        foreach ($dependencies as $dependency) {
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
  }

  protected function hasParameterOverride($dependency)
  {
      return array_key_exists(
          $dependency->name, $this->getLastParameterOverride()
      );
  }


  protected function resolvePrimitive(ReflectionParameter $parameter)
  {
      if ($parameter->isDefaultValueAvailable()) {
          return $parameter->getDefaultValue();
      }

      $this->unresolvablePrimitive($parameter);
  }

  protected function unresolvablePrimitive(ReflectionParameter $parameter)
  {
      $message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";

      echo $message;
  }

  protected function resolveClass(ReflectionParameter $parameter)
  {
    return $this->make($parameter->getClass()->name);
  }
}
?>

Laravelで実際に書かれているコードと比べるとだいぶ少ない分量でいけました。

動かそう

次に今回のように調べる&コピペで作ったオートローダ(その時の記事はこちら)と共に動かしていきます。
動作の結果は、ブラウザ上で確認します。
ちなみにファイルの構造はこんな感じ(白丸は黒丸の中にあるファイル)

<アプリケーション内>

  • app
    • Application.php(バインドと解決)
  • Model(このフォルダは実行したいクラスが入る)
    • First.php
    • Second.php
    • Third.php
  • app.php(Laravelのbootstrap\app.phpのポジション)
  • index.php(Laravelのpublic\index.phpのポジション。こいつの表示を見て動いているかどうかを見る。)
  • autoload_real.php(これ以下のファイルはオートロードの機能を担うがオートロードは今回の本筋ではないため紹介しない)
  • autoload_static.php
  • autoloas.php
  • ClassLoader.php

実行するクラス

First.php
<?php
namespace Model;

/**
 *
 */
class First
{
  public function copy()
  {
    echo "耳コピでしゅか?".nl2br("\n"); //nl2br()はブラウザ上で文字を改行するため
  }

  public function pai()
  {
    echo "じゃあだれが一体パイを焼くんだい?";
  }
}
?>
Second.php
<?php

namespace Model;
/**
 *
 */
class Second extends First
{
  public function __construct()
  {
    $this->comment = "キャー、のび太さんの".nl2br("\n");
  }
  public function comment()
  {
    echo $this->comment."エッジのきいたカッティングリフー!".nl2br("\n");
  }
}

?>
Third.php
<?php

namespace Model;
/**
 *
 */
class Third
{

  public function __construct(First $first)
  {
    $this->first = $first;
  }

  public function end()
  {
    $this->first->pai();
  }
}

?>

ひたすらバインドを行うクラス

app.php
<?php

$app = new App\Application();

$app->bind('Teacher', function ()
{
  echo "コピーよろしく".nl2br("\n");
});

$app->bind('Nobita', Model\First::class);

$app->bind('Shizuka', Model\Second::class);

$app->bind('End', Model\Third::class);

return $app;

?>

結果をブラウザに写すクラス(解決を行うクラス)

index.php
<?php
require 'autoload.php'; //自作のコピペオートローダの読み込み

$app = require_once __DIR__.'/app.php'; //自作のコピペサービスコンテナの読み込み

$app->make('Teacher');

$answer = $app->make('Nobita');
$answer->copy();

$comment = $app->make('Shizuka');
$comment->comment();

$ending = $app->make('End');
$ending->end();
?>

結果

コピーよろしく 耳コピでしゅか? キャー、のび太さんの エッジのきいたカッティングリフー! じゃあだれが一体パイを焼くんだい?

終わりに

 他の人に自分の調べたことを伝えることよりも自分の調べたことを言葉でまとめることに意識がいってしまい、わかりにくいところが多い記事になっていると思います。読んでくれた方ありがとうございます、そしてすいません。
 フレームワークについて調べると、そのフレームワークの理解が深まるだけではなく、言語自体の理解も深まることもあり、面白かったです。singletonメソッドなどまだまとめきれなかった項目が残っているので、今後これらも取り上げられたら良いなと思っています。

今回説明しきれなかった、singletonメソッドについての記事上げました。こちらも是非!
Laravelのsingletonメソッドの機能とその仕組みについて

こんな記事も書いてます!良かったらぜひ~
LaravelのCSRF対策の処理を実際のコードから見てみる

53
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?