Inverse BelongsToThrough relationships in Laravel models

No, Laravel doesn’t actually have a BelongsToThrough relationship, a common frustration for models where you want to access the top level of a BelongsTo chain.

Consider this setup:

1class User extends Model
2{
3 public function posts(): HasMany
4 {
5 return $this->hasMany(Post::class);
6 }
7}
8
9class Post extends Model
10{
11 public function reviews(): HasMany
12 {
13 return $this->hasMany(Review::class);
14 }
15}
16
17class Review extends Model
18{
19 public function postAuthor()
20 {
21 // Now what?
22 }
23}

A user has authored many posts. Those posts have many reviews. So how do we define the inverse relationship back up from Review -> User (the post author) without having to load Post in the middle?

The Solution

Enter the hasOneThrough relationship, albeit slightly repurposed.

1class Review extends Model
2{
3 public function postAuthor(): HasOneThrough
4 {
5 return $this->hasOneThrough(User::class, Post::class, 'id', 'id', 'post_id', 'user_id');
6 }
7}

The Explanation

The hasOneThrough relationship isn’t designed for inverse relationships as you’ll see when comparing our solution with the documentation:

// Reproduced from https://laravel.com/docs/10.x/eloquent-relationships#has-one-through
class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Foreign key on the cars table...
'car_id', // Foreign key on the owners table...
'id', // Local key on the mechanics table...
'id' // Local key on the cars table...
);
}
}

Because we want to invert the relationship, we have to invert the foreign keys for local keys and the local keys for foreign keys. So ours works out at:

class Review extends Model
{
public function postAuthor(): HasOneThrough
{
return $this->hasOneThrough(
User::class, // target class
Post::class, // through class
'id', // local key on the posts table
'id', // local key on the users table
'post_id', // foreign key on the reviews table
'user_id' // foreign key on the posts table
);
}
}

So now, if you have Review model, you can skip the Post model entirely with $review->postAuthor.