依存関係の受け渡し
引数、DI の用語でいう「依存関係」は、次の主な方法でクラスに渡せます。
- コンストラクタインジェクション
- メソッドインジェクション(いわゆるセッターインジェクション)
- プロパティインジェクション
inject*()メソッドまたは#[Inject]アトリビュートの利用
それぞれの形を具体的な例で見ていきましょう。
コンストラクタインジェクション
依存関係は、オブジェクトが生成されるときにコンストラクタの引数として渡されます。
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
この方法は、クラスが動くのに絶対に必要な依存関係に向いています。それがなければインスタンスを作れないからです。
PHP 8.0 以降は、より短い書き方(コンストラクタのプロパティ昇格)が使え、機能は同じです。
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
PHP 8.1 以降はプロパティに readonly
フラグを付けられ、初期化のあとに値が変わらないことを宣言できます。
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI コンテナはオートワイヤリングを使って、依存関係をコンストラクタに自動的に渡します。この方法で渡せない引数(文字列、数値、真偽値など)は設定で指定します。
コンストラクタ地獄
コンストラクタ地獄という言葉は、依存関係を必要とするコンストラクタを持つ親クラスを子クラスが継承し、その子クラスも依存関係を必要とする状況を指します。子クラスは親の依存関係も受け取って渡さなければなりません。
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ コンストラクタ地獄
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
問題が起こるのは、たとえば新しい依存関係が加わって BaseClass
のコンストラクタを変えたくなったときです。すると子クラスのコンストラクタもすべて直す必要が出てきます。こうしてその変更は地獄になります。
どうすれば防げるでしょうか。答えは継承よりコンポジションを優先することです。
そこでコードの設計を変えます。抽象の Base*
クラスを避けるのです。MyClass が BaseClass
を継承して機能を得る代わりに、その機能を依存関係として渡します。
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
セッターインジェクション
依存関係は、それを private
なプロパティに保存するメソッドを呼ぶことで渡されます。こうしたメソッドの一般的な命名は
set*() の形なのでセッターと呼ばれますが、もちろん別の名前でもかまいません。
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
この方法は、クラスの動作に不可欠でない省略可能な依存関係に向いています。オブジェクトが実際にその依存関係を受け取る保証(つまり呼び出し側がそのメソッドを呼ぶ保証)がないからです。
同時にこの方法では、セッターを繰り返し呼んで依存関係を変えられます。それが望ましくないなら、メソッドの中にチェックを足すか、PHP
8.1 以降なら $cache プロパティに readonly フラグを付けてください。
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
セッターの呼び出しは、DI コンテナの設定の setup キーで定義します。ここでもオートワイヤリングによる依存関係の自動的な受け渡しが使われます。
services:
- create: MyClass
setup:
- setCache
プロパティインジェクション
依存関係は、メンバーのプロパティに直接書き込むことで渡されます。
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
この方法は不適切と見なされます。メンバーのプロパティを public
として宣言しなければならないからです。その結果、渡された依存関係が本当に求める型かどうかを保証する制御を失い(これは
PHP 7.4
でプロパティの型宣言が入る前にはとくに当てはまりました)、新しく代入された依存関係に対して独自のロジックで反応する、たとえばそのあとの変更を防ぐ、といったこともできなくなります。同時にそのプロパティはクラスの公開
API の一部になってしまい、それは意図しないことかもしれません。
プロパティへの代入は、DI コンテナの設定の setup セクションで定義します。
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
ここまでの 3
つの方法はあらゆるオブジェクト指向言語に一般に当てはまりますが、inject*()
メソッドや #[Inject] アトリビュートによる注入は、ふつう Nette
のプレゼンターで使われ、そこでは既定で有効です。ほかのサービスも inject: trueで使えるようにできます。これらは別の章で扱います。
どの方法を選ぶか
- コンストラクタは、クラスが動くのに絶対に必要な依存関係に向いています。
- 逆にセッターは、省略可能な依存関係や、あとで変える必要があるかもしれない依存関係に向いています。
- 公開プロパティは一般におすすめしません。