Calling Eloquent from Blade: 6 Tips for Performance

One of the most common performance issues I've seen in Laravel is using Eloquent methods and relationships from Blade, creating unnecessary extra loops and queries. In this article, I will show different scenarios and how to handle them effectively.


Scenario 1. Loading belongsTo() Relationship: don't forget Eager Loading

Probably, the most typical case - you're looping with @foreach through the records, and in some column you need to show its parent record with some field.

@foreach ($sessions as $session)
<tr>
  <td>{{ $session->created_at }}</td>
  <td>{{ $session->user->name }}</td>
</tr>
@endforeach

And, of course, session belongs to user, in app/Session.php:

public function user()
{
    return $this->belongsTo(User::class);
}

Now, the code may look harmless and correct, but depending on the controller code, we may have a huge performance issue here.

Wrong way in Controller:

public function index()
{
    $sessions = Session::all();
    return view('sessions.index', compact('sessions');
}

Correct way:

public function index()
{
    $sessions = Session::with('user')->get();
    return view('sessions.index', compact('sessions');
}

Notice the difference? We're loading the relationship with the main Eloquent query, it's called Eager Loading.

If we don't do that, in our Blade foreach loop will call one SQL query for every session, asking for its user directly from the database every time. So if you have a table with 100 sessions, then you would have 101 query - 1 for session list, and another 100 for related users.

So, don't forget Eager Loading.


Scenario 2. Loading hasMany() Relationship

Another typical scenario is that you need to list all child entries in the loop of parent records.

@foreach ($posts as $post)
<tr>
  <td>{{ $post->title }}</td>
  <td>
    @foreach ($post->tags as $tag)
      <span class="tag">{{ $tag->name }}</span>
    @endforeach
  </td>
</tr>
@endforeach

Guess what - same thing applies here. If you don't use Eager Loading, for every post there will be a request to the database.

So, in your controller, you should do this:

public function index()
{
    $posts = Post::with('tags')->get(); // not just Post::all()!
    return view('posts.index', compact('posts'));
}

Scenario 3. NOT Using Brackets in hasMany() Relationship

Let's imagine you have a poll with votes, and want to show all polls with the amount of votes they had.

Of course, you're doing Eager Loading in Controller:

public function index()
{
    $polls = Poll::with('votes')->get();
    return view('polls', compact('polls'));
}

And then in Blade file you're showing it something like this:

@foreach ($polls as $poll)
    <b>{{ $poll->question }}</b>
    ({{ $poll->votes()->count() }})
    <br />
@endforeach

Seems ok, right? But notice ->votes(), with brackets. If you leave it like this, then there will STILL be one query for each poll. Because it doesn't get the loaded relationship data, instead it's calling its method from Eloquent again.

So please do this: {{ $poll->votes->count() }}. Without brackets.

And, by the way, same applied for belongsTo relationship. Don't use brackets while loading relationships in Blade.

Offtopic: while browsing StackOverflow, I've seen actually even worse examples of this. Like: {{ $poll->votes()->get()->count() }} or @foreach ($poll->votes()->get() as $vote) .... Try that with Laravel Debugbar and see the amount of SQL queries.


Scenario 4. What if Relationship May Be Empty?

One of the most common errors in Laravel is "trying to get property of non-object", have you seen it before in your projects? (come on, don't lie)

Usually it comes from something like this:

<td>{{ $payment->user->name }}</td>

There's no guarantee that user for that payment still exist. Maybe it was soft-deleted? Maybe there's foreign key missing in the database, which allowed someone to delete user permanently?

Now, solution depends on Laravel/PHP version. Before Laravel 5.7, typical syntax of showing default value was this:

{{ $payment->user->name or 'Anonymous' }}

Since Laravel 5.7, it changed the syntax to follow common PHP operator, which was introduced in PHP 7:

{{ $payment->user->name ?? 'Anonymous' }}

But did you know you can also assign default value on Eloquent level?

public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

This withDefault() method will return empty model of User class, if the relationship doesn't exist.

Not only that, you can also fill that default model with values!

public function user()
{
    return $this->belongsTo(User::class)
      ->withDefault(['name' => 'Anonymous']);
}

Scenario 5. Avoiding Where Statements in Blade with Extra Relationships

Have you seen code like this in Blade?

@foreach ($posts as $post)
    @foreach ($post->comments->where('approved', 1) as $comment)
        {{ $comment->comment_text }}
    @endforeach
@endforeach

So, you're filtering comments (eager loaded, of course, right? right?) with another where('approved', 1) condition.

It does work and it doesn't cause any performance issues, but my personal preference (and also MVC principle) says that logic should be outside of the View, somewhere in, well, "logic" layer. Which may be Eloquent model itself, where you can specify a separate relationship for approved comments in app/Post.php.

public function comments()
{
    return $this->hasMany(Comment::class);
}

public function approved_comments()
{
    return $this->hasMany(Comment::class)->where('approved', 1);
}

And then you load that specific relationship in Controller/Blade:
$posts = Post::with('approved_comments')->get();


Scenario 6. Avoiding Very Complex Conditions with Accessors

Recently in one project I had a task: listing jobs, with envelope icon for messages and with price for the job which should be taken from the LAST message that contained that price. Sounds complicated, and it is. But hey, real life is also quite complex!

In the code I first wrote something like this:

@foreach ($jobs as $job)
    ...
    @if ($job->messages->where('price is not null')->count())
        {{ $job->messages->where('price is not null')->sortByDesc('id')->first()->price }}
    @endif
@endforeach

Oh, the horror. Of course, you need to check if the price exists, then take the last message with that price, but... Screw it, it shouldn't be in the Blade.

So I ended up using Accessor method on Eloquent and defined this in app/Job.php:

public function getPriceAttribute()
{
    $price = $this->messages
        ->where('price is not null')
        ->sortByDesc('id')
        ->first();
    if (!$price) return 0;

    return $price->price;
}

Of course, with such complex situations it's also easy to jump into N+1 query problem or just launch queries multiple times by accident. So please use Laravel Debugbar to find the flaws.

Also, I can recommend a package called Laravel N+1 Query Detector.


Bonus. I want to leave you with probably the worst example of the code I've seen on Laracasts, while researching this topic. Someone wanted advice for this code below. Unfortunately, code like this is seen in live projects too often. Because, well, it works... (don't try this at home)

@foreach($user->payments()->get() as $payment)
<tr>
    <td>{{$payment->type}}</td>
    <td>{{$payment->amount}}$</td>
    <td>{{$payment->created_at}}</td>
    <td>
        @if($payment->method()->first()->type == 'PayPal')
            <div><strong>Paypal: </strong>
            {{ $payment->method()->first()->paypal_email }}</div>
        @else
            <div><strong>Card: </strong>
            {{ $payment->payment_method()->first()->card_brand }} **** **** ****
            {{ $payment->payment_method()->first()->card_last_four }}</div>
        @endif
    </td>
</tr>
@foreach

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 22 courses (477 lessons, total 38 h 20 min)
  • 2 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord