korridor/laravel-has-many-merged
Add a custom Eloquent hasManyMerged relationship to merge multiple hasMany relations into one collection. Query, eager load, sort, and paginate merged results as a single relation while keeping models and constraints intact.
Installation:
composer require korridor/laravel-has-many-merged
No additional configuration or service provider setup is required.
Define the Relationship:
In your Eloquent model (e.g., Post.php), replace manual merge logic with hasManyMerged:
use Korridor\HasManyMerged\HasManyMerged;
public function comments(): HasManyMerged
{
return $this->hasManyMerged([
// Primary relationship (e.g., direct comments)
$this->hasMany(Comment::class),
// Secondary relationship (e.g., replies)
$this->hasMany(Reply::class)->where('parent_id', $this->id),
]);
}
First Use Case: Fetch merged results in a controller or blade view:
$post = Post::with('comments')->find(1);
// $post->comments now contains a unified collection of Comment and Reply models.
Key Files to Review:
HasManyMerged.php: Core class with query-building logic.HasManyMergedBuilder.php: Query builder methods (e.g., where, orderBy).tests/: Real-world examples (e.g., polymorphic merging, withCount).Combine multiple hasMany relationships into a single query:
public function transactions(): HasManyMerged
{
return $this->hasManyMerged([
$this->hasMany(Order::class),
$this->hasMany(Payment::class),
$this->hasMany(Refund::class),
]);
}
Workflow:
with() for eager loading:
$user = User::with('transactions')->find(1);
foreach ($user->transactions as $transaction) {
// $transaction is an instance of Order|Payment|Refund
}
Merge relationships from polymorphic models (e.g., Comment on Post/Article):
public function comments(): HasManyMerged
{
return $this->hasManyMerged([
$this->morphMany(Comment::class, 'commentable'),
$this->morphMany(Reply::class, 'commentable'),
]);
}
Tip: Ensure polymorphic keys (commentable_id, commentable_type) match across models.
Dynamically include/exclude relationships based on logic:
public function activity(): HasManyMerged
{
$relations = [$this->hasMany(ActivityLog::class)];
if ($this->isAdmin()) {
$relations[] = $this->hasMany(AuditLog::class);
}
return $this->hasManyMerged($relations);
}
Apply constraints to the merged query:
$user->transactions()
->where('status', 'completed')
->whereBetween('created_at', [$startDate, $endDate])
->orderBy('amount', 'desc');
Combine with() and whereHas for optimized queries:
$posts = Post::with(['comments' => function ($query) {
$query->where('is_public', true);
}])->get();
Override how models are merged (e.g., deduplication, field prioritization):
public function comments(): HasManyMerged
{
return $this->hasManyMerged([
$this->hasMany(Comment::class),
$this->hasMany(Reply::class),
])->mergeUsing(function ($source, $target) {
// Custom logic: e.g., prioritize Replies over Comments
return $target->merge($source)->sortBy('created_at');
});
}
Use JsonResource to serialize merged relationships:
public function toArray($request)
{
return [
'id' => $this->id,
'transactions' => TransactionResource::collection($this->whenLoaded('transactions')),
];
}
Validate merged relationships in requests:
public function rules()
{
return [
'post_id' => 'required|exists:posts,id',
'comments.*' => 'required|array', // Validates merged Comment/Reply structure
];
}
Test merged relationships with assertHasManyMerged (if available) or custom assertions:
public function test_merged_relationships()
{
$post = Post::factory()->create();
$comment = Comment::factory()->create(['post_id' => $post->id]);
$reply = Reply::factory()->create(['post_id' => $post->id]);
$this->assertCount(2, $post->comments);
$this->assertInstanceOf(Comment::class, $post->comments->first());
$this->assertInstanceOf(Reply::class, $post->comments->last());
}
with() when accessing merged relationships.paginate() on merged queries for large datasets:
$user->transactions()->paginate(10);
select() to reduce memory usage:
$user->transactions()->select(['id', 'amount', 'created_at']);
commentable_id, commentable_type) don’t match across models, the merge will fail silently or return empty results.$this->morphMany(Comment::class, 'commentable')
->where('commentable_type', Post::class);
id in comments and replies) may cause duplicates.mergeUsing to deduplicate:
->mergeUsing(function ($source, $target) {
return $target->merge($source)->unique('id');
});
orWhere, groupBy) may not work as expected on merged queries.$this->hasManyMerged([
$this->hasMany(Comment::class)->where('is_approved', true),
$this->hasMany(Reply::class)->where('is_approved', true),
]);
$user->transactions()->paginate(20);
// OR
$user->transactions()->cursor();
@phpstan-ignore-line or cast to a base model:
/** @var \Illuminate\Support\Collection|\Korridor\HasManyMerged\HasManyMergedCollection */
$collection = $user->transactions;
Use Laravel’s query logging or Debugbar to verify the merged query:
\DB::enableQueryLog();
$user->transactions->toSql(); // Check the final query
\DB::getQueryLog();
Override mergeUsing to debug merging behavior:
->mergeUsing(function ($source, $target) {
\Log::debug('Merging source:', $source->toArray());
\Log::debug('Target before merge:', $target->toArray());
return $target->merge($source);
});
Ensure all
How can I help you explore Laravel packages today?