13

I've got a model, it belongs to another model, that model belongs to a third model, and I want an eloquent method to relate the first model to the third one.

There doesn't appear to be a belongsToThrough (or hasOneThrough) method, though. I've already tried chaining multiple belongsTo methods, but that hasn't worked (Call to undefined method Illuminate\Database\Query\Builder::belongsTo()). Any ideas?

Here is an example of the models:

// The first model
// Schema: this model has a middle_id column in the database
class Origin extends Eloquent {
    public function middle()
    {
        return $this->belongsTo('Middle');
    }
}

// The second model
// Schema: this model has a target_id column in the database, but NOT an origin_id column
class Middle extends Eloquent {
    public function target()
    {
        return $this->belongsTo('Target');
    }
}

// The third model
class Target extends Eloquent {
}

What I'd like to do is add something like the following method to the Origin model:

// A relationship method on the first "origin" model
public function target()
{
    // First argument is the target model, second argument is the middle "through" model, third argument is the database column in middle model that it uses to find the target model, or soemthing
    return $this->hasOneThrough('Target', 'Middle', 'target_id');
}

So that I can use $originInstance->target->title, etc.

Joe
  • 15,669
  • 4
  • 48
  • 83
BenjaminRH
  • 11,974
  • 7
  • 49
  • 76
  • 2
    have you tried `hasManyThrough`? – Jarek Tkaczyk Apr 29 '14 at 13:31
  • I have not, I just assumed that was for many relationships. Lemme try that, thanks! – BenjaminRH Apr 29 '14 at 13:41
  • 1
    HasOne and HasMany both extend abstract HasOneOrMany and they are pretty similar. I'm pretty sure it will work for you, however it may return a Collection instead of a single model, that could be the only downsie I can think of now. – Jarek Tkaczyk Apr 29 '14 at 14:02
  • Ah, no, unfortunately that won't work. This is because my origin table belongs to the middle table which belongs to the target table, but the middle table does not belong to the origin table, which is what `hasManyThrough` requires. Let me clarify a bit in the question. – BenjaminRH Apr 29 '14 at 14:26
  • 1
    OK, then that's not going to work. There is no method for through relations this way currently, only the other way around `A -> hasOneOrMany -> B -> hasOneOrMany C`. but you can still use dot nested relations like origin->middle->target->title (if it's hasOne everywhere) – Jarek Tkaczyk Apr 29 '14 at 14:35
  • Yeah, that works. I've still got this problem though because I'm using a library that requires one relationship it can use. Anyway, thanks for the effort! – BenjaminRH Apr 29 '14 at 14:41
  • I don't get your point. Anyway you can define this relation as a single method by chaining. or you can setup some helper to do the job. Please elaborate what is the expected behaviour and there should be a solution. – Jarek Tkaczyk Apr 29 '14 at 14:43
  • You got it working with IRC folks help or not? I created a new relation class exactly for this `belongsToThrough`, so you can have a look if you like. Didn't send a PR yet because it still needs some testing, but no time for that atm. – Jarek Tkaczyk May 01 '14 at 19:52
  • Looking at the [Documentation](https://laravel.com/docs/5.2/eloquent-relationships#many-to-many) I guess you're looking for a way of implementing the `withPivot`? Alternatively you can manually create a `join` when hydrate the desired Model with the results using `fill` or `hydrate` – Ash Apr 28 '16 at 15:18

4 Answers4

17
public function target() { 
    $middle = $this->belongsTo('Middle', 'middle_id'); 
    return $middle->getResults()->belongsTo('Target'); 
}

Update:

Starting from laravel 5.8 you can use the hasOneThrough relationship:

public function target() { 
    return $this->hasOneThrough('Target', 'Middle');
}
Razor
  • 9,577
  • 3
  • 36
  • 51
  • 1
    Thanks, I appreciate the answer! I probably haven't phrased my question clearly enough, but my problem is that I need to access the target directly as a relation to the origin. As it is setup currently, I could use `$origin->middle->target` to access the target, because each one belongs to the next. However, that won't work for me, as I need to skip the middle-man. I hope that clears my problem up a bit. – BenjaminRH Apr 30 '14 at 23:34
  • I would like to know more about "getResults()", i am reading this answear 2 years later than it was posted in, and by a simple google search it looks like getResults() got replaced by get(). Still need to confirm this, but i hope it could help others looking at this in 2017! – Yuri Scarbaci Mar 29 '17 at 08:23
  • AFAIK this answer is still valid, I double checked each `Relation` class and `getResults()` wasn't removed (e.g. [HasMany](https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php) Relation L5.4), did you test the answer? – Razor Mar 29 '17 at 18:46
  • Unfortunately this doesn't support eager loading :( – Charles Wood Sep 28 '20 at 22:50
  • 1
    @CharlesWood Laravel added a function `hasOneThrough` that supports eager loading, I've updated my old answer, thanks. – Razor Sep 29 '20 at 18:16
  • @Razor Nice! Bear in mind that to simulate `belongsToThrough`, you may need to customize the keys. See https://stackoverflow.com/a/58295018/1110820. (This is the solution I ended up using.) – Charles Wood Oct 01 '20 at 15:12
  • @Razor Say instead it was a Many-To-Many relation, how would that change? `return $this->middle()->getResults()->belongsToMany(Target::class);` works, but I can't seem to find the equivalent method call to hasManyThrough that will work. – csoler Aug 14 '22 at 01:48
16

If this is situation like message in a bottle, and bottle is owned by the user (user > bottle > message)

The only way I know to get the relation object is:

// THIS IS IN App\Message

public function bottle()
{
    return $this->belongsTo('App\Bottle');
}

public function user()
{
    return $this->bottle->belongsTo('App\User');
}
Mladen Janjetovic
  • 13,844
  • 8
  • 72
  • 82
  • 1
    It works. But is it efficient? Will this create a separate query for each bottle of each message, instead of running just one query for the entire request of a user. – Jasmeet Singh Feb 07 '18 at 08:18
  • @JasmeetSingh, Of course it will create separate queries, but it makes life easier while coding. Having in mind that object caching is standard thing in apps these days, I don't mind. If you don't use such things you will just have to dance between ease of coding and app speed. One query for the entire request of a user with joins will be much faster, but it is not much reusable further in app. And we want to separate database layer from app so no raw SQL queries in code, neither – Mladen Janjetovic Feb 11 '18 at 17:41
  • @JasmeetSingh or you can just use [Eager Loading](https://laravel.com/docs/5.6/eloquent-relationships#eager-loading) – Mladen Janjetovic Feb 11 '18 at 17:47
3

You can use hasOneThrough but you need to customize keys.

public function parent()
{
    return $this->hasOneThrough(Parent::class, Middle::class, 'id', 'id', 'middle_id', 'parent_id');
}

Origin belongs to Middle, and Middle belongs to Parent. Middle need has parent_id foreign key, and Origin has middle_id foreign key.

Finally you can use:

Origin::find(1)->parent;
-1
// First Model
public function secondModelRelation()
{
    return $this->belongsTo('App\Models\SecondModel');
}

public function thirdModelRelation()
{
    // Call the thirdModelRelation method found in the Second Model
    return $this->secondModelRelation->thirdModelRelation;
}


// Second Model
public function thirdModelRelation()
{
    return $this->belongsTo('App\Models\ThirdModel');
}


// Third Model
Jon
  • 2,277
  • 2
  • 23
  • 33