🫠

Contextual Bindingの仕様変更について(Lumen v10 -> v11)

に公開

はじめに

Lumen v10からv11にアップグレードする際、意図しない動作に戸惑った方もいるのではないでしょうか?
私は、Contextual Bindingの仕様変更により、以前は正常に動作していたコードが機能しなくなるケースに遭遇しました。

この記事では、Contextual Bindingに関する変更点を具体的なサンプルコードと一緒に解説します。v10での動作例、v11で発生する問題、そしてその解決方法を順を追って説明していきます。

参考: Contextual Bindingの基本について詳しく知りたい方は、Laravel公式ドキュメントをご覧ください。


仕様変更の背景

Lumen v10では、when メソッドを使ったContextual Bindingが、DI(依存注入)のチェーンを飛び越えて適用される仕組みになっていました。
つまり、あるクラスの依存先に設定した具象クラスが、その先のDIにも反映されていたのです。

しかし、v11ではこの挙動が変更され、when メソッドで指定したクラスに直接DIされる場合にのみ適用されるようになりました。
これにより、今まで動いていたコードが意図せず動作しなくなることがあります。

依存関係の図解

以下がこの記事で扱う依存関係の図です:

v10の動作:
CreditController ──依存→ MoneyService ──依存→ PaymentInterface
     │                                        ↑
     └────────────when().needs()──────────────┘
     (CreditControllerからの指定がPaymentInterfaceに届く)

v11の動作:
CreditController ──依存→ MoneyService ──依存→ PaymentInterface
     │                       │                ↑
     └─when().needs()─╳      └─when().needs()─┘
     (直接の依存先にのみ適用される)

では、具体的にどのようなケースで問題が発生するのか、コード例を使って見ていきましょう。


v10のコード例

まずは、Lumen v10で正常に動作していたコードの例から見ていきます。

AppServiceProvider内の設定

$this->app->bind(PaymentInterface::class, function ($app) {
    return new class implements PaymentInterface {
        public function paymentMethod(): string
        {
            return 'default';
        }
    };
});

$this->app
    ->when(CreditController::class)
    ->needs(PaymentInterface::class)
    ->give(function () {
        return new class implements PaymentInterface {
            public function paymentMethod(): string
            {
                return 'credit card';
            }
        };
    });

ここでは、CreditController を基準に PaymentInterface の具象クラスを切り替えるよう設定しています。

CreditController

class CreditController extends Controller
{
    public function __construct(private MoneyService $moneyService)
    {
    }

    public function index(): string
    {
        return $this->moneyService->paymentMethod();
    }
}

CreditControllerMoneyService をDIし、その中で PaymentInterfacepaymentMethod を呼び出しています。

MoneyService

class MoneyService
{
    public function __construct(private PaymentInterface $paymentInterface)
    {
    }

    public function paymentMethod(): string
    {
        return $this->paymentInterface->paymentMethod();
    }
}

MoneyServicePaymentInterface をDIするクラスです。

実行結果

このコードをLumen v10で実行すると、CreditControllerindex メソッドを呼び出した際に 'credit card' が正しく返されます。


v11の問題例

では、このコードをそのままLumen v11で実行するとどうなるでしょうか?

問題の原因

CreditControllerMoneyService をDIし、さらに MoneyServicePaymentInterface をDIしています。
しかし、v11では when メソッドで指定したクラスに直接DIされる場合にしか適用されなくなりました。そのため、CreditController を基準に設定しても、その中で使用される MoneyService に渡される PaymentInterface はデフォルトのまま、default が返されてしまいます。

実行結果

CreditController を呼び出しても、期待していた 'credit card' ではなく、デフォルト設定の 'default' が返ってしまいます。

発生するエラーの例

明示的なエラーメッセージは表示されませんが、戻り値が期待と異なる結果になります。例えば以下のようなテストを実施すると失敗します:

// テストコード
public function testCreditControllerReturnsCorrectPaymentMethod()
{
    $response = $this->get('/credit');
    $this->assertEquals('credit card', $response->getContent());
    // v11で失敗する - 'default'が返される
}

実際の開発環境では、このような挙動の違いが原因で機能が正しく動作しなくなり、トラブルシューティングに時間がかかることがあります。


修正版コード例(v11対応)

では、Lumen v11で正しく動作させるにはどうすれば良いのでしょうか?
答えはシンプルで、when メソッドで設定する基準を MoneyService に変更するだけです。

AppServiceProvider内の設定

$this->app->bind(PaymentInterface::class, function ($app) {
    return new class implements PaymentInterface {
        public function paymentMethod(): string
        {
            return 'default';
        }
    };
});

$this->app
    ->when(CreditController::class) 
    ->needs(MoneyService::class)
    ->give(function () {
        $creditCardInterface = new class implements PaymentInterface {
            public function paymentMethod(): string
            {
                return 'credit card';
            }
        };
        return new MoneyService($creditCardInterface);
    });

ここでのポイントは、needs メソッドの基準を PaymentInterface ではなく MoneyService に変更している点です。
これにより、正しい PaymentInterfaceをDIした MoneyService が使用されるようになります。

CreditController

class CreditController extends Controller
{
    public function __construct(private MoneyService $moneyService)
    {
    }

    public function index(): string
    {
        return $this->moneyService->paymentMethod();
    }
}

MoneyService

class MoneyService
{
    public function __construct(private PaymentInterface $paymentInterface)
    {
    }

    public function paymentMethod(): string
    {
        return $this->paymentInterface->paymentMethod();
    }
}

実行結果

この修正版コードを実行すると、CreditControllerindex メソッドを呼び出した際に 'credit card' が正しく返されるようになります。


まとめ

今回の変更点を振り返ると、Lumen v11ではContextual Bindingのスコープがより厳密になったようです。
その結果、when メソッドを使用する際には、DIチェーンを考慮して基準となるクラスを正しく指定する必要があります。

この仕様変更については、v10->v11のアップグレードガイドにも記述がなかったような気がするんですが、「どこかに書いてたよ!」とか「こうすればv10のコードのまま動くよ!」などあればコメント欄にて教えてください🙇


参考資料

Studio Tech Blog

Discussion