When implementing something that requires querying the database to find a record by its ID, the first thing that comes to mind is the find()
function from Eloquent.
Even though the function works, and you will be able to retrieve your data when it exists, a null object will be returned if the record is not found, and this null object usually brings unnecessary complexity.
As you know, Laravel is a full-featured framework, but one of the features that blew my mind once I got the hang of it, was the exception handling of the framework. It’s not random that some of Laravel’s helpers throw exceptions instead of returning null objects, such as the findOrFail()
function.
These helpers that throw exceptions can be pretty handy when you learn how to automate the handling of their exceptions, making the code cleaner and shorter. But before we dive into those functions or Laravel’s exception handler, let’s take a look at ways that a find operation can be implemented.
Content
Ways to implement
In the following two sections, we will see the most conventional ways of implementing a find operation. You will notice that the number of lines between the implementations is the same; although they are entirely different implementations and there are no real advantages between them, the choice of the approach is just a matter of preference.
The find function
Down below, we have a real use case where we are implementing a show action to the UsersController to find a user by its ID and render the record as a response to an endpoint.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class UsersController extends Controller
{
public function show(Request $request): Response
{
$user = User::find($request->id);
if ($user === null) {
return response("User with id {$request->id} not found", Response::HTTP_NOT_FOUND);
}
return response($user);
}
}
First, we query a user by its ID using the find function and store its result in an $user
object.
Next, because the record of a given ID may not be found in the database, we are checking if the $user
is null; in case it is, we will return a message that would evaluate to User with id 1 not found
.
Finally, if the ID passed into the query got found, the user object would be present, and we could just return it as a response to the API call.
The findOrFail function
With the same use case as before, let’s try a second approach and see how our code looks when using the findOrFail()
function without automating its exception handling.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class UsersController extends Controller
{
public function show(Request $request): Response
{
try {
$user = User::findOrFail($request->id);
} catch (ModelNotFoundException $exception) {
return response("User with id {$request->id} not found", Response::HTTP_NOT_FOUND);
}
return response($user);
}
}
At the try{}
block we are querying the user by its ID; when found, we will place the data at the $user
, but unlike the previous approach, a ModelNotFoundException will be thrown in case the record doesn't exist.
Thereafter, the catch{}
block gets used to intercept the exception thrown; once the exception gets intercepted, we build a response with a message that would be evaluated to User with id 1 not found
.
Then, if the given ID gets found, the $user
object would be present, and we would return it as a response to the endpoint.
Automating exceptions
It’s important to notice that the approach about to be explained can be used to automate the handling of various Laravel exceptions. By automating these exceptions, you will have cleaner code and a more structured way of handling the exceptions.
To automatically handle the exception thrown by the findOrFail()
function, we will have to override the render()
function in the app/Exceptions/Handler.php class, as shown in the example down below.
<?php
namespace App\Exceptions;
use Throwable;
use Illuminate\Support\Arr;
use Illuminate\Http\Response;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $e)
{
if ($e instanceof ModelNotFoundException) {
$replacement = [
'id' => collect($e->getIds())->first(),
'model' => Arr::last(explode('\\', $e->getModel())),
];
$error = new Error(
help: trans('exception.model_not_found.help'),
error: trans('exception.model_not_found.error', $replacement)
);
return response($error->toArray(), Response::HTTP_NOT_FOUND);
}
return parent::render($request, $e);
}
}
We start by declaring a conditional that checks if the $e
being received in the render()
parameter is an instance of the ModelNotFoundException thrown by the findOrFail()
used in the previous examples.
Then, we create a $replacement
array, an associative with the keys id and model, whose values are getting extracted from the intercepted exception. This replacement array gets used as a replacement parameter in a trans()
function that gets called in the constructor of the error object.
Subsequently, we are creating an $error
object. In case you don’t know what this Error class is about, please take a moment to read the previous post in this series, where I discuss custom exceptions. The error class takes two mandatory parameters: $help
which shows a possible solution to the error, and $error
where the error that happened gets specified.
Note that we are using named parameters and the trans()
helper to specify the messages we want to pass into the $help
and $error
parameters from the error object.
Lastly, with the error object instantiated, we can build the response object to be rendered to the API call. To achieve that, we are using the response()
function passing $error->toArray()
as the first parameter, and Response::HTTP_BAD_REQUEST
as the second one, which is the constant of the HTTP status 400.
Final implementation
As a final result, we have a show action that can be implemented with one single line, without the need for checking for a null object nor try/catch blocks to handle the exception getting thrown.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class UsersController extends Controller
{
public function shows(Request $request): Response
{
return response(User::findOrFail($request->id));
}
}
Now our findOrFail exception automation is done.
Happy coding!
Top comments (3)
Thanks, Such a good tips.
I want to point out what about adding a custom exception that only handles render function for our exception response like:
and then extend our exceptions based on that
Hello @kazemmdev,
I'm glad you've found this article interesting and useful, the idea here is to explain how developers can take advantage of Laravel's exception handler to automate the handling of Laravel's built-in exceptions, such as the
ModelNotFoundException
thrown by thefindOrFail()
helper.In case you want to read my thoughts on the usage and structuring of custom Exceptions that can be automatically handled by Laravel, you can take a look at the second article of this series.
There, I talk about creating a base exception that implements the
render()
method alongside a few other things I judged to be necessary to create a well structure error modeling for automated exception handling.Fell free to add your thoughts in there, it would be a pleasure to have more insights on the article!
Hey, @jackmiras!
Thanks a bunch for getting back to me. Yeah, you're totally right, there are various ways to get things done. Personally, I always try to use as little code as possible.