Laravel Quick Tip: Model Route Binding

Younes Rafie
Younes Rafie
Share

One of the great things that Laravel provides is the easy to use routing component. It offers simple URLs, parameters, grouping, naming and event guarding route groups, to name a few of the different options.

Laravel Logo

Let’s pretend we have a list of categories in a database, and the admin can manage categories from the back end. Here’s how your routes file should look like.

Route::group(['namespace' => 'Admin', 'prefix' => 'admin', 'middleware' => 'admin'], function () {
    Route::resource('categories', 'CategoriesController');
});

Inside your CategoriesController class, you’ll have the seven resource methods. Inside the edit action, we should check if the category being edited exists in the database, otherwise we redirect back with an error message.

public function edit($id)
{
    $category = Category::find($id);
    if (!$category) {
        return redirect()->route('admin.categories.index')->withErrors([trans('errors.category_not_found')]);
    }

    // ...
}

Model Binding

This is the usual way of doing it, but Laravel also has a nicer way of optimizing this repetitive task called route model binding. You basically type hint the model name instead of the ID parameter.

If you list the available routes, it should look something like this.

+--------+-----------+------------------------------------+------------------------------------+----------------------------------------------------------------------+-----------------+
| Domain | Method    | URI                                | Name                               | Action                                                               | Middleware      |
+--------+-----------+------------------------------------+------------------------------------+----------------------------------------------------------------------+-----------------+
|        | GET|HEAD  | admin/categories                   | admin.categories.index             | App\Http\Controllers\Admin\CategoriesController@index                | web,admin       |
|        | POST      | admin/categories                   | admin.categories.store             | App\Http\Controllers\Admin\CategoriesController@store                | web,admin       |
|        | GET|HEAD  | admin/categories/create            | admin.categories.create            | App\Http\Controllers\Admin\CategoriesController@create               | web,admin       |
|        | GET|HEAD  | admin/categories/{categories}      | admin.categories.show              | App\Http\Controllers\Admin\CategoriesController@show                 | web,admin       |
|        | PUT|PATCH | admin/categories/{categories}      | admin.categories.update            | App\Http\Controllers\Admin\CategoriesController@update               | web,admin       |
|        | DELETE    | admin/categories/{categories}      | admin.categories.destroy           | App\Http\Controllers\Admin\CategoriesController@destroy              | web,admin       |
|        | GET|HEAD  | admin/categories/{categories}/edit | admin.categories.edit              | App\Http\Controllers\Admin\CategoriesController@edit                 | web,admin       |

You can see that the routing parameter is {categories} which you can leave like that if you wish. However, Laravel provides an option to change it.

Route::resource('categories', 'CategoriesController', [
    'parameters' => 'singular',
]);
+--------+-----------+------------------------------------+------------------------------------+----------------------------------------------------------------------+-----------------+
| Domain | Method    | URI                                | Name                               | Action                                                               | Middleware      |
+--------+-----------+------------------------------------+------------------------------------+----------------------------------------------------------------------+-----------------+
|        | GET|HEAD  | admin/categories                   | admin.categories.index             | App\Http\Controllers\Admin\CategoriesController@index                | web,admin       |
|        | POST      | admin/categories                   | admin.categories.store             | App\Http\Controllers\Admin\CategoriesController@store                | web,admin       |
|        | GET|HEAD  | admin/categories/create            | admin.categories.create            | App\Http\Controllers\Admin\CategoriesController@create               | web,admin       |
|        | GET|HEAD  | admin/categories/{category}        | admin.categories.show              | App\Http\Controllers\Admin\CategoriesController@show                 | web,admin       |
|        | PUT|PATCH | admin/categories/{category}        | admin.categories.update            | App\Http\Controllers\Admin\CategoriesController@update               | web,admin       |
|        | DELETE    | admin/categories/{category}        | admin.categories.destroy           | App\Http\Controllers\Admin\CategoriesController@destroy              | web,admin       |
|        | GET|HEAD  | admin/categories/{category}/edit   | admin.categories.edit              | App\Http\Controllers\Admin\CategoriesController@edit                 | web,admin       |

Note: Laravel 5.3 uses singular by default.

public function edit(Category $category)
{
    return view('admin.categories.edit', [
        'category'      => $category
    ]);
}

