オートワイヤリング
オートワイヤリングは、必要なサービスをコンストラクタやほかのメソッドに自動的に渡してくれる素晴らしい機能で、明示的に指定する必要がなくなります。多くの時間を節約できます。
おかげで、サービスの定義を書くときに大部分の引数を省けます。次のように書く代わりに、
services:
articles: Model\ArticleRepository(@database, @cache.storage)
こう書くだけで済みます。
services:
articles: Model\ArticleRepository
オートワイヤリングは型に導かれるので、それが働くには ArticleRepository
クラスがおおよそ次のように定義されている必要があります。
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
オートワイヤリングはサービスの名前を決して使いません。PHP の型システムだけに導かれるので、クラスが実装するインターフェースや継承するクラスも満たすと分かります。おかげでサービスの名前は補助的な識別子にすぎず、名前を変えてもアプリケーションの何も壊れません。
オートワイヤリングを使うには、コンテナの中に各型のサービスがちょうどひとつなければなりません。複数あると、オートワイヤリングはどれを渡すべきか分からず例外を投げます。
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # 例外を投げます。mainDb と tempDb の両方が当てはまります
ひとつの解は、オートワイヤリングを迂回してサービス名を明示的に指定すること(たとえば
articles: Model\ArticleRepository(@mainDb))です。とはいえ、いずれかのサービスのオートワイヤリングを無効にするか、ひとつのサービスをほかより優先するほうが便利です。
オートワイヤリングの無効化
autowired: false
オプションを使うと、サービスのオートワイヤリングを無効にできます。
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # tempDb サービスはオートワイヤリングから除かれます
articles: Model\ArticleRepository # なのでコンストラクタには mainDb が渡されます
articles サービスは、コンストラクタに使える PDO サービスが 2
つ(mainDb と tempDb)あるという例外を投げません。mainDb
サービスだけを考慮するからです。
オートワイヤリングは、di › excluded設定オプションを使って型ごとにまとめて無効にすることもできます。ここには、決してオートワイヤリングされるべきでない型(とその子孫)を並べます。
Nette のオートワイヤリングの設定は Symfony とは違います。Symfony では
autowire: false
は、そのサービスのコンストラクタの引数にオートワイヤリングを使わない、という意味です。Nette
では、オートワイヤリングはコンストラクタの引数と、コンテナ経由で呼ばれるほかのメソッド(セッターインジェクションなど)に当てはまります。autowired: false
オプションは、そのサービスのインスタンスがほかのサービスの依存関係として自動的に渡されるのをコンテナに禁じます。
オートワイヤリングの優先
同じ型のサービスが複数あり、そのひとつに autowired
オプションを指定すると、そのサービスが優先されるようになります。
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # 優先されるようになります
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
articles サービスは、当てはまる PDO サービスが複数(mainDb と
tempDb)あるという例外を投げず、優先されるほう、つまり
mainDb を使います。
サービスのコレクション
オートワイヤリングは、特定の型のサービスの配列を渡すこともできます。PHP
は型宣言で配列の要素の型を指定できないので、array の型宣言に ClassName[]
のように要素の型を示す phpDoc のコメントを添える必要があります。
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
すると DI コンテナは、その型に対応するサービスの配列を自動的に渡します。オートワイヤリングが無効にされたサービスは除かれ、今まさに作られているサービス自身がそのコレクションに含まれることもありません。個々のサービスを渡す場合と違い、オートワイヤリングを特定の型に絞り込んだり、サービスを優先にしたりしても、ここでは影響がありません。配列には常にその型のすべてのサービスが入ります。
コメントの型は array<int, Class> や list<Class>
の形でもかまいません。phpDoc のコメントの形を自分で決められない場合は、typed()を使って設定でサービスの配列を直接渡せます。
スカラーの引数
オートワイヤリングが働くのはオブジェクトとオブジェクトの配列だけです。スカラーの引数(文字列、数値、真偽値など)は設定で指定しなければなりません。別の方法として、そのスカラー値(や複数の値)を包む設定オブジェクトを作ることもできます。そのオブジェクトはオートワイヤリングで渡せます。
class MySettings
{
public function __construct(
// readonly は PHP 8.1 以降で使えます
public readonly bool $value,
)
{}
}
設定に足してサービスとして登録します。
services:
- MySettings('any value')
これでほかのクラスがオートワイヤリングで要求できます。
省略可能な依存関係
コンストラクタやメソッドのパラメータに既定値があり、求める型のサービスがコンテナにない場合、オートワイヤリングは例外を投げず、その引数を単に飛ばすので既定値が使われます。省略可能な依存関係はこうして宣言します。
class Foo
{
public function __construct(
private ?Logger $logger = null,
) {}
}
これに対し、既定値のないパラメータでは、サービスがないと常に例外になります。
オートワイヤリングの絞り込み
個々のサービスについて、オートワイヤリングを特定のクラスやインターフェースに絞り込めます。
ふつうオートワイヤリングは、そのサービスが型として当てはまるすべてのメソッドのパラメータにサービスを渡します。絞り込みとは、サービスが渡されるためにメソッドのパラメータの型が満たすべき条件を設けることです。
例を見てみましょう。
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
これらをすべてサービスとして登録すると、オートワイヤリングは失敗します。
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # 例外を投げます。parent と child の両方が当てはまります
childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します
parentDep サービスは Multiple services of type ParentClass found: child, parent
という例外を投げます。parent と child
の両方がそのコンストラクタに当てはまり、オートワイヤリングがどちらを選ぶか決められないからです。
そこで child サービスについて、オートワイヤリングを ChildClass
型に絞り込めます。
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # 'autowired: self' とも書けます
parentDep: ParentDependent # オートワイヤリングは parent サービスをコンストラクタに渡します
childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します
これで parentDep サービスのコンストラクタには parent
サービスが渡されます。当てはまるオブジェクトがそれだけになったからです。child
サービスはもうそこへオートワイヤリングされません。たしかに child サービスは今も
ParentClass 型ですが、絞り込みの条件 autowired: ChildClass により、明示的に
ChildClass(またはその派生型)と型付けされたパラメータにしか渡されません。ParentDependent
は ParentClass を求めるので、child
サービスはそこでのオートワイヤリングの候補から外れます。
child サービスの autowired: ChildClass は autowired: self
とも書けます。self は現在のサービスのクラスを表すプレースホルダーだからです。
autowired
キーには、複数のクラスやインターフェースを配列として指定することもできます。
autowired: [ParentClass, FooInterface]
例にインターフェースを足してみましょう。
interface FooInterface
{}
interface BarInterface
{}
class ParentClass implements FooInterface
{}
class ChildClass extends ParentClass implements BarInterface
{}
class FooDependent
{
function __construct(FooInterface $obj)
{}
}
class BarDependent
{
function __construct(BarInterface $obj)
{}
}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
child
サービスを何も制限しなければ、FooDependent、BarDependent、ParentDependent、ChildDependent
のすべてのクラスのコンストラクタに当てはまり、オートワイヤリングはそこへ渡します。
しかし autowired: ChildClass(または self)でオートワイヤリングを
ChildClass に絞り込むと、渡されるのは ChildDependent
のコンストラクタだけになります。そこは ChildClass
型の引数を求めており、ChildClass は ChildClass
であるからです。ほかのパラメータが求める型はどれも ChildClass
でもその派生型でもないので、サービスは渡されません。
autowired: ParentClass で ParentClass に制限すると、オートワイヤリングは再び
ChildDependent のコンストラクタに渡し(求められる ChildClass は
ParentClass の派生型だからです)、さらに ParentDependent
のコンストラクタにも渡します。求められる型 ParentClass も適合するからです。
FooInterface に制限すると、引き続き ParentDependent(求められる
ParentClass は FooInterface の派生型です)と ChildDependent
にオートワイヤリングされ、加えて FooDependent
のコンストラクタにも渡されますが、BarDependent には渡されません。BarInterface
は FooInterface の派生型ではないからです。
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # オートワイヤリングは child サービスをコンストラクタに渡します
barDep: BarDependent # 例外を投げます。当てはまるサービスがありません
parentDep: ParentDependent # オートワイヤリングは child サービスをコンストラクタに渡します
childDep: ChildDependent # オートワイヤリングは child サービスをコンストラクタに渡します