korridor/laravel-has-many-merged
Laravel package adding a “hasManyMerged” relationship to combine results from multiple hasMany relations into one merged collection/query. Useful for aggregating related models across different types or sources while keeping a familiar Eloquent API.
Installation
composer require korridor/laravel-has-many-merged
No additional configuration or publisher required—just require the package.
Basic Usage Define a merged relationship in your Eloquent model:
use Korridor\HasManyMerged\HasManyMerged;
class Post extends Model
{
public function mergedComments(): HasManyMerged
{
return $this->hasManyMerged(
Comment::class, // Base model
['comments', 'replies'] // Relationship names to merge
);
}
}
First Query
$post = Post::find(1);
$mergedComments = $post->mergedComments; // Returns merged results from `comments` and `replies`
Eager Loading
$posts = Post::with('mergedComments')->get();
hasMany RelationshipsUse Case: Combine results from multiple hasMany relations (e.g., comments + replies).
class Post extends Model
{
public function mergedComments(): HasManyMerged
{
return $this->hasManyMerged(
Comment::class, // Base model
['comments', 'replies'] // Relationship names
);
}
}
Key Points:
Use Case: Merge polymorphic relations (e.g., comments on Post and Article).
public function mergedComments(): HasManyMerged
{
return $this->hasManyMerged(
Comment::class,
['comments', 'replies'],
['commentable_id', 'commentable_type'] // Polymorphic keys
);
}
Notes:
Use Case: Filter, order, or deduplicate merged results.
public function mergedComments(): HasManyMerged
{
return $this->hasManyMerged(Comment::class, ['comments', 'replies'])
->orderBy('created_at', 'desc')
->where('is_active', true)
->unique('id'); // Deduplicate by ID
}
Advanced Customization:
mergeUsing() to apply custom logic during merging:
$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->mergeUsing(fn ($query) => $query->where('priority', 'high'));
Use Case: Conditionally include/exclude relations based on runtime logic.
public function dynamicMergedComments(bool $includeReplies = true): HasManyMerged
{
$relations = ['comments'];
if ($includeReplies) {
$relations[] = 'replies';
}
return $this->hasManyMerged(Comment::class, $relations);
}
Example Usage:
$post->dynamicMergedComments($user->hasPermission('view_replies'));
Use Case: Serialize merged relations in JSON:API or custom responses.
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'comments' => $this->whenLoaded('mergedComments', fn () => $this->mergedComments),
];
}
}
Notes:
whenLoaded() to avoid errors if the relationship isn’t eager-loaded.->makeVisible()/->makeHidden() as needed.Use Case: Reduce memory usage and query load.
$posts = Post::with(['mergedComments' => function ($query) {
$query->limit(100); // Limit merged results
}])->get();
Best Practices:
mergeUsing in high-traffic endpoints.Use Case: Add reusable query scopes or methods.
// In a service provider or model trait
HasManyMerged::macro('activeOnly', function () {
return $this->where('is_active', true);
});
Usage:
$post->mergedComments->activeOnly();
Pitfall: Merged results may contain duplicates if the same record exists in multiple relations. Solutions:
$merged = $post->mergedComments->unique('id');
$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->unique('id');
->mergeUsing(fn ($query) => $query->whereNotIn('id', $alreadyMergedIds));
Pitfall: Incorrect polymorphic keys cause silent failures or incorrect joins. Debugging:
commentable_id and commentable_type match your model’s structure.dd($this->mergedComments->getQuery()->toSql());
Pitfall: Merging large datasets can be slow or memory-intensive. Mitigations:
$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->limit(50);
cursor() for large datasets:
$post->mergedComments->cursor();
mergeUsing BehaviorPitfall: mergeUsing callbacks run after all relations are loaded.
Example:
$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->mergeUsing(fn ($query) => $query->where('is_active', true));
// Filters the final merged result, not individual queries.
Workaround: Apply filters to individual relations first:
$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->where(function ($query) {
$query->where('is_active', true)
->orWhere('created_at', '>', now()->subDays(7));
});
Pitfall: Merged relations may not serialize correctly in JSON. Fix:
Comment) implements JsonSerializable or uses Arrayable.$this->hasManyMerged(Comment::class, ['comments', 'replies'])
->makeHidden(['pivot']);
Common Test Scenarios:
orderBy works as expected.Example Test:
public function test_merged_relations_with_duplicates()
{
$post = Post::factory()->create();
$comment = Comment::factory()->create(['commentable_id' => $post->id]);
// Insert duplicate in another relation
Comment::factory()->create([
'commentable_id' => $post->id,
'id' => $comment->id, // Duplicate ID
]);
$merged = $post->mergedComments->unique('id');
$this->assertCount(1, $merged);
}
Tool: Use Laravel Debugbar or toSql() to inspect queries.
dd($this->mergedComments->getQuery()->toSql());
Common Issues:
where clauses in polymorphic relationsHow can I help you explore Laravel packages today?