170
124

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 3 years have passed since last update.

【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。

Last updated at Posted at 2020-03-09

はじめに

Laravelを使ってアプリケーションを実装するうえで、
最も便利だと感じる機能が、「コンストラクターインジェクション」です。
Laravel 6.x コントローラ:コンストラクターインジェクション

クラスのコンストラクタの引数で、特定のクラスを型宣言しておくだけで、
自動的に引数としてそのクラスのインスタンスを渡してくれるという機能です。

これには、
サービスコンテナの「依存解決」という機能が利用されています。

サービスコンテナとは何なのか?
依存解決とは何なのか?
についてはこちらの記事で解説しています。
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。

先にこちらの記事を読んで、サービスコンテナの概要について理解してから
本記事を読むことをお勧めします。

今回は、そのサービスコンテナの「依存解決」機能の内部で
具体的にどのような処理を行っているのかを解説していきます。

依存解決とは

まずは、サービスコンテナの「依存解決」とは何なのか、
ざっくり確認しておきます。

このような、ClassAがあったとします。
コンストラクタの引数で、ClassBを受け取っています。

ClassA.php
class ClassA
{
    private $classB;

    public function __construct(ClassB $classB)
    {
        $this->classB = $classB;
    }
}

 
 
このClassAをインスタンス化するとき、
普通ならこのように
まずClassBをインスタンス化し、それを引数としてClassAをインスタンス化しますね。

Main.php
$classB = new ClassB();
$classA = new ClassA($classB);

 
 
引数を無視していきなり
ClassAをnewでインスタンス化しようとすると、
もちろんエラーが発生します。

Main.php
$classA = new ClassA();

// laravel.log
// local.ERROR: Too few arguments to function ClassA::__construct()

 
 
サービスコンテナのmake()メソッドを利用すると
この様にすることができます。

Main.php
$classA = app()->make(ClassA::class);

事前にClassBをインスタンス化せずに、
いきなりClassAをmake()しましたが、
エラーは発生しませんし
ちゃんとClassAの中でClassBがインスタンス化されています。

ClassBがさらにClassC、D、Eと階層的に依存していたとしても、
再帰的にすべて依存解決してくれます。 
 
 
これが、サービスコンテナの依存解決という機能です。

このmake()メソッドの内部ではいったいどのような処理をして
依存解決しているのか、具体的に解説していきます。

リフレクションとは

PHPには「リフレクション」という機能があり、
サービスコンテナのmake()メソッドでは
この「リフレクション」を活用して依存解決を行っています。

依存解決の仕組みを知るうえで
「リフレクション」というものが何なのかを知っておく必要があるので
簡単に見ていきます。

wiki:リフレクション (情報工学)

ウィキペディアによると、

