この記事は何?
この記事では下記について説明します
- 依存性注入 / DI(Dependency Injection)のメリット
- DIコンテナとPSR-11について
- 依存関係の自動解決
- PHPの代表的なDIコンテナライブラリ
※ 他の言語でも似たような概念はあると思いますが、PHPの話です。
前提: 依存性注入 / DI(Dependency Injection)って何?
DI(依存性注入) とDIコンテナーは別のものです。
- 依存性注入はより良いコードを書くための方法です。
- コンテナは依存関係の注入を支援するツールです。
php-di: Understanding Dependency Injectionより抜粋
では具体的に依存性注入(DI)について見ていきましょう。
依存性注入(DI)がされていないコード
class Foo
{
private Foge $foge;
private Huga $huga;
public function __construct()
{
$this->foge = new Foge();
$this->fuga = new Huga();
}
}
ここでのデメリットを考えてみましょう
- DIを仕様しない場合はクラスが密結合になってしまい、テスト時に大きな問題になります。
- 別のクラスでFoge, Hugaクラスを使う場合、再度newして再生成する必要があり、パフォーマンス的に良くはありません。
特にテストの面では大きく問題になり、Hoge, FugaクラスがDBや外部のAPIなど、PHP以外の要素に依存している場合は、テスト毎にそれらを呼び出す必要があります。
DIされているコード
上記の問題を解決するためにDIされているコードに修正してみます。
class Foo
{
private FogeInterface $foge;
private HugaInterface $huga;
public function __construct(FogeInterface $foge, HugaInterface $fuga)
{
$this->foge = $foge;
$this->fuga = $fuga;
}
}
修正したことによって、モックを使ってテストできるようになりました。
また別途Intercaceを設けることで、FooClassがFoge,Hugaの具体的な処理が変更されても影響がないようになりました。
ただデメリットとしては、このFooクラス外でFoge,Fugaのインスタンスを作成する必要があります。
これらの依存関係の管理の為に利用するのがDIコンテナです
DIコンテナって何? なぜ開発が効率的になるの?
DIコンテナを1行でまとめると
依存関係の注入を管理し、クラスの依存関係の解決と取得を自動化するクラスです。
一般的に、アプリケーションは複数のクラスやコンポーネントで構成されています。
例えば、商品情報を取得するクラスは、データベースへの接続やキャッシュ機能を持つ別のクラスに依存しているかもしれません。
DIコンテナは、これらの依存関係を自動的に解決し、必要なコンポーネントをインスタンス化して提供します。
より詳しく説明する為に、下記のようなRepositoryに依存したクラスがあるとします。
class SomeService
{
public function __construct(
private HogeRepository $hogeRepository,
private FugaRepository $fugaRepository,
)
{
}
public function isHogeFugaUser(int $userId): bool
{
// ホゲフガ判定ロジック
}
}
DIコンテナが存在していない場合
これらの依存関係を下記のように毎回手動で解決する必要があります。
$db = new DatabaseManager();
(new SomeService(
new HogeRepository($db),
new FugaRepository($db),
))->isHogeFugaUser(123);
上記の実装の場合、下記のような問題があります。
- Repositoryを生成するロジックがアプリケーションの至る所に広がる
- コンストラクタを変更した場合、莫大な数の修正する必要がある。
- RepositoryがInterfaceだった場合、開発者が毎回どのクラスを使うか明示する必要がある。
DIコンテナを利用した例
DIコンテナは、これらの依存関係を自動的に解決し、必要なコンポーネントをインスタンス化して提供します。
/** @var \Psr\Container\ContainerInterface $container */
/** @var SomeService $someClass */
$someClass = $container->get(SomeService::class);
$someClass->isHogeFugaUser($userId);
上記の例では、クラス SomeService
のコンストラクタで依存している HogeRepository
や FugaRepository
を自動的に解決することができます。
これにより、
これで開発者は依存関係を手動で解決する(ネストしたnewをいっぱい書く行為)必要がなくなり、
コードの保守性が劇的に向上します!
$container
(DIコンテナ)の生成方法に関しては後ほど記載します。
PSR11 (DIコンテナ作成のルール)
PSR11はPHPのコンテナインターフェースに関する標準仕様です。
過去PHPでは様々な形でDIコンテナのライブラリが作成されていましたが、互換性がなくライブラリ同士を結合させた場合に、オブジェクトの相互運用が簡単にできなかった経緯があり、PSR11が作成されました。
なので、DIコンテナを作成する際は基本的に、PSR11に準拠して作成することになります。
(あくまで標準化された仕様なだけであって、準拠せずに作ることも可能です。需要ないと思いますが)
ContainerInterface
LaravelやSymfony、CakePHPなど主要なフレームワーク、ライブラリの殆どがこのPSR11に準拠してPsr\Container\ContainerInterface
を継承して作成されています。
なので、PSR11に準拠していないライブラリは技術選定から外しても良いぐらいにはなってます。
ContainerInterface
の主なメソッドは2つだけです。
-
get($id)
: 指定された識別子(ID)に対応する依存関係を解決してインスタンスを取得します。 -
has($id)
: 指定された識別子がコンテナ内に存在するかどうかを確認します。
Interfaceにsetterが存在しないので、
DIコンテナに実際のインスタンスを登録する処理は、具象のクラスで定められています。
なんでContainerInterfaceにSetterがないの?
PSR11ではあえて、Setterを用意していません。
これはInterfaceで定義された識別子$id
の使い方が大きく分けて下記の2通りに別れる為、
識別子の使われ方によって、Setterの実装が大きく変わってしまうからです。
※ 混在するDIコンテナもあります
公式のNon-goalsという見出しに詳細が記載されています。
識別子をクラス名で定義するパターン
例: $container->get('FugaRepository::class')
-
メリット: 返されるクラスが想像しやすい
クラス名やインターフェース名を使用することで、識別子からどのようなクラスが取得できるか容易に想像できます、 -
メリット: AutoWireしやすい
DIコンテナがはクラス名に基づいて依存関係を解決し、自動的に適切なエントリを提供することができます -
デメリット: 識別子に対して、クラスの具象は必ず一意である必要がある。
DIコンテナにInterfaceを登録した場合、必ず具象のクラスは1つしか登録できません。
識別子を指定したIDで定義するパターン
例: $container->get('repoistory.fuga')
先ほどのメリット、デメリットの逆ですね、AutoWireなどが難しく手動設定の手間が増える代わりに、
一つのinterfaceに対して、複数の具象クラスを登録ができ柔軟性があります。
DIコンテナを自作してみよう!
PSR11に準拠して、DIコンテナを作成してみます。
作成するのは識別子をクラス名で定義するパターンです。
これは簡略化した例です
業務でDIコンテナを利用する際は、キャッシュを活用してパフォーマンスに考慮したり、AutoWireと呼ばれる自動で依存関係を解決する仕組みを利用して、依存関係の記載を減らす必要があります。
class SimpleContainer implements Psr\Container\ContainerInterface
{
/** @var array<class-string, callable> */
private $services = [];
public function get(string $id): mixed
{
if ($this->has($id)) {
return $this->services[$id]($this);
}
throw new \Exception("Service '$id' not found.");
}
public function has(string $id): bool
{
return isset($this->services[$id]);
}
/**
* @param class-string $id
* @param callable $service
*/
public function set(string $id, callable $service): void
{
$this->services[$id] = $service;
}
}
使用例
クラスの定義、依存関係の定義はこちら(長いので省略)
下記のようなクラスがあるとします。
class DatabaseManager
{
public function getPDO(): \PDO
{
// get PDO object
}
}
class HogeRepository
{
public function __construct(private DatabaseManager $manager)
{
}
public function find(): array
{
// something..
}
}
class FugaRepository
{
public function __construct(private DatabaseManager $manager)
{
}
public function find(): array
{
// something..
}
}
class SomeService
{
public function __construct(
private HogeRepository $hogeRepository,
private FugaRepository $fugaRepository,
)
{
}
public function isHogeFugaUser(int $userId): bool
{
return
$this->hogeRepository->findByUserId($userId) !== null &&
$this->fugaRepository->findByUserId($userId) !== null;
}
}
依存関係を定義して、Containerに登録します。
$container = new SimpleContainer();
$container->set(HogeRepository::class, static function(SimpleContainer $container) {
return new DatabaseManager();
});
$container->set(HogeRepository::class, static function(SimpleContainer $container) {
return new HogeRepository($container->get(DatabaseManager::class));
});
$container->set(FugaRepository::class, static function(SimpleContainer $container) {
return new FugaRepository($container->get(DatabaseManager::class));
});
$container->set(SomeService::class, static function(SimpleContainer $container) {
return new SomeService(
$container->get(HogeRepository::class),
$container->get(FugaRepository::class),
);
});
下記のように呼び出しが可能です。
/** @var SomeService $someClass */
$someClass = $container->get(SomeService::class);
$someClass->isHogeFugaUser($userId);
依存関係の解決の自動化(AutoWire)について
「毎回依存関係を手動で定義しないといけないのか?」と先ほどのコードを見て思ったかもしれません。
サンプルで作成したようなDIコンテナではそうですが、有名なDIコンテナにはAutoWireと呼ばれる、Constructorの型定義からReflectionClassを使って自動で依存関係を解決する機能がついています。
DIコンテナがUserRegistrationServiceを作成する必要がある場合、Constructorの型を読み取りUserRepositoryを自動的に作成します。
class UserRepository
{
// ...
}
class UserRegistrationService
{
public function __construct(UserRepository $repository)
{
// ...
}
}
型定義から依存関係を解決しているので、下記のようなケースは対応ができず、手動で定義する必要があります。
- 型定義が存在しない場合
- Interface(Abstract)で型が指定されている場合
class Database
{
public function __construct($dbHost, $dbPort)
{
// ...
}
public function setLogger(LoggerInterface $logger)
{
// ...
}
}
AutoWireの機能を使うことで、クラスの依存関係が変更されても、依存関係の定義を手動で変更する手間がなくなります。
例などはPHPDIのAutowiringより抜粋
PHPの代表的なDIコンテナライブラリ
LaravelのDIコンテナ
人気の高いフレームワークであるLaravelでは、デフォルトでDIコンテナが存在します。
ただこのDIコンテナはLaravel自体と密に結合しており、調べた限り公式も単体で利用するドキュメントを用意していないので、Laravel以外のプロダクトで利用するのはやめた方がよさそうです。
SymfonyのDIコンテナ
Symfonyという有名フレームワークのコンポーネントの一つです。
Symfonyで使われることが前提の作りにはなっていますが、一応独立したコンポーネントとして利用もできます。
単体で触ったことはありましたが、PHPDIの方が設定がシンプルなように感じました。
PHPDI
PHP-DIはシンプルかつ軽量なDIコンテナであり、独立したライブラリとして使用することができます。
AutoWireにも対応しています。
ただ認知度が低いのと、最近のフレームワークではデフォルトでDIコンテナが存在するので、フレームワークを利用すると使うことがあまりなさそうです。
参考文献
php-fig: PSR-11: Container interface
php-fig: PSR-11: Meta Document
php-di: Understanding Dependency Injection
まとめ
いかがだったでしょうか!
この記事を参考にDIコンテナを利用して、依存性の管理に時間を取られない開発を楽しんでいただければと思います!