Laravel provides a huge amount of cool features that help improve our development experience (DX). But with the regular releases, stresses of day-to-day work, and the vast amount of features available, it's easy to miss some of the lesser-known features that can help improve our code.
In this article, I'm going to cover some of my favourite tips for working with Laravel models. Hopefully, these tips will help you write cleaner, more efficient code and help you avoid common pitfalls.
Spotting and Preventing N+1 Issues
The first tip we'll look at is how to spot and prevent N+1 queries.
N+1 queries are a common issue that can occur when lazy loading relationships, where N is the number of queries that are run to fetch the related models.
But what does this mean? Let's take a look at an example. Imagine we want to fetch every post from the database, loop through them, and access the user that created the post. Our code might look something like this:
$posts = Post::all();
foreach ($posts as $post) {
// Do something with the post...
// Try and access the post's user
echo $post->user->name;
}
Although the code above looks fine, it's actually going to cause an N+1 issue. Say there are 100 posts in the database. On the first line, we'll run a single query to fetch all the posts. Then inside the foreach
loop when we're accessing $post->user
, this will trigger a new query to fetch the user for that post; resulting in an additional 100 queries. This means we'd run 101 queries in total. As you can imagine, this isn't great! It can slow down your application and put unnecessary strain on your database.
As your code becomes more complex and features grow, it can be hard to spot these issues unless you're actively looking out for them.
Thankfully, Laravel provides a handy Model::preventLazyLoading()
method that you can use to help spot and prevent these N+1 issues. This method will instruct Laravel to throw an exception whenever a relationship is lazy-loaded, so you can be sure that you're always eager loading your relationships.
To use this method, you can add the Model::preventLazyLoading()
method call to your App\Providers\AppServiceProvider
class:
namespace App\\Providers;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Support\\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
}
}
Now, if we were to run our code from above to fetch every post and access the user that created the post, we'd see an Illuminate\Database\LazyLoadingViolationException
exception thrown with the following message:
Attempted to lazy load [user] on model [App\\Models\\Post] but lazy loading is disabled.
To fix this issue, we can update our code to eager load the user relationship when fetching the posts. We can do this by using the with
method:
$posts = Post::with('user')->get();
foreach ($posts as $post) {
// Do something with the post...
// Try and access the post's user
echo $post->user->name;
}
The code above will now successfully run and will only trigger two queries: one to fetch all the posts and one to fetch all the users for those posts.
Prevent Accessing Missing Attributes
How often have you tried to access a field that you thought existed on a model but didn't? You might have made a typo, or maybe you thought there was a full_name
field when it was actually called name
.
Imagine we have an App\Models\User
model with the following fields:
id
name
email
password
created_at
updated_at
What would happen if we ran the following code?:
$user = User::query()->first();
$name = $user->full_name;
Assuming we don't have a full_name
accessor on the model, the $name
variable would be null. But we wouldn't know whether this is because the full_name
field actually is null, because we haven't fetched the field from the database, or because the field doesn't exist on the model. As you can imagine, this can cause unexpected behaviour that can sometimes be difficult to spot.
Laravel provides a Model::preventAccessingMissingAttributes()
method that you can use to help prevent this issue. This method will instruct Laravel to throw an exception whenever you try to access a field that doesn't exist on the current instance of the model.
To enable this, you can add the Model::preventAccessingMissingAttributes()
method call to your App\Providers\AppServiceProvider
class:
namespace App\\Providers;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Support\\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventAccessingMissingAttributes();
}
}
Now if we were to run our example code and attempt to access the full_name
field on the App\Models\User
model, we'd see an Illuminate\Database\Eloquent\MissingAttributeException
exception thrown with the following message:
The attribute [full_name] either does not exist or was not retrieved for model [App\\Models\\User].
An additional benefit of using preventAccessingMissingAttributes
is that it can highlight when we're trying to read a field that exists on the model but that we might not have loaded. For example, let's imagine we have the following code:
$user = User::query()
->select(['id', 'name'])
->first();
$user->email;
If we have prevented missing attributes from being accessed, the following exception would be thrown:
The attribute [email] either does not exist or was not retrieved for model [App\\Models\\User].
This can be incredibly useful when updating existing queries. For example, in the past, you may have only needed a few fields from a model. But maybe you're now updating the feature in your application and need access to another field. Without having this method enabled, you might not realise that you're trying to access a field that hasn't been loaded.
It's worth noting that the preventAccessingMissingAttributes
method has been removed from the Laravel documentation (commit), but it still works. I'm not sure of the reason for its removal, but it's something to be aware of. It might be an indication that it will be removed in the future.