Now, Laravel will automatically resolve the category using the ID parameter, and will throw an exception if the model does not exist.

Note: To resolve the parameter it uses the findOrFail Eloquent method, unless the parameter has a default value.

Everything looks great for the moment, because we removed the check for the model from all methods. Nevertheless, we still need to catch the exception and take the proper action.

Handling Exceptions

The App\Exceptions\Handler@render method is responsible for converting exceptions to an HTTP response. We’ll be using it to handle the ModelNotFoundException and redirect to the 404 not found page.

The render method has a request and exception parameter that we can use to determine what to do.

public function render($request, Exception $e)
{
    if ($e instanceof ModelNotFoundException) {
        $view = view("admin.404");

        if ($e->getModel() == Category::class) {
            $view->withErrors(['Category not found'])->render();
        }

        return response($view, 404);
    } else {
        // handle other exceptions
        return parent::render($request, $e);
    }
}

We test to see if the thrown exception is an instance of ModelNotFoundException. We can also test the model name to display the proper error message. To avoid adding multiple if tests for all our models we can create an indexed array of messages and use the model class name to pull the proper message.

Resolving Parameters

Laravel resolves the routing parameters using the name and type hinting. If the parameter type is a model, it tries to find a record in the database using the ID and it fails if no records are found.

As a good practice, we tend to avoid exposing our internal IDs to the end user, and this problem is often solved by using universally unique identifiers (UUID). But since Laravel is using the table primary key to resolve the bound parameter, it will always throw an error!

To solve this problem, Laravel lets us override the getRouteKeyName method from the parent model class. The method should return the attribute name, in this case uuid.

class Category extends Model
{
    // ...

    public function getRouteKeyName()
    {
        return "uuid";
    }
}

Now, if we try editing a specific category using the UUID it should work as expected, e.g. http://local.dev/admin/categories/b86266d4-63c7-11e6-8c98-08002751e440/edit.

Got more Laravel tips? Share some with us!

Frequently Asked Questions on Laravel Model Route Binding

What is the difference between implicit and explicit route model binding in Laravel?

In Laravel, route model binding provides a convenient way to automatically inject the model instances directly into your routes. There are two types of route model binding: implicit and explicit.

Implicit binding is the automatic resolution of Eloquent models by their ID. For example, if you have a route defined as /users/{user}, Laravel will automatically fetch the User model with the ID provided in the URL.

Explicit binding, on the other hand, allows you to define more advanced resolution logic. For instance, you might want to resolve a user by their email instead of their ID. In this case, you would need to define an explicit binding in the RouteServiceProvider.

How can I use route model binding with Laravel controllers?

To use route model binding in your Laravel controllers, you simply need to type-hint the model instance in your route definition. For example, if you have a route defined as /users/{user}, you can type-hint the User model in your controller method like so:

public function show(User $user)
{
return view('user.show', compact('user'));
}

In this example, Laravel will automatically inject the User model instance that corresponds to the {user} ID in the URL.

Can I use route model binding with Laravel resource controllers?

Yes, you can use route model binding with Laravel resource controllers. In fact, it’s one of the most common use cases for this feature. When you generate a resource controller using the artisan command, Laravel will automatically set up route model binding for you.

For example, if you generate a UserController, Laravel will create methods for the show, edit, update, and destroy actions that type-hint the User model. This means that you can access the corresponding User model instance directly in these methods, without having to manually fetch it from the database.

How can I customize the key used in route model binding?

By default, Laravel uses the ID column for route model binding. However, you can customize this by overriding the getRouteKeyName method in your Eloquent model. For example, if you want to use the email column instead, you can do so like this:

public function getRouteKeyName()
{
return 'email';
}

Now, Laravel will resolve the model instances by their email instead of their ID.

Can I use route model binding with nested routes?

Yes, you can use route model binding with nested routes in Laravel. This is often useful when you have a parent-child relationship between your models. For example, if you have a route defined as /users/{user}/posts/{post}, you can type-hint both the User and Post models in your controller method:

public function show(User $user, Post $post)
{
return view('posts.show', compact('user', 'post'));
}

In this example, Laravel will automatically inject the User and Post model instances that correspond to the {user} and {post} IDs in the URL.