Custom Missing Model handler in Laravel Implicit Binding or how to catch ModelNotFoundException in Laravel 9

While developing backend part in my current project I facing a problem with customizing “missing model” logic in Laravel Implicit Binding. Of course, we’ve Missing Model Behavior from documentaion, but I’ve lots of routes with groups – so it’ll not very suitable method.

I spent hours digging the solution and finally want to share it with you.

Option 1. Simplifying method from documentation

As I sad above – it is not very suitable to pass closure on every “missing” method, but it is still a solution, especially when you assign anonymous function to variable and make your route file more pretty like this

$notFound = function (Request $request) {
    return response('',404, ['content-type' => 'application/json']);
};

Route::middleware('auth:sanctum')->group(
    function () use ($notFound){
        Route::get('/user', [UserController::class, 'info']);
        Route::middleware('can:admin')->group(function () use ($notFound) {
            Route::get('/users', [UserController::class, 'index'])->name('users.index');
            Route::get('/user/{user}', [UserController::class, 'view'])->name('user.view')->missing($notFound);
            Route::patch('/user/{user}', [UserController::class, 'update'])->name('user.update')->missing($notFound);
        });
    }
);

Sounds like solution, but it is not very suitable when you’ve lots of routes, so second option is …

Option 2. Catch ModelNotFoundException in Laravel 9.

If you look into documentaion – you’ll see, that you can easily catch any exception from global handler, but you’ll facing a problem

/* this works */
$this->renderable(function (NotFoundHttpException $e, $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Page not found.'
            ], 404);
        }
    });

/* this don't work */
$this->renderable(function (ModelNotFoundException $e, $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });

that ModelNotFoundException is not handled (deer)

First of all – we can do a workaround in Laravel 7 style using render method in global handler like this

/* this works */
public function render($request, Throwable $e)
{
   if ($e instanceof ModelNotFoundException) {
        return response('',404,['content-type' => 'application/json']);
   }
}

and this’ll work. But in Laravel 9 method render is not blank and contains logic, so I dig again and find this code

/* this works in Laravel 9 */
    public function register()
    {

        $this->renderable(function (NotFoundHttpException $e, $request) {
                if ($e->getPrevious() instanceof ModelNotFoundException) {
                    return response('',404,['content-type' => 'application/json']);
                }
        });
    }

on stackoverflow.

As you can see – there is a confusing condition – we match against NotFoundHttpException and than do a $e->getPrevious(), here is the answer  – ModelNotFoundException automatically call NotFoundHttpException.

Option 3. Call ModelNotFoundException on one Model

This is not my case – but I think to add it to complete the answer.

If you need customize ModelNotFoundException only on one model, you need to add an explicit binding  for model in RouteServiceProvider and pass a callback as a third parameter that’s called when the model is not found

public function boot()
{
    parent::boot();

    Route::model('order', \App\Order::class, function ($order) {
        throw new OrderNotFoundException($order);
    });
}

That’s all. Have a nice ModelNotFoundException Laravel handling

Leave a Reply

Your email address will not be published.