DI コンテナとは?
依存性注入コンテナ(DIC、または DI コンテナ)は、ほかのオブジェクト(サービスと呼ばれます)を生成して設定する役目を持つオブジェクトです。
意外に思うかもしれませんが、多くの場合、依存性注入(略して DI)の恩恵を受けるのに依存性注入コンテナは必要ありません。実際、はじめの章でも DI の具体的な例を示しましたが、コンテナは不要でした。
とはいえ、複雑な依存関係を持つ多数のオブジェクトを管理するようになると、DI コンテナはとても役に立ちます。フレームワークの上に作られたウェブアプリケーションでは、たいていそうなります。
前の章で Article と EditController
のクラスを紹介しました。どちらも依存関係、すなわちデータベースとファクトリ
ArticleFactory
を持っています。これらのクラスのために、ここでコンテナを作ってみましょう。もちろん、これほど単純な例にコンテナを作るのはやりすぎです。それでも、どんな形でどう動くかを示すために作ります。
上の例のための、値を直接書いた単純なコンテナです。
class Container
{
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection('mysql:', 'root', '***');
}
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->createDatabase());
}
public function createEditController(): EditController
{
return new EditController($this->createArticleFactory());
}
}
使い方は次のようになります。
$container = new Container;
$controller = $container->createEditController();
オブジェクトをコンテナに要求するだけで、その作り方も依存関係も知る必要はありません。すべてコンテナが引き受けます。依存関係はコンテナが自動的に注入します。それがコンテナの強みです。
今のところ、コンテナはすべての情報を直接書き込んでいます。次の段階として、コンテナを本当に使えるものにするためにパラメータを足しましょう。
class Container
{
public function __construct(
private array $parameters,
) {
}
public function createDatabase(): Nette\Database\Connection
{
return new Nette\Database\Connection(
$this->parameters['db.dsn'],
$this->parameters['db.user'],
$this->parameters['db.password'],
);
}
// ...
}
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
目ざとい読者は問題に気づくかもしれません。EditController
オブジェクトを取り出すたびに、ArticleFactory
とデータベース接続の新しいインスタンスも作られてしまいます。これは望ましくありません。
そこで、常に同じインスタンスを返す getService() メソッドを足します。
class Container
{
private array $services = [];
public function __construct(
private array $parameters,
) {
}
public function getService(string $name): object
{
if (!isset($this->services[$name])) {
// getService('Database') は createDatabase() を呼びます
$method = 'create' . $name;
$this->services[$name] = $this->$method();
}
return $this->services[$name];
}
// ...
}
たとえば $container->getService('Database') の最初の呼び出しでは、createDatabase()
を呼んでデータベースのオブジェクトを作り、$services
配列に保存して返します。以降の呼び出しでは、保存されたインスタンスをそのまま返します。
コンテナの残りの部分も getService() を使うように書き換えます。
class Container
{
// ...
public function createArticleFactory(): ArticleFactory
{
return new ArticleFactory($this->getService('Database'));
}
public function createEditController(): EditController
{
return new EditController($this->getService('ArticleFactory'));
}
}
ちなみに、サービスという言葉はコンテナが管理する任意のオブジェクトを指します。メソッド名が
getService() なのはそのためです。
これで完成です。きちんと動く DI コンテナができました。さっそく使ってみましょう。
$container = new Container([
'db.dsn' => 'mysql:',
'db.user' => 'root',
'db.password' => '***',
]);
$controller = $container->getService('EditController');
$database = $container->getService('Database');
ご覧のとおり、DIC を書くのは難しくありません。注目すべきは、オブジェクト自身はコンテナが自分を作っていることを知らない、という点です。ですから、ソースコードを変えずに任意の PHP オブジェクトをこの方法で作れます。
とはいえ、コンテナのクラスを手で作って保守するのは、すぐに悪夢になりかねません。そこで次の章では、ほぼ自動で自分自身を生成・更新できる Nette DI Containerを扱います。