Learn how to use Laravel middleware and PSR-15 decorators for dynamic dependency injection and runtime service swapping to build flexible, testable apps.
Last month, our team hit a wall while trying to implement a multi-tenant feature that required different payment gateway implementations based on the request's origin. We were already using Laravel service container binding: Master interface-driven design, but standard service provider bindings felt too static for a request-specific requirement. We needed a way to change the implementation after the request started but before the controller logic fired.
The solution wasn't found in a complex package, but in the power of PSR-15 middleware combined with the Laravel container's runtime modification capabilities.
When you're building high-scale applications, you often reach a point where standard Laravel dependency injection: A Practical Guide to Method Injection isn't enough. You don't just need a service; you need the right service for this specific HTTP context.
We first tried using app()->bind() inside a controller, but it felt dirty—it leaked infrastructure concerns into our application layer. Instead, we moved this logic into a middleware decorator.
Here is how you can implement a pattern to swap implementations on the fly:
PHPnamespace App\Http\Middleware; use Closure; use App\Contracts\PaymentGateway; use App\Services\StripeGateway; use App\Services\PayPalGateway; use Illuminate\Http\Request; class SwapPaymentGateway { public function handle(Request $request, Closure $next) { $provider = $request->header('X-Payment-Provider'); #6A9955">// Swap the implementation in the container at runtime app()->bind(PaymentGateway::class, function () use ($provider) { return $provider === 'paypal' ? new PayPalGateway() : new StripeGateway(); }); return $next($request); } }
By binding the interface to a different concrete class inside the middleware, any class resolved via the container after this point—including your controllers—will automatically receive the swapped implementation.
This approach is essentially a form of runtime architecture. It allows you to decouple your core business logic from the infrastructure constraints of the incoming request. When you master Laravel interfaces and service contracts for cleaner architecture, you stop worrying about which class you're injecting and start focusing on what the object does.
However, there's a caveat. If you resolve your dependency before the middleware executes—perhaps in a Service Provider's boot method—the swap won't take effect. You must ensure that your service resolution is deferred until the request cycle is well underway.
You might ask why we wouldn't just use `Laravel Service Providers: A Beginner’s Guide to Bootstrapping](/blog/laravel-service-providers-a-beginner-s-guide-to-bootstrapping) to handle this. Service providers are typically instantiated early in the lifecycle. By the time they run, the request context is often not fully resolved or is too generic. Middleware, on the other hand, sits perfectly inside the request-response cycle.
app()->bind() won't work as expected because the instance is already cached. Use app()->instance() or re-register the singleton if you absolutely must swap it.StripeGateway, the container cannot swap it for PayPalGateway without throwing a TypeError.Q: Can I use this to swap database connections?
A: Yes. You can use the same pattern to dynamically change the database config using config(['database.connections.tenant.host' => '...']) inside your middleware.
Q: Is this "magic" bad for maintainability?
A: It can be. Document your middleware clearly. If a developer looks at a controller and sees PaymentGateway, they should be able to find the middleware responsible for the swap easily. Use a consistent naming convention for your middleware classes.
Q: Does this work with Queues? A: No. Middleware only runs during the HTTP request cycle. If you need to swap dependencies for queued jobs, you'll need to handle that via the Job's constructor or a dedicated Job middleware.
Architecting your application this way feels a bit like "hacking" the container, but it's a standard practice in robust Laravel development. It keeps your code dry and ensures that your services remain focused on the task at hand rather than the context of the request. Next time, I’d probably look into using a dedicated Factory pattern to clean up the binding logic inside the middleware, as the if/else block can get messy once you support more than two providers. We're still iterating on this, but for now, it's saved us roughly three days of refactoring time across our multi-tenant modules.
Laravel tail latency can kill your p99 performance. Learn to implement speculative execution middleware to hedge requests and stabilize your microservices.