Introduction
When upgrading from Lumen v10 to v11, have you ever been confused by unexpected behavior?
I encountered this issue myself when the changes to Contextual Binding caused previously functional code to stop working.
In this article, I will explain the changes to Contextual Binding with detailed sample code. We’ll look at how it worked in v10, the issues that arise in v11, and how to fix them step by step.
Reference: If you’d like to learn more about the basics of Contextual Binding, please refer to the official Laravel documentation.
Background of the Changes
【Key Changes】
Lumen v10: `when` method-based Contextual Binding applied across the entire DI chain (including indirect dependencies).
Lumen v11: `when` method-based Contextual Binding applies only when directly injected into the specified class.
In Lumen v10, the when
method allowed Contextual Binding to propagate across the entire dependency injection (DI) chain.
In other words, a concrete class specified for one class's dependency was also reflected in subsequent layers of the DI chain.
However, as of v11, this behavior has changed. The when
method now applies only to cases where the specified class is directly injected.
As a result, code that previously worked might stop functioning as intended.
Dependency Diagram
Here’s a diagram showing the dependencies discussed in this article:
Behavior in v10:
CreditController ──depends→ MoneyService ──depends→ PaymentInterface
│ ↑
└────────────when().needs()───────────────────────┘
(The PaymentInterface binding is correctly applied from CreditController)
Behavior in v11:
CreditController ──depends→ MoneyService ──depends→ PaymentInterface
│ │ ↑
└─when().needs()─╳ └─when().needs()──────────┘
(The binding only applies to direct dependencies)
Let’s explore specific cases where this issue arises, along with code examples.
Example Code in v10
First, let’s look at example code that worked correctly in Lumen v10.
Configuration in 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';
}
};
});
Here, PaymentInterface
is configured to switch its concrete class based on CreditController
.
CreditController
class CreditController extends Controller
{
public function __construct(private MoneyService $moneyService)
{
}
public function index(): string
{
return $this->moneyService->paymentMethod();
}
}
CreditController
injects MoneyService
, which internally calls the paymentMethod
of PaymentInterface
.
MoneyService
class MoneyService
{
public function __construct(private PaymentInterface $paymentInterface)
{
}
public function paymentMethod(): string
{
return $this->paymentInterface->paymentMethod();
}
}
MoneyService
is a class that depends on PaymentInterface
.
Execution Result
When this code is executed in Lumen v10, calling the index
method of CreditController
correctly returns 'credit card'
.
Issues in v11
What happens if we run the same code in Lumen v11?
Cause of the Issue
CreditController
injects MoneyService
, which in turn injects PaymentInterface
.
However, in v11, the when
method applies only when directly injecting into the specified class. Therefore, even though CreditController
is the basis of the configuration, the PaymentInterface
passed to MoneyService
remains the default, and 'default'
is returned.
Execution Result
Calling CreditController
returns 'default'
instead of the expected 'credit card'
.
Example of a Failing Test
No explicit error message appears, but the return value differs from expectations. For example, the following test will fail:
// Test code
public function testCreditControllerReturnsCorrectPaymentMethod()
{
$response = $this->get('/credit');
$this->assertEquals('credit card', $response->getContent());
// Fails in v11 - 'default' is returned
}
In real-world environments, this discrepancy can cause features to break, requiring time-consuming debugging.
Fixed Code Example (v11-Compatible)
How can we make this code work correctly in Lumen v11?
The fix is simple: change the basis of the when
method to MoneyService
.
Configuration in 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);
});
Key Fix
`needs(PaymentInterface::class)` → `needs(MoneyService::class)`
Consider the DI chain, and specify the class that directly requires the interface.
The key here is changing the basis of the needs
method from PaymentInterface
to MoneyService
.
This ensures that the correctly configured PaymentInterface
is injected into 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();
}
}
Execution Result
With this fixed code, calling the index
method of CreditController
correctly returns 'credit card'
.
Conclusion
In summary, Lumen v11 enforces stricter scoping for Contextual Binding.
As a result, when using the when
method, you need to carefully specify the correct class within the DI chain.
If you’ve encountered this behavior during an upgrade, please share your experience in the comments! If you know of alternative solutions or related documentation, feel free to share those as well :)