ideas icon indicating copy to clipboard operation
ideas copied to clipboard

Inverse of Eloquent hasManyThrough relationship

Open mbkv opened this issue 6 years ago • 7 comments

I realized that there was no inverse of the hasManyThrough relationship, even though there's an inverse for everything else.

Consider the following minimalist slack-like messaging system

users:
+----------+--------+
|  column  |  type  |
+----------+--------+
| id       | int    |
| username | string |
+----------+--------+

display_names:
+------------+------+
|   column   | type |
+------------+------+
| id         | int  |
| user_id    | int  |
| channel_id | int  |
+------------+------+

messages:
+-----------------+------+
|     column      | type |
+-----------------+------+
| id              | int  |
| display_name_id | int  |
| message         | text |
+-----------------+------+

Now ignoring the fact this is definitely over-engineered. Doing

public function messages()
{
    return $this->hasManyThrough(Message::class, DisplayName::class);
}

would generate a query similar to

SELECT
    *
FROM
    `messages`
INNER JOIN
    `display_names` ON `display_names`.`id` = `messages`.`display_name_id`
WHERE
    `display_names`.`user_id` = ?

But there's no way to go the reverse. IE: having a message model and getting the user. Like such

SELECT
    *
FROM
    `users`
INNER JOIN
    `display_names` ON `display_names`.`user_id` = `users`.`id`
WHERE
    `display_names`.`id` = ?

Admitedly this is a contrived example, and I'm not too sure if there's any other use case than a poorly engineered database like this

mbkv avatar May 17 '18 04:05 mbkv

Just a point of pedantry... The inverse of a HasManyThrough is ALSO a HasManyThrough. What you're asking for should be doable with standard queries. Something like:

User::with('displayNames')->where('display_name.id', $id)

(disclaimer: This is untested code. Use at your own risk)

fletch3555 avatar May 17 '18 06:05 fletch3555

@mbitokhov: You have to swap the models in your relationship:

public function messages()
{
    return $this->hasManyThrough(Message::class, DisplayName::class);
}

The reverse is just the concatenation of two BelongsTo relationships:

$message->displayName->user

staudenmeir avatar May 17 '18 11:05 staudenmeir

@fletch3555 I thought so too and it didn't really work out. Doing the inverse of a hasManyThrough with a hasManyThrough would require a display_name_id inside the users table. The inverse of a hasMany to hasMany is a belongsTo to belongsTo. You're thinking of belongsToMany being the inverse of a belongsToMany.

@staudenmeir Thanks for the warning!

mbkv avatar May 17 '18 13:05 mbkv

So you need two relations, belongsToThrough, and belongsToManyThrough x)

monaam avatar May 17 '18 13:05 monaam

Found this : https://github.com/znck/belongs-to-through

bleuscyther avatar Jun 03 '18 07:06 bleuscyther

Found this : https://github.com/znck/belongs-to-through

very good

quancoder avatar Apr 15 '20 16:04 quancoder

Definitely after 2 years there is no more need for you to solve your problem, but for those that still arrive on this issue, there is also a solution available with Laravel methods. Starting with version 5.8, Laravel has hasOneThrough method (documentation).

In the case of the problem described here, the solution is not very pretty, but it would be something like this:

// Message::class
public function user()
{
    return $this->hasOneThrough(User::class, DisplayName::class, 'id', 'id', 'display_name_id', 'user_id');
}

adnedelcu avatar May 31 '21 11:05 adnedelcu