Patrique Ouimet
Senior Product Engineer
Thu, Feb 15, 2024 8:16 AM
In this article, we will discuss some common performance mistakes that developers make when working with Laravel. In order to keep the example small, we'll assume there's a User
model and a Post
model, and that a user has many posts.
$user->posts->first();
This code will retrieve all the posts for the user and then return the first one. This is inefficient because it retrieves all the posts from the database, and it will hydrate all the returned records into post models.
The underlying SQL query will look something like this:
SELECT * FROM posts WHERE user_id = 1;
Instead, we should use the first
method on the relationship itself to only retrieve a single record/model:
$user->posts()->first();
This will only retrieve the first post from the database, which is much more efficient.
The underlying SQL query will look something like this:
SELECT * FROM posts WHERE user_id = 1 LIMIT 1;
$user->posts()->first()->title;
This code will hydrate the post model and then retrieve the title property.
The underlying SQL query will look something like this:
SELECT * FROM posts WHERE user_id = 1 LIMIT 1;
Since we only need the title, we can use the value
method instead:
$user->posts()->value('title');
The underlying SQL query will look something like this:
SELECT title FROM posts WHERE user_id = 1 LIMIT 1;
This results in less data retrieved from the database and conserves memory as it does not hydrate the model.
In this example the method is defined on the User
model:
public function hasPosts(): bool
{
return $this->posts()->count() > 0;
}
The underlying SQL query will look something like this:
SELECT COUNT(*) FROM posts WHERE user_id = 1;
This is inefficient as it retrieve all the records which meet the condition and then count them.
Instead, we should use the exists
method:
public function hasPosts(): bool
{
return $this->posts()->exists();
}
The underlying SQL query will look something like this:
SELECT EXISTS (SELECT * FROM posts WHERE user_id = 1);
This will stop execution as soon as a single record meets the condition.
$user->posts->pluck('title');
Underlying SQL query will look something like this:
SELECT * FROM posts WHERE user_id = 1;
This is inefficient as we're hydrating all the post models to retrieve a list of titles.
Instead, we should use the pluck
method on the relationship itself:
$user->posts()->pluck('title');
Underlying SQL query will look something like this:
SELECT title FROM posts WHERE user_id = 1;
This will only retrieve the titles from the database, which is much more efficient.
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory()->create(),
'title' => fake()->words(3, true),
];
}
}
// within a test
$user = User::factory()->create();
Post::factory()->create(['user_id' => $user->id]);
Though this looks harmless, there's a flaw that can result in very slow tests by generating too many database records (and models). The part where we define User::factory()->create()
within the PostFactory
will be called immediately regardless of the fact that we passed an override when setting up the test. There are a few ways to fix this.
Call factory
without calling create
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->words(3, true),
];
}
}
You'll find this approach in the official documentation. This will only resolve the factory if not override is provided for user_id
.
Alternatively, you can wrap the User::factory()->create()
call in a closure:
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => fn() => User::factory()->create(),
'title' => fake()->words(3, true),
];
}
}
This approach will only resolve the closure if no override for user_id
is provided.
You'll notice a theme in the examples provided above. The majority relate to the idea of reducing the amount of data retrieved from the database and the amount of models hydrated. And as seen in the factory example, it's important to understand how the code execution works in order to avoid performance issues.