Follow on twitter
Jun 3rd, 2019

Redirect a request to a specific route in Laravel

Defining routes in Laravel is relatively simple, although sometimes you’ll have to deal with some tricky situations like the one I’m about to describe now.

Let’s say you have a blog with the following routes:

<?php
Route::get(‘{category}/{slug}-{id}’, ‘ArticleController@show’)->name('article');
Route::get(‘{category}/{tag}’, ‘TagController@show’)->name('tag');
view raw 1.php hosted with ❤ by GitHub

This could be a list of URLs that matches with those routes:

// Tag routes
http://localhost/movies/accion
http://localhost/movies/comedy
http://localhost/movies/kids
http://localhost/movies/lord-of-the-rings-2
// Article routes
http://localhost/movies/10-action-movies-for-the-weekend-123
http://localhost/movies/2020-upcoming-movies-124
http://localhost/movies/top-100-classic-movies-125
view raw 2.text hosted with ❤ by GitHub

Nothing wrong there, right?

Well, if you look closer, we have a “tag route” that looks like an “article route”: http://localhost/movies/lord-of-the-rings-2

When a user tries to access the URL above, the application is going to handle that request using the ArticleController, which is not Ideal.

Now the client calls you saying that they have an increasing number of 404s on their website, and just going to the editors and telling them “you can’t create a tag that ends with a number (some-tag-123)” is not a solution.

So, you need to identify those requests and redirect to the right controller.

<?php
class ArticleController
{
public function show($category, $slug, $id)
{
$article = Article::find($id);
if (empty($article)) {
return app(\App\Http\Controllers\TagController::class)
->show($category, $slug.'-'.$id)
}
return view('article')->withArticle($article);
}
}
view raw 3.php hosted with ❤ by GitHub

Note: the controller receives the parameters in the same order defined in the route: Route::get('{category}/{tag}-{id}', 'ArticleController').

Resolving a controller class from the container, and then, call the specific controller action, is one of the options you have to solve this issue, although it's not the ideal situation and will leave you with some loose ends all over the place:

app(\App\Http\Controllers\TagController::class)->show($category, $slug.'-'.$id)

On this example, the ArticleController receives three parameters, but the TagController receives only two.

<?php
class TagController
{
public function show($category, $tag)
{
// do somehting
return view('tag');
}
}
view raw 4.php hosted with ❤ by GitHub

What happens if, instead of passing each param in the controller, you are using the Request object as follows:

<?php
class TagController {
public function show(Request $request) {
$tag = $request->tag;
$category = $request->category;
// do somehting
return view('tag');
}
}
view raw 5.php hosted with ❤ by GitHub

This will introduce another problem to our current situation, remember that the request was captured by the ArticleController in the first place, through the article route.

That means that the current request object has three parameters:

<?php
// Route::get('{category}/{slug}-{id}');
// http://localhost/movies/lord-of-the-rings-2
$request->category; // movies
$request->slug; // lord-of-the-rings
$request->id // 2
view raw 6.php hosted with ❤ by GitHub

So, if you want to redirect the same request to the TagController when you try to access the tag parameter, this is what you are going to get:

<?php
class ArticleController
{
public function show(Request $request)
{
return app(\App\Http\Controllers\TagController::class)
->show($request)
// ...
}
}
class TagController
{
public function show(Request $request)
{
$request->category; // movies
$request->tag; // lord-of-the-rings
}
}
view raw 7.php hosted with ❤ by GitHub

As you can see, you are missing the -2 portion of the tag lord-of-the-rings-2.

What happen if we just the redirect()->route() method?

You’ll get a too many redirection error because the redirect()->route() will construct the URL with the given params and will make a new call to that URL, not to the specific route:

<?php
// In the ArticleController
return redirect()->route('tag', [
'category' => $request->category,
'tag' => $request->slug . '-' $request->id
]);
view raw 8.php hosted with ❤ by GitHub

The code above basically will make a call to /movies/lord-of-the-rings-2, which is going to be captured by the ArticleController once again.

So we need a different solution here, we need to redirect the request to a specific route

Redirecting a request to a specific route in Laravel

The key of this approach relies on the Illuminate/Routing/Router.php class, which by the way, is the instance you get when you use the Route facade.

Check out the following function from the router method:

<?php
public function respondWithRoute($name)
{
$route = tap($this->routes->getByName($name))->bind($this->currentRequest);
return $this->runRoute($this->currentRequest, $route);
}
view raw 9.php hosted with ❤ by GitHub

Once the application get’s the request, the router will run the respondWithRoute() method, to send the current request to the given route: return $this->runRoute($this->currentRequest, $route);

The runRoute method is protected, so we can't call it from outside the class.

Fortunately for us, the Router class implements the Macroable trait; that means that we can add macros (custom methods) to the class.

The solution

<?php
Route::macro(
'sendToRoute',
function (Request $request, string $routeName) {
$route = tap($this->routes->getByName($routeName))->bind($request);
$this->current = $route;
return $this->runRoute($request, $this->current);
}
);
view raw 10.php hosted with ❤ by GitHub

This macro will receive a request object as a first argument, and the “name” of the route that we want to handle the given request.

This is how we can use the new macro to solve our problem:

<?php
class ArticleController
{
public function show(Request $request)
{
//...
// is not and article? then redirect
return \Route::sendToRoute($request, 'tag');
}
}
view raw 11.php hosted with ❤ by GitHub

In this case, we are not only redirecting the request to a new controller-action, but actually, the URL arguments will be resolved using the given route; basically we are forcing the application to resolve the request with the specified route.

After redirecting the request with this method you can do request()->route()->getName() and get the name of the exact route you are redirecting to, instead of the one that captured the request in the first place.

Also, the route arguments will be resolved again for the new route, and now in your TagController you’ll get the following:

<?php
$request->category; // movies
$request->tag; // lord-of-the-rings-2
view raw 12.php hosted with ❤ by GitHub

Wrapping up

When you use the redirection methods Laravel offers out of the box, basically you are making a new call to an URL instead of forcing the application to handle the request on a specific way. Take that into consideration next time you encounter yourself in a similar situation as the one described on this article.

Jeff
5 months ago
Share: