How to Merge Two Eloquent Collections?
Asked Answered
C

9

141

I have a questions table and a tags table. I want to fetch all questions from tags of a given question. So, for example, I may have the tags "Travel," "Trains" and "Culture" attached to a given question. I want to be able to fetch all questions for those three tags. The tricky, so it seems, is that questions and tags relationship is a many-to-many defined in Eloquent as belongsToMany.

I thought about trying to merge the questions Collections as below:

foreach ($question->tags as $tag) {
    if (!isset($related)) {
        $related = $tag->questions;
    } else {
        $related->merge($tag->questions);
    }
}

It doesn't seem to work though. Doesn't seem to merge anything. Am I attempting this correctly? Also, is there perhaps a better way to fetch a row of rows in a many-to-many relationship in Eloquent?

Chickadee answered 29/5, 2015 at 6:25 Comment(2)
Did you check the documentation about eager loading and the with method? Your issue could be easily resolved using a better eloquent query. Once I get behind a computer I'll write an example unless someone beat me to it.Angora
@Angora with won't help. It's whereHas that is needed - like in the answer below.Mentality
N
209

The merge method returns the merged collection, it doesn't mutate the original collection, thus you need to do the following

$original = new Collection(['foo']);

$latest = new Collection(['bar']);

$merged = $original->merge($latest); // Contains foo and bar.

Applying the example to your code

$related = new Collection();

foreach ($question->tags as $tag)
{
    $related = $related->merge($tag->questions);
}
Nicknack answered 29/5, 2015 at 7:54 Comment(4)
I was trying to build a flat list from a tree, using push was what I needed, but the foreach approach really helped.Menstruation
Bear in mind that Eloquent collections don't behave like normal Collections, ie. they use getKey to merge results, so Model::all()->merge(Model::all())->count() === Model::all()->count()Bashan
be aware that The merge method will replace any first collection in the original second collection's items if a string key with the same value exists in the supplied $items. If the first collection keys are numeric, the second collection will be added to the end of the new collection's laravel.com/docs/8.x/collections#method-mergeHacking
I think removing duplicates is required.Hagen
Z
49

The merge() method on the Collection does not modify the collection on which it was called. It returns a new collection with the new data merged in. You would need:

$related = $related->merge($tag->questions);

However, I think you're tackling the problem from the wrong angle.

Since you're looking for questions that meet a certain criteria, it would probably be easier to query in that manner. The has() and whereHas() methods are used to generate a query based on the existence of a related record.

If you were just looking for questions that have any tag, you would use the has() method. Since you're looking for questions with a specific tag, you would use the whereHas() to add the condition.

So, if you want all the questions that have at least one tag with either 'Travel', 'Trains', or 'Culture', your query would look like:

$questions = Question::whereHas('tags', function($q) {
    $q->whereIn('name', ['Travel', 'Trains', 'Culture']);
})->get();

If you wanted all questions that had all three of those tags, your query would look like:

$questions = Question::whereHas('tags', function($q) {
    $q->where('name', 'Travel');
})->whereHas('tags', function($q) {
    $q->where('name', 'Trains');
})->whereHas('tags', function($q) {
    $q->where('name', 'Culture');
})->get();
Zirkle answered 29/5, 2015 at 6:53 Comment(2)
+, however the 2nd (all the tags) option you suggested might be simplified: https://mcmap.net/q/161619/-laravel-tag-search-to-return-images-that-contain-all-tagsMentality
but you can't hardcode the tag names. In this example, the question happens to have those tags, but in other questions the tags will varyIslamite
T
35
$users = User::all();
$associates = Associate::all();

$userAndAssociate = $users->merge($associates);
Thorough answered 14/2, 2018 at 11:49 Comment(3)
Read this (overwriting): medium.com/@tadaspaplauskas/…Derbent
@Derbent it is really unbelievable that it merges "duplicates" based on the ID aloneHarder
merge is for merge, concat is for concat.Hama
D
20

Merge two different eloquent collections into one and some objects happen to have the same id, one will overwrite the other. Use push() method instead or rethink your approach to the problem to avoid that. Refer to web

Dayna answered 19/12, 2017 at 23:16 Comment(2)
Thanks, this put me onto the comment found here medium.com/@jeffparr_57441/… which seems to cleanly do the job without overwriting.Knitter
use ->push() is saferHacking
A
17

Creating a new base collection for each eloquent collection the merge works for me.

$foo = collect(Foo::all());
$bar = collect(Bar::all());
$merged = $foo->merge($bar);

In this case don't have conflits by its primary keys.

Arraign answered 23/3, 2020 at 17:59 Comment(0)
S
7

I have faced some issue by using merge. So I used concat. You can used it like below.

$users = User::all();
$associates = Associate::all();
$userAndAssociate = $users->concat($associates);
Substratum answered 21/12, 2021 at 6:5 Comment(0)
H
2

All do not work for me on eloquent collections, laravel eloquent collections use the key from the items I think which causes merging issues, you need to get the first collection back as an array, put that into a fresh collection and then push the others into the new collection;

public function getFixturesAttribute()
{
    $fixtures = collect( $this->homeFixtures->all() );
    $this->awayFixtures->each( function( $fixture ) use ( $fixtures ) {
        $fixtures->push( $fixture );
    });
    return $fixtures;
}
Huerta answered 20/4, 2018 at 11:59 Comment(0)
D
0

I would like to add that, i found that the concat method does not seem to override based on ID, while the merge method does. concat seems to work for me, while merge caused issues.

Drama answered 13/12, 2021 at 10:19 Comment(0)
E
-1

I'm sorry about that, but since PHP 7.4 you're available to do like this (better use merge).

$foo = Foo::all();
$bar = Bar::all();

/** $foo will contain $foo + $bar */
$foo->push(...$bar);
Entranceway answered 16/2, 2022 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.