LoginSignup
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-06

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. 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()を追加で実行したい、というものです。

クラス図

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())が次のようになっていました。

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
    $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のバインド方法を定義するサービスプロバイダです。

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で次のように定義されています。

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. 参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5