Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

hillcow's avatar

Laravel: How to always eager load with condition?

Let's say I am developing stackoverflow.

When a user visits the site, threads must be loaded. But it's not just the threads, I also need the information, whether a user has already voted on a thread or not and how he voted (up/down).

Models: Thread, Vote

Now how can I eager load the votes on the threads? I know I can eager load ALL votes using $with on the Thread model, but I don't need to load thousands of votes with each thread. I only need to know if this one user, who is logged in, has voted and how.

I would like to know the best approach to solve these kind of problems. Thank you!

edit: is this maybe the solution?

    Thread::with([
    
    'vote' => function($q) use ($userId = Auth::user()->id) {
    
    $q->where('user_id',$userId)
    
    }
    
    ])->get();

and if it is - how can I tell the Thread model itself to always apply this condition? Because I might have many different places where I need to eager load the votes from the user and it would be always the same condition.

0 likes
6 replies
teos_97's avatar

You can create a macro combing "With" and "WhereHas" - https://stackoverflow.com/questions/29591931/merge-with-and-wherehas-in-laravel-5

In the AppServiceProvider within the boot method do :

\Illuminate\Database\Eloquent\Builder\Eloquent::macro('withAndWhereHas', function($relation, $constraint){
    return $this->whereHas($relation, $constraint)->with([$relation => $constraint]);
});

Next in your Thread model you can create a custom scope :

public function scopeWithUserVotes($query, $user_id)  {
	return $query->withAndWhereHas('vote',  fn($q)  => $q->where('user_id', $user_id));
}

Usage

Thread::withUserVotes(auth()->id());

Note - haven't tested it (check the stackoverflow thread)

hillcow's avatar

Thanks, I went ahead and subscribed again to Laracasts to be able to understand the solution provided here: https://laracasts.com/series/build-a-voting-app/episodes/19?autoplay=true

It goes like this: https://laravel.com/docs/8.x/eloquent#subquery-selects

Thread::addSelect(['user_has_voted' => Vote::select('id')
    ->where('user_id', auth()->id())
    ->whereColumn('thread_id', 'thread.id')
    ->limit(1)
])->get();

This works. The user_has_voted column is null whenever the user has not voted. However, I would like to do this on the model side of things, because I have many different places where I get the threads and I always need the column. How would I go about that?

teos_97's avatar

Same thing add a scope to your Thread model https://laravel.com/docs/8.x/eloquent#local-scopes

public function scopeUserVotes($query, $user_id) {
	return $query->addSelect(['user_has_voted' => Vote::select('id')
    ->where('user_id', $user_id)
    ->whereColumn('thread_id', 'thread.id')
    ->limit(1);
}

Then you can do Thread::userVotes($user_id)->get() ...

hillcow's avatar

Thank you so much! Now the problem that remains is the following: In my case I need this attribute on a sub-sub-model.

A thread can have a poll which has options. Models: Thread, Poll, Option

Do I need to include these nested ->with() calls every time now?

Thread::with(['poll' => function($q) {
            $q->with(['pollOptions' => function ($q) {
                $q->userHasVoted(auth()->id());
            }]);
        }])->........

and for my understanding: why do I need to give the auth()->id() to the scope? I tried including auth()->id() directly inside of the scope, but that didn't work, the results did not consider the according ->where condition.

Thanks!

teos_97's avatar

The userHasVoted is on the Thread level you are adding the where constraint on the pollOptions. You don’t have to eager load them everytime if you don’t need them. Generally you can query a sub relationship with just whereHas(‘relationship_name’,fn($q) => $q->where(this clause applies to the model returned by the relationship) e.g if you have whereHas(pollOptions….) the clauses you specify within the callback are applied on the PollOption model. In your case if the thread has both relationships you can chain them e.g

Thread::userHasVoted(auth->id())->whereHas(‘pollOptions’,fn($polloption) =>// you can go depper and do whereHas on a relationship of the pollOptionhere as well

$polloption->where(‘xxx’, smth))->get()

Sorry if its not very readable I’m typing on my phone atm.

Note : if you sre using auth()->id() in tinker you need to Login with a user first Auth::loginUsingId(user_id);

hillcow's avatar

Ah sorry, my post was misleading. Actually my userHasVoted is on the pollOptions level and not the thread level. The thread voting was just an example in the beginning, the pollOption thing is what I am actually working on.

Please or to participate in this conversation.