Ever found yourself writing repetitive code to fetch models based on route parameters in your Laravel applications, or fed up with manually querying IDs in your controllers? Laravel’s model binding techniques offer a clean and eloquent solution to this common problem. In this article, we’ll explore route model binding in Laravel, a elegant way to automatically inject models into your routes and controllers.
Let’s take a look at the demo code for this article, below is the routes we will be working with,
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/users/{user}/posts/{post}', [PostController::class, 'show'])
->scopeBindings()
->name('users.posts.show');
Route::get('/posts/tags/{tag}', [PostTagController::class, 'show'])->name('posts.tags.show');
Simplifying Model Retrieval with Route Model Binding #
I’m expecting you to have a basic understanding of Laravel and its routing system. Let’s start with /users/{id} route. id is the placeholder for the user ID. In a typical scenario, you would fetch the user in the controller like this,
public function show(int $id)
{
$user = User::findOrFail($id);
return view('users.show', compact('user'));
}
Above we manually retrieve the user model using the provided ID and if the user is not found, it throws a 404 error. There is nothing wrong with this approach, but Laravel provides a more elegant way to handle this using route model binding. With route model binding, you can directly type-hint the model in your controller method, and Laravel will automatically resolve it for you,
public function show(User $user)
{
return view('users.show', compact('user'));
}
Notice how we replaced the int $id parameter with User $user. Laravel automatically fetches the user model based on the ID provided in the route. If the user is not found, it will still throw a 404 error. Using route model binding we have removed the need to manually query the database for the user, making our code cleaner and more maintainable. Make sure to update the route parameter name to match the model name so Laravel can resolve it correctly.
We can apply the same technique to the /users/{user}/posts/{post} route. Here’s how the show method in PostController would look:
public function show(User $user, Post $post)
{
return view('posts.show', compact('user', 'post'));
}
Laravel will automatically resolve both the User and Post models based on the IDs provided in the route. However, there is a potential issue here: what if the post does not belong to the specified user? To handle this scenario, we can use scoped bindings. By adding the ->scopeBindings() method to the route definition, Laravel will ensure that the Post model is only resolved if it belongs to the specified User. If the post does not belong to the user, a 404 error will be thrown.
Using Slugs in URLs #
What if you want to change the default behavior of using IDs in URLs to something more user-friendly. A common approach in blogs is to use slugs instead of numeric IDs. You can do this by changing the route parameter to {post:slug} and updating the controller method accordingly,
Route::get('/users/{user}/posts/{post:slug}', [PostController::class, 'show'])
->scopeBindings()
->name('users.posts.show');
Above we specify {post:slug} which tells Laravel to use the slug column in the posts table to resolve the Post model. The controller method remains the same.
Enum Binding and Validation #
In the /posts/tags/{tag} route, we can leverage enum binding to ensure that only valid tags are accepted. The controller method without enum binding would look like this,
public function show(string $tag): View
{
$validTags = ['laravel', 'php', 'javascript', 'vue', 'react'];
if (!in_array($tag, $validTags)) {
abort(404);
}
$posts = Post::whereJsonContains('tags', $tag)->get();
return view('posts.tags.show', compact('tag', 'posts'));
}
We manually validate the tag parameter against a list of valid tags. If the tag is not valid, we throw a 404 error. With enum binding, we can simplify this process by defining an enum for the valid tags and using it in the controller method,
enum PostTag: string
{
case Laravel = 'laravel';
case PHP = 'php';
case JavaScript = 'javascript';
case Vue = 'vue';
case React = 'react';
}
public function show(PostTag $tag): View
{
$posts = Post::whereJsonContains('tags', $tag->value)->get();
return view('posts.tags.show', compact('tag', 'posts'));
}
Laravel will automatically validate the tag parameter against the defined enum values. If the tag is not valid, a 404 error will be thrown automatically.
Explicit Binding #
If you may want to customize how a model is resolved from a route parameter. Laravel allows you to define explicit bindings in the boot method of the AppServiceProvider.
Simple explicit binding #
Let’s say if someone use post as a placeholder in the route definition, this should be mapped to Post model. This can be useful if you have a different naming convention or if you want to bind a model to a different parameter name.
use Illuminate\Support\Facades\Route;
use App\Models\Post;
public function boot()
{
Route::model('post', Post::class);
}
Customizing resolution logic #
You can also customize the resolution logic by using the Route::bind method. For example, if you want to fetch a post by its slug instead of ID, you can do it like this:
use Illuminate\Support\Facades\Route;
use App\Models\Post;
public function boot()
{
Route::bind('post', function (string $value) {
return Post::where('slug', $value)->firstOrFail();
});
}
Here we define a custom binding for the post parameter that fetches the Post model based on the slug column. If no post is found with the given slug, it throws a 404 error.
Binding with additional Constraints #
You can go a little bit crazy if you want to add additional constraints while resolving a model. For example, if you want to ensure that only published posts are resolved, you can do it like this,
use Illuminate\Support\Facades\Route;
use App\Models\Post;
public function boot()
{
Route::bind('post', function (string $value) {
return Post::where('slug', $value)
->where('is_published', true)
->firstOrFail();
});
}
getRouteKeyName Method in Model #
Another way to customize how a model is resolved is by overriding the getRouteKeyName method in the model itself. This method allows you to specify which column should be used for route model binding.
public function getRouteKeyName(): string
{
return 'slug';
}
Laravel will automatically use the slug column for route model binding whenever this model is type-hinted in a controller method.