こちらは「コドモン Advent Calendar 2024」の 8日目の記事です
こんにちは! プロダクト開発部の岡村 亮太です!
コドモンに入ってからPHPについて考える機会が増え、日に日にPHPへの愛着が深まっています。 そこで、PHPについて何か書いてみたいと思っています!
今回は、開発でよく使われる便利な仕組み「DIコンテナ」について、PHPで 「作ってみた」話を書きます!
まずは下記のコードを見てください。
<?php class TestRepository { public function __construct() {} } class TestService { public function __construct(private readonly TestRepository $repository) {} } class TestController { public function __construct(private readonly TestService $service) {} } $controller = new TestController(); // ERROR
このコードはエラーになります。なぜなら、TestControllerのコンストラクタがTestServiceを必要としているにもかかわらず、そのインスタンスが渡されていないからです。
エラーを回避するには、以下のように依存するクラスのインスタンスを手動で作成し、それぞれ渡してあげる必要があります。
<?php $repository = new TestRepository(); $service = new TestService($repository); $controller = new TestController($service);
普通に考えれば当たり前の手順ですよね。しかし、これが複数のクラスや深い依存関係になると、手動で管理するのが面倒になり、コードが複雑になります。
一方でフレームワーク等によく搭載されている魔法を使うと、こういった依存関係の解決を自動で行ってくれます。
この魔法こそが「DIコンテナ」です。
DIコンテナとは
DIコンテナとは、クラスのインスタンス化を代わりに行い、クラス間の依存関係を自動で解決します。
これにより、開発者は面倒な依存関係の管理から解放され、コードがシンプルで保守性が高くなります。
例えば、TestControllerを作るとき、DIコンテナに「TestControllerを作って!」とお願いするだけで、依存するTestServiceやTestRepositoryも自動で作ってくれます。
まさに魔法です。
なので、DIコンテナを使うと下記のようになります。
<?php // Laravelを使用した場合 $controller = app()->make(TestController::class);
必要なものの作成や、依存関係の解決をすべてDIコンテナがやってくれています。 便利ですよね!
DIコンテナ作ってみる!
ここから本題です。
DIコンテナって最初は「なんだこの魔法!便利すぎる!」と感動していました。
でも調べてみると、「簡易的なDIコンテナは自作できる」という記事を見つけ、「自分もこの手で魔法を作ってみたい!」と思い、実際にDIコンテナを自作してみることにしました。
<?php use ReflectionClass; use Exception; class Container { /** * 依存関係を解決済みのクラスをキャッシュしておくための配列 * * @var array<string, object> */ private array $resolved = []; /** * 循環依存を検出するための配列 * * @var array<string, bool> */ private array $resolving = []; /** * @param array $container * [key] => [クラス名::class] */ public function __construct( private array $container = [] ) { } /** * @param string $key interface or class 名 * @param string|object $className クラス名またはobject */ public function set(string $key, string | object $className) { $this->container[$key] = $className; } public function make(string $id) { $pathPart = explode('\\', $id); $key = end($pathPart); // 循環依存チェック if (isset($this->resolving[$key])) { throw new Exception("循環依存が検出されました: " . $key); } // 既に解決済みの依存関係を返す if (isset($this->resolved[$key])) { return $this->resolved[$key]; } $className = $this->container[$key] ?? $id; // 対象がオブジェクトの場合、オブジェクトをそのまま返す if (is_object($className)) { $this->resolved[$key] = $className; return $className; } // クラス名からコンストラクタ情報を取得する $ref = new ReflectionClass($className); $constructor = $ref->getConstructor(); // コンストラクタがない場合はそのクラス名をnewして返す if ($constructor === null) { $instance = new $className(); $this->resolved[$key] = $instance; return $instance; } // 現在解決中の依存関係としてマーク $this->resolving[$key] = true; // コンストラクタのパラメータごとに処理をする $params = []; foreach ($constructor->getParameters() as $param) { // コンストラクタのパラメータの型名を取得する $type = $param->getType(); $paramClass = $type?->getName(); if ($paramClass) { $params[] = $this->make($paramClass); } } // インスタンスを生成 $instance = new $className(...$params); // 解決済みの依存関係としてキャッシュ $this->resolved[$key] = $instance; // 解決中のマークを解除 unset($this->resolving[$key]); return $instance; } }
こちらのサイトを参考に作ってみました!
今回作ったDIコンテナの使い方は下記になります。
<?php // このキーが呼ばれたらこのクラスを返すことを定義する連想配列を用意 // [キー] => [クラス名] $definitions = [ "TestController" => TestController::class ]; // DIコンテナのクラスをnewする(このときに必要な定義を渡す) $container = new Container($definitions); // TestControllerを作る $controller = $container->make("TestController");
TestRepository, TestServiceをnewしなくてもTestControllerが作成できました! 魔法の完成です!
魔法の種明かし
インスタンス化を代わりにやってくれる
ステップ1: 定義の作成(連想配列)
まず最初に、定義 を作成します。この定義には、キー(識別名)とクラス名のペアを持っており、キーを指定するとそのクラスを返すというルールを定義します。
<?php // このキーが呼ばれたらこのクラスを返すことを定義する連想配列を用意 // [キー] => [クラス名] $definitions = [ "TestController" => TestController::class ];
ここでは、キー "TestController" に対して TestController::class(TestController クラス)を紐付けています。つまり、"TestController" というキーを使うと、TestController クラスが返されるということです。
ステップ2: DIコンテナのインスタンス作成
次に、Containerクラスを使ってDIコンテナのインスタンスを作成します。Containerクラスは、依存関係を管理し、必要なクラスをインスタンス化する役割を担います。
<?php // DIコンテナのクラスをnewする(このときに必要な定義を渡す) $container = new Container($definitions);
ここでは、ステップ1で作成した$definitions(依存関係の定義)をコンテナに渡して、Container クラスのインスタンスを生成しています。コンテナは、登録されたクラスを管理し、必要に応じてインスタンスを生成するために使用されます。
ステップ3: インスタンスの生成
最後に、make() メソッドを使って、"TestController" クラスのインスタンスを生成しています。
<?php // TestControllerを作る $controller = $container->make("TestController");
$container->make("TestController") と呼び出すことで、コンテナは次の処理を行います:
- "TestController" というキーを探す
- TestController::class として登録されたクラスを見つける
- TestController クラスのインスタンスを生成し、そのインスタンスを返す
これで、$controller には TestController のインスタンスが格納されます。
クラス依存関係を勝手に解決
プログラム内でクラスを作成しようとするとき、そのクラスが他のクラスを必要とすることがあります。これを依存関係と呼びます。 例えば、クラスAがクラスBを必要としていた場合、クラスAのインスタンスを作成する際に、クラスBも一緒に作らなければなりません。この依存関係を手動で解決するのは面倒ですよね。でも、PHPのReflectionClassを使うことで、自動で依存関係を解決できます。
ステップ1: コンストラクタの情報を取得する
まず、作成したいクラスのコンストラクタ(初期化処理)の情報を取得します。これを使って、そのクラスがどんな依存関係(引数)を持っているかがわかります。
<?php // クラス名からコンストラクタ情報を取得する $ref = new ReflectionClass($className); // クラス名をReflectionClassに渡す $constructor = $ref->getConstructor(); // コンストラクタ情報を取得
ここでは、クラス名をもとにそのクラスの詳細情報を取得し、コンストラクタの引数を調べています。コンストラクタに引数があれば、それらが他のクラスを要求している可能性があります。
ステップ2: 引数の依存関係を解決する
コンストラクタに引数がある場合、それぞれの引数がどのクラスを必要としているかを調べ、そのクラスを作成します。これを再帰的に行います。つまり、クラスAのコンストラクタでクラスBが必要なら、まずクラスBを作り、次にそのクラスBのコンストラクタで別のクラスCが必要なら、クラスCを作成するという風に進んでいきます。
<?php // コンストラクタのパラメータごとに処理をする $params = []; foreach ($constructor->getParameters() as $param) { // コンストラクタの引数の型名を取得する $type = $param->getType(); $paramClass = $type?->getName(); if ($paramClass) { // 依存しているクラスを再帰的に作成 $params[] = $this->make($paramClass); } }
この部分では、コンストラクタの引数を一つずつ取り出して、その引数が要求するクラス(型)を自動で作成しています。
ステップ3: クラスのインスタンスを生成する
依存関係がすべて解決したら、最後に元々作りたかったクラスのインスタンスを生成します。このとき、必要な依存クラスを引数として渡します。
<?php $instance = new $className(...$params);
これで、依存しているクラスもすべて自動で作成され、目的のクラスのインスタンスが完成します。
まとめ
- DIコンテナは魔法ではなく、インスタンスの生成や依存関係の解消をしてくれる仕組み
- 実際に作ってみることで深く理解できる!
DIコンテナがあまりにも便利すぎて、これまで「魔法みたいなものだから」と思って深く考えずに使っていました。でも、実際に自分で作ってみることで、その「魔法」の仕組みがどんな風に動いているのか、まるで謎が解けたような感覚を味わいました。
「魔法だと思っていたものを、自分の手で作って理解する」この体験を通じて、単に便利なツール・機能を使うだけでなく、その背後にあるロジックや動作を深く掘り下げることができ、技術的にも一歩成長できた気がしています!
最後まで読んでいただきありがとうございます!
参考記事
とても丁寧に説明が書いてあり、こんな私でも理解してDIコンテナを作ることができました。