1. はじめに
私は職場でLaravelを使っています。Laravelのサービスコンテナは依存性の注入(DI)が手軽にできて便利ですよね。
今回、DIコンテナが依存解決する際、オブジェクトが特定のインタフェースや抽象クラスを実装している場合に追加の処理をおこなうようにしたいと思い、調べてみたものです。
2. 環境
- PHP 7.2
- Laravel 6.0
3. いきさつ
3.1. 通常の事例
通常、依存解決で生成されたオブジェクトに何らかの追加処理をおこなってから注入してほしい場合、公式ドキュメントによると、サービスプロバイダ内でDIコンテナのextend()
メソッドを呼出すことで、これを実現できるとあります。
Extending Bindings
The
extend
method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. Theextend
method accepts a Closure, which should return the modified service, as its only argument:$this->app->extend(Service::class, function ($service) { return new DecoratedService($service); });
上記の公式の例では、Service
クラスの生成時にDecoratedService
クラスでラップしてから返させています。これにより、Service
を利用する側は最初からDecoratedService
オブジェクトを使うことができるようになります。
3.2. 悩んだ事例
これに対し私が今回悩んでいたのは、追加の処理をおこないたいクラスが、ある抽象クラスのサブクラスである、という点でした。
例えば、下図のようなクラス構成になっている場合、AbstractInjectee
のサブクラスすべてに対して、methodShoulBeCalledByContainer()
を追加で実行したい、というものです。
4. 試したこと
最初、とりあえず次のようなコードを書いて動かしてみました。
<?php
// 親クラスに対して追加の処理を登録する
$this->app->extend(
AbstractInjectee::class, function ($injectee, $app) {
$injectee->methodShouldBeCalledByContainer();
return $injectee;
});
しかし、これは実際にAbstractInjectee
じたいが依存解決されるわけではない(利用側のタイプヒントがAbstractInjectee
になっているわけではない)ため、期待したような動きにはなりせん。
続いて次のように書きました。
<?php
$proc = function ($injectee, $app) {
$injectee->methodShouldBeCalledByContainer();
return $injectee;
};
// サブクラスひとつひとつに対し、追加処理を登録する
$this->app->extend(Injectee::class, $proc);
$this->app->extend(OtherInjectee::class, $proc);
$this->app->extend(YetAnotherInjectee::class, $proc);
とりあえずこれで動いたので、しばらくはこの方法でやっていたんですが、サブクラスが増えるたびにコンテナ側の処理を追加するのは面倒だな~と…。Laravelなら何か方法が用意されているのでは??と考え、調べてみました。
5. こう書けばいいみたい
何かいい方法はないかな~とLaravelのソースコードを見ていたところ、生成したオブジェクトに対し、追加で実行するコールバックを収集する処理(Container::getCallbacksForType()
)が次のようになっていました。
<?php
/**
* Get all callbacks for a given type.
*
* @param string $abstract
* @param object $object
* @param array $callbacksPerType
*
* @return array
*/
protected function getCallbacksForType($abstract, $object, array $callbacksPerType)
{
$results = [];
foreach ($callbacksPerType as $type => $callbacks) {
if ($type === $abstract || $object instanceof $type) {
$results = array_merge($results, $callbacks);
}
}
return $results;
}
上のソースコードのなかの$object
が実際に依存解決で生成されたオブジェクトです。$callbacksPerType
内の各コールバックが実行対象の処理であるかどうかの判定を、instanceof
を使っておこなっています。
つまり、このメソッドに渡される$callbacksPerType
の元の配列に、おこないたい処理を抽象クラス名義で登録できれば、サブクラスの生成時にもそれが適用されるということになります。
$callbacksPerType
として渡される配列にコールバックを登録する方法は、次のふたとおりあります。
5.1. resolving()
を使って登録する
<?php
$this->app->resolving(
AbstractInjectee::class, function ($injectee, $app) {
$injectee->methodShouldBeCalledByContainer();
return $injectee;
});
公式ドキュメントにもresolving()
メソッドの記載はありますが、抽象クラスの処理を登録すれば子クラスに対しても処理が実行されるとは思わず…orz
The injectee container fires an event each time it resolves an object. You may listen to this event using the
resolving
method:$this->app->resolving(function ($object, $app) { // Called when container resolves object of any type... }); $this->app->resolving(HelpSpot\API::class, function ($api, $app) { // Called when container resolves objects of type "HelpSpot\API"... });
As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consumer.
5.2. afterResolving()
を使って登録する
ドキュメントに記載がありませんが、afterResolving()
というメソッドも用意されており、こちらを経由して登録することもできます。
<?php
$this->app->afterResolving(
AbstractInjectee::class, function ($injectee, $app) {
$injectee->methodShouldBeCalledByContainer();
return $injectee;
});
入力値のバリデーションチェックで使うFormRequest
クラスの依存解決では、このafterResolving()
が呼ばれているようでした。
とはいえ、resolving()
で登録したコールバックを実行した直後にafterResolving()
で登録したコールバックも呼ばれるため、私にはいまいち使いわけがわかりませんでした。特に理由がなければresolving()
で登録しておけばいいと思います。
2019/10/08追記:
FormRequest
のDIの定義に目をとおしなおしていたところ、これらのふたつのメソッドの使いわけの好例に思えたのでご紹介します。
次のソースはFormRequest
のバインド方法を定義するサービスプロバイダです。
<?php
public function boot()
{
$this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
$resolved->validateResolved();
});
$this->app->resolving(FormRequest::class, function ($request, $app) {
$request = FormRequest::createFrom($app['request'], $request);
$request->setContainer($app)->setRedirector($app->make(Redirector::class));
});
}
依存解決の定義が2つあります。
まず下側のresolving()
による依存解決では、FormRequest
の生成後、不足している必要なプロパティなどをサービスコンテナからセッタをとおして充てはめており、この処理によって意味合いとしてFormRequest
のできあがり、というようになるようです。
続いて上側のafterResolving()
によって登録された処理では、ValidatesWhenResolved
インタフェースを実装しているクラスのインスタンスに対し、オブジェクトの生成処理後、validateResolved()
メソッドを実行するよう指示しています。実はFormRequest
はこのインタフェースを実装しており、このメソッドの実装はValidatesWhenResolvedTrait
で次のように定義されています。
<?php
trait ValidatesWhenResolvedTrait
{
/**
* Validate the class instance.
*
* @return void
*/
public function validateResolved()
{
$this->prepareForValidation();
if (! $this->passesAuthorization()) {
$this->failedAuthorization();
}
$instance = $this->getValidatorInstance();
if ($instance->fails()) {
$this->failedValidation($instance);
}
$this->passedValidation();
}
このメソッドこそ、コントローラのアクションに入る前に、自動で走ってくれるバリデーションチェックの正体です。
passesAuthorization()
で我々Laravel利用者によるauthorize()
のオーバーライドが呼ばれ、getValidatorInstance()
ではrules()
に定義したバリデーション要件が満たされているかをチェックするバリデータインスタンスが返されます(別の場所でマクロ機能によって定義されています)。
認可や入力チェックで引っかかってしまった場合は、それぞれfailedAuthorization()
やfailedValidation()
が呼ばれ、内部で例外が投げられることで、403エラー画面への遷移や遷移元画面へのリダイレクトがおこなわれる、という段取りになっています。
バリデーションに関するロジックをコントローラから切離したことでコードの見通しがよくなり、自動でリダイレクトもしてくれるこれらの一連のありがたい処理は、実はresolving()
とafterResolving()
のおかげで実現されていたんですね。
ということで、resolving()
とafterResolving()
の使いわけの基準としては、
- 依存解決の一環として追加処理をおこなう場合、つまり実行時点でオブジェクトの生成が終わっていないと判断される場合であれば
resolving()
を使う。 - 初期化処理後、できあがって一人前になったオブジェクトに対し、必要な事前処理をおこないたい場合は
afterResolving()
を使う。
と考えることができそうです。