🤯 50% Off! 700+ courses, assessments, and books

Laravel Quick Tip: Model Route Binding

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!