Eloquent provides one Relation type for far related tables – hasManyThrough
. However it works only with with cascade of hasOne/hasMany
relations and you can use it only for 2 levels of nesting, while sometimes you need more.
Imagine, that you want to get the posts for logged-in user, having this setup:
1 2 3 4 5 6 7 |
// User subscribed to many tags User belongsToMany Tag // Post migh be tagged by many tags Tag belongsToMany Post |
Now, because there is no built-in method for such case, you would probably think of getting all the tags
, looping through them, and merging all the posts
from each tag. While this would work, it would be cumbersome, so let’s make it easier!
First, let’s see what hasManyThrough
can do for us:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// User is the author of many posts User hasMany Post // each Post has many comments Post hasMany Comment // now getting all the comments through posts // is as easy as this simple relation User hasManyThrough Comment, Post public function comments() { return $this->hasManyThrough(Comment::class, Post::class); } // then $user->comments; // collection of all the comments |
Compared to the User, Tag, Post
case, it is so easy. For the latter, we could do this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$user->load('tags.posts'); // eager load far relation $posts = new Collection; // Illuminate\Database\Eloquent\Collection foreach ($user->tags as $tag) { $posts = $posts->merge($tag->posts); } $posts = $posts->unique(); // remove the duplicates $posts; // this is the collection we needed |
As you can see it is rather cumbersome and would be really cool, if it could be simplified.
So here’s the trick that you can use for any level of nesting and any type of relation:
1 2 3 4 5 6 7 |
$user->load(['tags.posts' => function ($q) use ( &$posts ) { $posts = $q->get()->unique(); }]); $posts; // the collection we needed |
Woow, it was sooo easy, but wtf is going on there? you could say
So let’s do it again step by step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$user->load([ // again: it can be any relation type here // and any level of nesting 'tags.posts' // we're passing $posts by reference here, and, // if it was never declared, it will be created now => function ($q) use (&$posts) { // now we will simply execute the query on the posts table // and store its result, a collection, in our $posts variable $posts = $q->get() // duplicates are likely to occur for a m-m relation, // so let's remove them with collection's unique() ->unique(); }]); |
There’s one more thing to say here – when we call get
in the closure, the query is executed straight away, unlike with eager loading. That being said, the code above calls one additional query: 1 for tags, 1 for posts (eager loading) + 1 for posts, that we called.
And that’s it. Now you can get any far related models with no effort.
—
Edit: As pointed out by Joseph Silber in the comment, you can handle such case easily with collection methods, like shown below. However, the idea behind the trick here, is that you can use it in this form for no-matter-how-far relation and it will work just the same, and that’s something you couldn’t achieve with collection methods easily.
1 2 3 4 5 6 7 |
$user = User::with('tags.posts')->find($someId); $postsArray = $user->tags->pluck('posts'); // in L < 5.3 it was lists() $posts = (new Collection($postsArray))->collapse()->unique(); |