情報工学においてリフレクション (reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことを指す。

とあります。

重要なのはここ
「プログラム自身の構造を読み取ったり書き換えたりする技術」
です。

さらに、
「プログラム自身の構造を読み取ったり書き換えたりする技術」
これを今回のPHPの例でわかりやすく具体的に言うと、
「クラスの中身を読み取って、
どんなプロパティを持っているか?
どんなメソッドを持っているか?
メソッドの引数は何か?
アクセス修飾子は何か?
などいろいろと調べることができる機能」
という感じ。

実際にリフレクション機能を使ってコードを書いてみます。

この様なクラスがあったとします。

SampleClass.php
class SampleClass
{
    private $classA;
    private $classB;

    public function __construct(ClassA $classA, ClassB $classB)
    {
        $this->classA = $classA;
        $this->classB = $classB;
    }

    public function method1(int $int)
    {
    }

    private function method2()
    {
    }
}

 
 
そのSampleClassの内部を調べるコードを
Main.phpのほうに書いていきます。

Main.php
$reflectionClass = new \ReflectionClass(SampleClass::class);

この様に、new \ReflectionClass()にクラス名を文字列で与えることで、
リフレクションクラスを作成します。

そしてこのリフレクションクラスに対して様々なメソッドを実行することで
クラスの内部構造をいろいろ読み取ることができます。

例えば、クラスの持つプロパティ一覧を取得してみたり。

Main.php
$reflectionClass = new \ReflectionClass(SampleClass::class);
$properties = $reflectionClass->getProperties(); // クラスの持つプロパティ一覧を取得する

dd($properties);
// array:2 [▼
//   0 => ReflectionProperty {#360 ▼
//     +name: "classA"
//     +class: "App\Sample\SampleClass"
//     modifiers: "private"
//   }
//   1 => ReflectionProperty {#361 ▼
//     +name: "classB"
//     +class: "App\Sample\SampleClass"
//     modifiers: "private"
//   }
// ]

 
 
クラスの持つメソッド一覧を取得してみたり。

Main.php
$reflectionClass = new \ReflectionClass(SampleClass::class);
$methods = $reflectionClass->getMethods(); // クラスの持つメソッド一覧を取得する

dd($methods);
// array:3 [▼
//   0 => ReflectionMethod {#360 ▼
//     +name: "__construct"
//     +class: "App\Sample\SampleClass"
//     parameters: {▼
//       $classA: ReflectionParameter {#373 ▶}
//       $classB: ReflectionParameter {#372 ▶}
//     }
//     modifiers: "public"
//   }
//   1 => ReflectionMethod {#361 ▼
//     +name: "method1"
//     +class: "App\Sample\SampleClass"
//     parameters: {▼
//       $int: ReflectionParameter {#374 ▶}
//     }
//     modifiers: "public"
//   }
//   2 => ReflectionMethod {#363 ▼
//     +name: "method2"
//     +class: "App\Sample\SampleClass"
//     modifiers: "private"
//   }
// ]

メソッド名や、そのアクセス修飾子、パラメータなども丸見えですね。

他にもいろいろとできることがあるので、
よかったら公式マニュアルで確認してみてください。
PHP: リフレクション - Manual

リフレクションは、
ユニットテストでプライベートメソッドのテストを実行したいときに、
メソッドのアクセス修飾子をpublicに書き換えて実行。
とかやったりするときに使ったりしますね。(それ以外では使ったことない)
 
 
 
そして、もう多少想像できてきているかもしれませんが、
サービスコンテナのmake()では
このリフレクションを利用して、
・クラスのコンストラクタはあるか?
・コンストラクタの引数はあるか?
・その引数は型宣言されているか?
・その宣言されている型はクラスか?
みたいなことをやって、依存解決を実現させています。

それではリフレクションの説明は終わりにして、
make()の仕組みを見ていきます。

make()の仕組み

今回は、サービスコンテナのmake()メソッドの仕組みを説明するにあたり、
make()メソッドの簡易版を自作していきたいと思います。

想像以上に簡単なので、安心して読み進めてください。

1階層のみの依存解決

まずは簡単に、1階層のみ依存解決できるコードを作ってみます。

対象クラス作成

依存解決の対象となるクラスはこの様な形にします。

Untitled Diagram-Copy of Page-1.png

ClassAがあり、
そのコンストラクタ引数としてClassBに依存している状態です。

ClassA
class ClassA
{
    public function __construct(ClassB $classB)
    {
        \Log::info('ClassAインスタンス化完了');
    }
}
ClassB
class ClassB
{
    public function __construct()
    {
        \Log::info('ClassBインスタンス化完了');
    }
}

それぞれインスタンス化されたことが分かるように
コンストラクタでログを出すようにしています。
 
 
それでは次に、
このクラスの依存解決をしてくれるmake()メソッドを作っていきます。

クラス名を文字列で受け取る

make()を呼び出す側は、
この様な形でmake()を実行します。

Main.php
$classA = $this->make(ClassA::class);

※このClassA::classは、"App\Sample\ClassA"のように名前空間を含むクラスの完全修飾名を文字列で出しています。

なので、まずmake()では引数としてクラス名を文字列で受け取りましょう。

Main.php
public function make(string $className)
{
}

いまこの$classNameには"App\Sample\ClassA"という文字列が渡されている状態ですね。

リフレクションクラス作成

次に、受け取ったクラス名からリフレクションクラスを作成します。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
}

