Let's say you have a route, and depending on certain conditions, the page changes drastically. We typically create multiple routes, one for each case, allowing us to split responsibilities. But what if you really want to keep everything under the same route?
The first solution that comes to mind is to have a bunch of `if/else` sentences in the controller, and based on that, we do different things, render different views, etc. Or maybe create some services that are dynamically injected into the controller based on certain conditions. But this doesn't sound like the Laravel way.
I always thought that controllers are kind of static entities; we just load them in the route as an entry point, and that's it. But what if I told you we could load them dynamically based on certain conditions, while maintaining the same route?
This is what we are going to talk about in this article.
Let's bind controllers dynamically.
It occurred to me that maybe we can set a contract instead of the controller in the route, something like this:
Route::get('this-route/{resource}', ThisControllerContract::class)->name('this.route');
It turns out this is possible! We need to do a couple of things to make it work. Let me explain it with an example from my previous article, "How to Extend MustVerifyEmail.".
In that article, I extended the email verification feature to add my own phone verification. For the verification page, I wanted to keep the same route, but change the way I handle it depending on whether your email or phone number is not verified.
So, I created a new interface and used it on the route like this:
Route::get('verify', UserVerificationPromptControllerContract::class)
->name('verification.notice');
And I defined the interface like this:
interface UserVerificationPromptController
{
public function __invoke(): mixed;
}
In this case, I don't want to bind any route item to a model, so I don't add any parameter to the `__invoke` method. But, if you have any route model binding in your case, you need to add them as parameters; this is only the case for route model binding.
Then, in a service provider, we bind the controllers like this:
public function register(): void
{
$this->app->bind(UserVerificationPromptController::class, function (Application $app) {
/** @var ?User $user */
$user = request()->user();
if ($user && ! $user->hasVerifiedPhoneNumber()) {
return $this->app
->make(PhoneVerificationPromptController::class);
}
return $this->app->make(EmailVerificationPromptController::class);
});
}
See how I can get the request data, in this case the user, to define what controller I want to inject into the route.
And it's that simple! Now you can have multiple controllers that handle the same route. And I just have to say... This is the way.