Tech Notes: Laravel hasOneThrough / belongsToThrough confusion

It’s kinda hard to believe that in all my years of writing Laravel I’ve not come across this before.

But today I needed “belongsToThrough” in an Eloquent Model and I got myself so confused! Turns out it doesn’t exist, and the docs are (IMO) confusing and others seem to have even got it backwards.

I’m assuming some knowledge here to keep things brief. You would know what an ORM is and how relationships work.

Also – note for pedants – I’m using American “license” throughout examples for naming consistency, even for the noun which would otherwise be “licence”. I know. And I’m sorry.

My use case

I’ve been working on my licensing system for software products I’m developing. It’s pretty simple. And it has a data model with Customers, Orders, and Licenses.

It looks like this:

License        Order             Customer
--------       -----------       --------
order_id  -->  customer_id  -->  email

In ORM terminology:

  • A License belongsTo an Order
  • An Order hasMany Licenses
  • An Order belongsTo a Customer
  • A Customer hasMany Orders

This is all good. But, for reasons I won’t go into, I wanted to define a relationship between License and Customer through the Orders table. So I could do:

$license->customer

I know that I could simply do:

$license->order->customer

but please, just let me do my thing and assume I have reasons! Thanks.

Laravel is a superb framework and often just does what you need. It’s conventions make sense and you can often guess the name of the thing you need.

I was vaguely aware of “hasOneThrough” (those of you who are ahead of me are already rolling your eyes). And so I attempted to implement it:

public function customer()
{
  return $this->hasOneThrough(Customer::class, Order::class);
}

BUT…this gave me an error saying that the Order table didn’t have a license_id.

My immediate reaction: WHAT? THAT’S BACKWARDS.

The Laravel Docs Example

Time to consult the Laravel docs where the example given is:

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

Now, first of all, this is (in my opinion – sorry documenters) a poor example data model. It says that an owner can only have one car, but a car can have multiple owners. Which is…err…backwards? And a car has one mechanic. And I don’t understand what real-world use-case this data model is modelling. But let’s go with it.

In this model:

  • A Mechanic hasMany Cars
  • A Car belongsTo a Mechanic
  • A Car hasMany Owners
  • An Owner belongsTo a Car (?!)

And in the example we want to get the Owner for a given Mechanic (?! – let’s go with it!). So you can do:

public function carOwner()
{
  return $this->hasOneThrough(Owner::class, Car::class);
}

I don’t know about you but I’m so confused. (And no, I’m not going to PR the docs. I’ve tried this before. It’s no fun.)

In this model a Mechanic hasMany Cars and a Car hasMany Owners. So a Mechanic has many, many Owners! And we’re asking for one of them. And what’s even more confusing is that the docs don’t specify which one we will get? The most-recently created? The most-recently updated? A random owner? It’s just weird.

The Laravel docs are explicit about this for hasOne saying:

Sometimes a model may have many related models, yet you want to easily retrieve the “latest” or “oldest” related model of the relationship.

and it defines latestOfMany(), oldestOfMany() and a generic ofMany() modifiers.

I can see that you might want to use this. For example, you might have Category <- Post <- Comment and you might want to get the most recent Comment for a Category? Maybe? This is the kind of thing that hasOneThrough seems to do.

Has and BelongsTo naming…

To get why this is all like it is, you have to properly understand the Has and BelongsTo conventions in ORMs.

Has/have is always about having lots of things. You can haveMany. And you can haveOne. But haveOne is actually really haveOneOfMany.

Belongs is sometimes about having one (belongsTo) and, confusingly, sometimes about having many (which is called belongsToMany). But let’s just ignore belongToMany for now. Belongs is about being related to one of another thing. It’s the child -> parent relationship.

But this is really confusing. Deal with it.

Enter stage left: belongsToThrough !

Finally, with the tenacious web-searching that only a persistent nerd like me has, I found a “belongsToThrough” package for Eloquent. And I realised that I had had it wrong all along.

The existence of this package with the relationship named correctly made me realise that I’d misunderstood hasOneThrough and the whole has/belongs convention. THIS was what I needed.

My only remaining questions:

1. Is this the best way to do this? Does this not exist for a reason in Laravel core?

and

2. Why DOESN’T this exist in Laravel’s core?

And it seems that this doesn’t exist in Laravel’s core because:

a) There aren’t good use cases for it (apparently, even though OrderItem -> Order -> Customer seems like a really common use case)

b) There are other ways of achieving it, such as with an accessor, or just by chaining the relationships (e.g. $license->order->customer)

It seems that the potential benefits of being able to eager load, or use PHP Reflection on this relationship just aren’t there.

Leave a Reply

Your email address will not be published. Required fields are marked *