いまこの$reflectionClassには、ClassAのリフレクションクラスが入っている状態です。

コンストラクタ取得

次に、このリフレクションクラスからコンストラクタを取得します。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
}

これで、$constructorにはClassAのコンストラクタの情報が入っている状態です。

コンストラクタの引数取得

次に、このコンストラクタの引数を取得します。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
}

これで、$parametersにはClassAのコンストラクタの引数の一覧が配列で入ってる状態です。

今回の例では、ClassBで型宣言された$classBという引数の情報が1つ入っているだけですね。
簡易版なので、コンストラクタの引数が複数あるケースを無視して1つだけの場合で進めちゃいます。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $parameter = $parameters[0];
}

$parameter = $parameters[0];

この様に、先ほど取得したパラメータ一覧の最初の要素を取っておきます。

これで、$parameterには
ClassBで型宣言された$classBという引数の情報が入っている状態ですね。

引数で型宣言されたクラス名取得

取得したパラメータ情報から、
型宣言されているクラスの名前を取得します。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $parameter = $parameters[0];
    $parameterClassName = $parameter->getClass()->name;

    dd($parameterClassName);
    // "App\Sample\ClassB"
}

$parameterClassName = $parameter->getClass()->name;

先ほどの$parameter->getClass()することで、型宣言されているクラスが取得できます。
さらにそのクラスから->nameとするとクラス名を取得することができます。

ddでちゃんと"App\Sample\ClassB"というクラス名が文字列で取得できていますね。

型宣言されているクラスをインスタンス化する

コンストラクタの引数で型宣言されているクラスのクラス名
"App\Sample\ClassB"を取得できたので、
このクラスをインスタンス化します。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $parameter = $parameters[0];
    $parameterClassName = $parameter->getClass()->name;
    $parameterInstance = new $parameterClassName;

    // laravel.log
    // local.INFO: ClassBインスタンス化完了  
}

クラス名文字列を使ってnewしているので、
これで$parameterInstanceにはClassBのインスタンスが入っている状態です。

クラスがインスタンス化されたときに、
ログを出力するようにしているので
この時点でlaravel.logにはClassBインスタンス化完了
とログ出力されているはずです。

作成したインスタンスを引数に渡して、対象クラスをインスタンス化する

最後に、インスタンス化したクラス(ClassB)を引数にして、
対象クラス(ClassA)をインスタンス化して返却しましょう。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $parameter = $parameters[0];
    $parameterClassName = $parameter->getClass()->name;
    $parameterInstance = new $parameterClassName;

    return new $className($parameterInstance);
}

$classNameにはClassAのクラス名が入っていて、
$parameterInstanceにはClassBのインスタンスが入っている状態なので、
これで無事にClassAをインスタンス化することができました。

改めて、この自作make()メソッドを実行してみましょう。

Main.php
$classA = $this->make(ClassA::class);

// laravel.log
// local.INFO: ClassBインスタンス化完了  
// local.INFO: ClassAインスタンス化完了  

これで、make()内部では

  • ClassAのリフレクションクラス作成
  • ClassAのコンストラクタ取得
  • コンストラクタの引数取得
  • 引数で型宣言されているクラス名を取得
  • そのクラス(ClassB)をインスタンス化
  • そのClassBインスタンスを引数にしてClassAをインスタンス化

という処理が行われたわけです。

laravel.logにもちゃんと

// local.INFO: ClassBインスタンス化完了
// local.INFO: ClassAインスタンス化完了

と出力されています。

これにて、一番簡単な形での依存解決を自力で実装することができました。
ただし、現状は1階層のみの依存解決しかできていません。

ClassBのコンストラクタ引数でさらにClassCが渡されていたりしたら、
このmake()メソッドでは依存解決することができないですね。

なので、次は多階層の依存解決ができるように
自作make()メソッドをもう少し進化させてみましょう。

