I have two models that I don't want to have a prefix in front of its URLs. E.g. Users and Posts
If I have a URL https://example.com/title-of-the-post and https://example.com/username
I will do something like this in the web.php
routes file:
// User
Route::get('{slug}', function ($slug) {
$user = User::whereSlug($slug)->get();
return view('users.show', $user);
});
// Post
Route::get('{slug}', function ($slug) {
$post = Post::whereSlug($slug)->get();
return view('posts.show', $user);
});
Now the issue I am facing is that the first route is entered and will never reach the second even if there is no model with a matching slug.
How can I exit to the next route (Post) if $user
is not found?
Note: I have tried many different exit strategies but none seem to work.
return;
return false;
return null;
// and return nothing
Thanks in advance!
UPDATE:
Another issue is that if I have other resource
routes they too get blocked by the first route.
E.g. If I have Route::resource('cars', 'CarController')
it generates a /cars
path which matches the {slug} and is also being blocked by first User route.
I think you already got the idea but I ,kind of, have a similar setup in my application and in my particular case, I also needed to be able to catch multi-segments routes.
So here's how I did it. For example, the last route in my web.php
is the following.
Route::get('{catchall}', 'SlugRoutesController@route')->where('catchall', '.*');
The where class ->where('catchall', '.*');
ensures that we are also able to catch slugs that have multiple segments.
For example, the following routes will all be matched:
/blog/this-is-an-article
/user/mozammil/articles
Then, in my SlugRoutesController
, I am able to inject my other Controller dependencies.
<?php
namespace App\Http\Controllers;
use App\Post;
use App\User;
use PostController;
use UserController;
use Illuminate\Http\Request;
class HomeController extends Controller
{
private $postController;
private $userController;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct(UserController $userController, PostController $postController)
{
$this->userController = $userController;
$this->postController = $postController;
}
public function route(Request $request, string $slug)
{
$post = Post::where('slug', $slug)->first();
if(post) {
return $this->postController->index($request);
}
$user = User::where('slug', $slug)->first();
if($user) {
return $this->userController->index($request);
}
abort(404);
}
}
My actual controller is a bit more complex than that, but you get the idea.
You should check both of them in one route :
Route::get('{slug}', function ($slug)
{
$user = User::whereSlug($slug)->first();
if ($user)
{
return view('users.show', $user);
}
else
{
$post = Post::whereSlug($slug)->first();
if ($post)
{
return view('posts.show', $post);
}
else
{
abort(404);
}
}
});
However, it can be more cleaner. But the concept is there.
Not sure if this is best practice but here is what was done to accomplish what I needed.
Created a function on the Post model that checks if the slug is calling a post. It uses both a regex and database lookup.
public static function isRequestedPathAPost() {
return !preg_match('/[^\w\d\-\_]+/', \Request::path()) &&
Post::->whereSlug(\Request::path())->exists();
}
Then I wrap the Route in a if
statement like this.
if (\App\Models\Post::isRequestedPathAPost()) {
Route::get('{slug}', 'PostController@show');
}
Now the route is only used if it actually exist. You can put this at the bottom of the route file to reduce unnecessary lookups to the database.