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

thebookcollector's avatar

I'd like to return all books in a user's collection with all the user's ratings

My app allows users to add books to their collection and then rate the books if they'd like. When a user views their book collection, I'm trying to add their ratings to the page so they can see what rating they've given each book in their collection and even then sort by ratings. I currently have:

'books' => Book::with('images')
    ->join('collections as c', 'c.book_id', '=', 'books.id')
    ->where('c.user_id', '=', $this->user->id)
    ->paginate(20)

and that returns all the books in the given user's collection just fine (along with the image relationship if an image exists for the book).

I have the appropriate relationships set up in the models (Book, User, and Rating models) and I've tried adding the ratings relationship like this:

'books' => Book::with('images', 'ratings')
    ->join('collections as c', 'c.book_id', '=', 'books.id')
    ->where('c.user_id', '=', $this->user->id)
    ->paginate(20)

But that obviously returns all users' ratings for each book, not just the specified user's ratings for the books.

I also tried joining the ratings table:

'books' => Book::with(['images'])
    ->join('collections as c', 'c.book_id', '=', 'books.id')
    ->where('c.user_id', '=', $this->user->id)
    ->leftJoin('ratings as r', 'r.book_id', '=', 'books.id')
    ->where('r.user_id', '=', $this->user->id)
    ->paginate(20)

But that (for some reason that I can't figure out) is only returning books that have ratings by the user instead of all books in the user's collection even if they don't have a rating. I'd like all books in the users collection returned even if they don't have ratings yet. - which I thought the leftJoin would take care of.

I could certainly split it up into two different queries like:

'ratings' => Rating::where('user_id', $this->user->id)

but I don't want to do that because in the blade page, I have a table that displays the user's collection and I'd like to be able to sort by categories (book title, author, release_date, etc - which are all part of the book model AND by rating which is another model). I don't know how I'd do that if it's not all part of the same query.

0 likes
3 replies
kevinbui's avatar
kevinbui
Best Answer
Level 41

I reckon you want to get all the books with the ratings by the specified user only. You might want to try constraining eager loading:

Book::with([
    'images', 
    'ratings' => function ($query) {
        $query->where('user_id', $this->user_id);
    }])
    ->join('collections as c', 'c.book_id', '=', 'books.id')
    ->where('c.user_id', '=', $this->user->id)
    ->paginate(20)

// Or this might also work.
Book::with([
    'images', 
    'ratings' => fn ($query) => $query->where('user_id', $this->user_id)
    ])
    ->join('collections as c', 'c.book_id', '=', 'books.id')
    ->where('c.user_id', '=', $this->user->id)
    ->paginate(20)

By the way, though joining tables can be more performant, it would be a bit cleaner to create a relationship and a query scope.

class Book extends Model
{
    public function collectedByUsers()
    {
        return $this->belongsToMany(User::class, 'collections');
    }

    public function scopeCollectedBy($query, User $user)
    {
        return $query->whereRelation('collectedByUsers', 'id', '=', $user->id );
    }
}

class Rating extends Model
{
    public function scopeRatedBy($query, User $user)
    {
        return $query->where('user_id', $user->id);
    }
}

So you can simply do this:

Book::with([
    'images', 
    'ratings' => function ($query) {
        $query->ratedBy($this->user);
    }])
    ->collectedBy($this->user)
    ->paginate(20)
1 like
thebookcollector's avatar

@kevinbui - I could have sworn I'd tried this before but I must have done something wrong because your first suggestion worked perfect. Thank you! And thank you for the additional info about the relationship and query scope. I'll play with that to clean my queries up more.

1 like

Please or to participate in this conversation.