多階層の依存解決

対象クラス作成

まずは依存解決対象のクラスたちを作るところから。

次はこのようなクラスの形にします。

Untitled Diagram-Copy of Page-1 (1).png

先ほどのものからさらに、
ClassBのコンストラクタ引数としてClassCに依存している形ですね。

php.ClassA.php
class ClassA
{
    public function __construct(ClassB $classB)
    {
        \Log::info('ClassAインスタンス化完了');
    }
}

ClassAはさっきと同じ状態です。
 
 

php.ClassB.php
class ClassB
{
    public function __construct(ClassC $classC)
    {
        \Log::info('ClassBインスタンス化完了');
    }
}

ClassBではコンストラクタ引数としてClassCを型宣言しておきます。
 
 

ClassC.php
class ClassC
{
    public function __construct()
    {
        \Log::info('ClassCインスタンス化完了');
    }
}

ClassCは依存なし。

 

現状の問題点

現状のmake()メソッドの状態のままで
このClassAの依存解決をしようとしたらどうなるでしょうか。

Main.php
$classA = $this->make(ClassA::class);

すると、
Too few arguments to function App\Sample\ClassB::__construct()
のエラーが発生します。

どこでエラーが発生したのか、
make()の中を見てみましょう。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className); // ClassAのリフレクションクラス作成
    $constructor = $reflectionClass->getConstructor();   // ClassAのコンストラクタ取得
    $parameters = $constructor->getParameters();         // コンストラクタの引数取得
    $parameter = $parameters[0];
    $parameterClassName = $parameter->getClass()->name;  // 引数で型宣言されているクラス名(ClassB)を取得
    $parameterInstance = new $parameterClassName;        // そのクラス(ClassB)をインスタンス化

    return new $className($parameterInstance);
}

エラーが発生したのはここですね。
$parameterInstance = new $parameterClassName; // そのクラス(ClassB)をインスタンス化

ここで、ClassBのコンストラクタ引数としてClassCのインスタンスを渡さないといけないのに、
何も渡さずにnewしているのでエラーが発生しています。

ここを解消していきます。

再帰的にmake()させる

この問題を解消するのは簡単で、
$parameterInstance = new $parameterClassName;
ここで、型宣言されているクラスをnewでインスタンス化していますが、
これをmake()でインスタンス化すればいいだけです。

$parameterInstance = new $parameterClassName;
// ↓↓変更
$parameterInstance = $this->make($parameterClassName);

この様にするだけです。

そうすることで、
ClassBもまたmake()メソッドに投げられ、
引数であるClassCを依存解決したうえでインスタンス化して返ってきてくれます。

ただし、このままだとまだ1つ問題があるのでそこも修正しておきます。

再帰的にmake()にクラスを渡すようにすると、
依存関係の最後にあたるClassCもmake()メソッドに投げられることになります。

その時、ClassCにはコンストラクタの引数が何も指定されていない状態ですので、

$parameters = $constructor->getParameters();
$parameter = $parameters[0];

この辺でパラメータが取得できないまま次の処理に行こうとして、
エラーが発生してしまいます。

なのでコンストラクタの引数がない場合は
そのまま対象クラスをインスタンス化して返却するようにしましょう。

Main.php
public function make(string $className)
{
    $reflectionClass = new \ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    // コンストラクタの引数がない場合はそのままインスタンス化して返却
    if (empty($parameters)) {
        return new $className;
    }
    $parameter = $parameters[0];
    $parameterClassName = $parameter->getClass()->name;
    $parameterInstance = $this->make($parameterClassName); // ここでnewではなくmake()する

    return new $className($parameterInstance);
}

これでエラーにならずに多階層の依存解決ができる状態になりました。

改めて、この自作make()メソッドを実行してみましょう。

Main.php
$classA = $this->make(ClassA::class);

// laravel.log
// local.INFO: ClassCインスタンス化完了  
// local.INFO: ClassBインスタンス化完了  
// local.INFO: ClassAインスタンス化完了  

