[Laravel / PHP] DIコンテナの依存解決で、抽象クラスやインタフェースに対してフック的な追加処理を実行してもらう

# 1. はじめに

私は職場でLaravelを使っています。Laravelのサービスコンテナは依存性の注入(DI)が手軽にできて便利ですよね。

今回、DIコンテナが依存解決する際、オブジェクトが特定のインタフェースや抽象クラスを実装している場合に追加の処理をおこなうようにしたいと思い、調べてみたものです。


# 2. 環境

- PHP 7.2
- Laravel 6.0


# 3. いきさつ

## 3.1. 通常の事例

通常、依存解決で生成されたオブジェクトに何らかの追加処理をおこなってから注入してほしい場合、[公式ドキュメント](https://github.com/laravel/docs/blob/6.x/container.md#extending-bindings)によると、サービスプロバイダ内で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. The `extend` 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()`を追加で実行したい、というものです。


![クラス図](http://www.plantuml.com/plantuml/svg/TP6xJaCn38PtFuNL2OJw1WozJ4mCJ9sEt9CV4iYbYbtG4V3kf58ZT5hLcvClts_5ApMfTaoHo0S7HZhOn0bfZwga7o4Vf4fYzUBALyXl_2fHGKIoLwio6ezHL7cnFlTF-HsZ0GiMvGuDYlePHl0tSQk71FF5lVXoZ6w3hSG8jvcs9Pk4Z7ftJxz4P_6_i5SJTr-7disZpiW6bTFXS8dkOEjSvdo39zXmyO2knd_yMIvxzetRcUC6SvNL_fDMoEwqY3y0)


# 4. 試したこと

最初、とりあえず次のようなコードを書いて動かしてみました。

```php
<?php
    // 親クラスに対して追加の処理を登録する
    $this->app->extend(
        AbstractInjectee::class, function ($injectee, $app) {
            $injectee->methodShouldBeCalledByContainer();
            return $injectee;
        });
```

しかし、これは実際に`AbstractInjectee`じたいが依存解決されるわけではない(利用側のタイプヒントが`AbstractInjectee`になっているわけではない)ため、期待したような動きにはなりせん。


続いて次のように書きました。

```php
<?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:Illumiate/Container/Container.php
<?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
<?php
    $this->app->resolving(
        AbstractInjectee::class, function ($injectee, $app) {
            $injectee->methodShouldBeCalledByContainer();
            return $injectee;
        });
```

[公式ドキュメント](https://github.com/laravel/docs/blob/6.x/container.md#container-events)にも`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
<?php
    $this->app->afterResolving(
        AbstractInjectee::class, function ($injectee, $app) {
            $injectee->methodShouldBeCalledByContainer();
            return $injectee;
        });
```

入力値のバリデーションチェックで使う[`FormRequest`](https://github.com/laravel/docs/blob/6.x/validation.md#form-request-validation)クラスの依存解決では、この`afterResolving()`が呼ばれているようでした。

とはいえ、`resolving()`で登録したコールバックを実行した直後に`afterResolving()`で登録したコールバックも呼ばれるため、私にはいまいち使いわけがわかりませんでした。特に理由がなければ`resolving()`で登録しておけばいいと思います。


## 2019/10/08追記:


`FormRequest`のDIの定義に目をとおしなおしていたところ、これらのふたつのメソッドの使いわけの好例に思えたのでご紹介します。

次のソースは`FormRequest`のバインド方法を定義するサービスプロバイダです。

```php:Illuminate/Foundation/Providers/FormRequestServiceProvider.php
<?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:Illuminate\Validation\ValidatesWhenResolvedTrait.php
<?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()`を使う。

と考えることができそうです。


# 6. 参考

- [LaravelのDIコンテナはどう使われているのか - 新人Webエンジニアの記録。](http://blog.fagai.net/2016/09/17/laravel-dependency-injection/)
- [Laravelでセッターインジェクションする - はらへり日記](https://sota1235.hatenablog.com/entry/2016/12/09/105841)