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();
    }
}
CreditController は MoneyService をDIし、その中で PaymentInterface の paymentMethod を呼び出しています。
MoneyService
class MoneyService
{
    public function __construct(private PaymentInterface $paymentInterface)
    {
    }
    public function paymentMethod(): string
    {
        return $this->paymentInterface->paymentMethod();
    }
}
MoneyService は PaymentInterface をDIするクラスです。
実行結果
このコードをLumen v10で実行すると、CreditController の index メソッドを呼び出した際に 'credit card' が正しく返されます。
v11の問題例
では、このコードをそのままLumen v11で実行するとどうなるでしょうか?
問題の原因
CreditController は MoneyService をDIし、さらに MoneyService は PaymentInterface を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();
    }
}
実行結果
この修正版コードを実行すると、CreditController の index メソッドを呼び出した際に 'credit card' が正しく返されるようになります。
まとめ
今回の変更点を振り返ると、Lumen v11ではContextual Bindingのスコープがより厳密になったようです。
その結果、when メソッドを使用する際には、DIチェーンを考慮して基準となるクラスを正しく指定する必要があります。
この仕様変更については、v10->v11のアップグレードガイドにも記述がなかったような気がするんですが、「どこかに書いてたよ!」とか「こうすればv10のコードのまま動くよ!」などあればコメント欄にて教えてください🙇
Discussion