エラーが発生せず、
laravel.logにはちゃんとClassC→B→Aのインスタンスが作成されたログが出力されています。

make()内部では

  • ClassAのリフレクションクラス作成
  • ClassAのコンストラクタ取得
  • コンストラクタの引数取得
  • 引数で型宣言されているクラス名を取得
  • そのクラス(ClassB)をmake()に渡す
  • ClassBのリフレクションクラス作成
  • ClassBのコンストラクタ取得
  • コンストラクタの引数取得
  • 引数で型宣言されているクラス名を取得
  • そのクラス(ClassC)をmake()に渡す
    • ClassCのリフレクションクラス作成
    • ClassCのコンストラクタ取得
    • コンストラクタの引数取得
    • 引数がないので、ClassCをインスタンス化して返却
  • そのClassCインスタンスを引数にしてClassBをインスタンス化
  • ClassBインスタンスを返却
  • そのClassBインスタンスを引数にしてClassAをインスタンス化
  • ClassAインスタンスを返却

という処理が行われたわけです。
 
 
これで、多階層の依存解決ができるmake()メソッドを自作することができました。

本物のmake()メソッド

多階層の依存解決ができるmake()メソッドを自作しましたが、
本物のmake()メソッドと比べるとまだまだたくさん足りない機能があります。

簡単なところで言うと
・引数が複数あった場合に対応していない
・引数にクラス以外があった場合に対応していない
など。

さらに複雑な機能で言うと
・bindでカスタマイズされたインスタンス化方法に対応していない
・クラス名ではなくインターフェース名を渡された場合に対応していない
などがありますね。

ただ、基本としてはこれまで解説したような仕組みが
本物のmake()でも利用されています。

そのうえでさらに、上記のような様々なパターンに対応するための
複雑な条件分岐や処理がいろいろ追加されているイメージです。

これをきっかけに、本物のmake()メソッドの
ソースコードを読み込んでみるとさらにLaravelへの理解が深まると思います。

おわりに

サービスコンテナがどのように依存解決を行っているのか、
簡易版make()メソッドを自作することで確認してみました。

Laravelのソースコードでは、このmake()メソッドがたくさん登場するので、
ぜひ読んでみてください。

そして、最初に話した「コンストラクターインジェクション」

Laravelを使ってアプリケーションを実装するうえで、
最も便利だと感じる機能が、「コンストラクターインジェクション」です。

クラスのコンストラクタの引数で、特定のクラスを型宣言しておくだけで、
自動的に引数としてそのクラスのインスタンスを渡してくれるという機能です。

いつもコントローラのコンストラクタ引数にクラス名を指定するだけで
勝手にインスタンスが取得できるという
あの便利な機能は、
サービスコンテナのmake()メソッドが内部的に利用されていて、
そのmake()メソッドでは今回解説したリフレクション機能を利用して
実現されていたわけですね。

この
コントローラクラスをmake()で依存解決している場所
を見てみたい場合は、
こちらの記事で解説しています。
魔法のようなLaravelも素のPHPに始まり素のPHPに終わる。Laravelのライフサイクルを簡単に解説。

WEBサーバにリクエストが届いてから
コントローラクラスをmake()する瞬間までの流れを見ることができるので、
本記事の後に読むと結構楽しいんじゃないかと思います。

Laravelのちょっと深いところを理解していきたいと思っている方、
これらの記事もおすすめですので是非読んでみてください。
(次に進む前に、LGTMしてもらえるとうれしいです)
魔法のようなLaravelも素のPHPに始まり素のPHPに終わる。Laravelのライフサイクルを簡単に解説。
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
【Laravel】ファサードとは?何が便利か?どういう仕組みか?

参考

【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
魔法のようなLaravelも素のPHPに始まり素のPHPに終わる。Laravelのライフサイクルを簡単に解説。
https://blog.asial.co.jp/751
https://codezine.jp/article/detail/2011
https://www.php.net/manual/ja/book.reflection.php

170
124
1

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
170
124

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?