staudenmeir/eloquent-has-many-deep
Extend Eloquent’s HasManyThrough to traverse unlimited intermediate models. Define deep relationships by concatenating existing relations or manually specifying models/keys. Supports many-to-many, polymorphic relations, and combinations, plus some third‑party packages.
Installation:
composer require staudenmeir/eloquent-has-many-deep:"^1.7"
Ensure compatibility with your Laravel version (check version table).
Basic Usage: Add the trait to your model:
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
Define a deep relationship via concatenation:
public function comments()
{
return $this->hasManyDeepFromRelations($this->posts(), (new Post())->comments());
}
First Use Case: Fetch nested data in a single query:
$country = Country::find(1);
$comments = $country->comments()->get(); // Retrieves Country → User → Post → Comment
Concatenating Existing Relationships:
hasManyDeepFromRelations() for chaining existing relationships (e.g., hasManyThrough + hasMany).Country → User → Post → Comment:
$this->hasManyDeepFromRelations($this->users(), (new User())->posts(), (new Post())->comments());
Manual Definition:
$this->hasManyDeep(
Comment::class, // Target model
[User::class, Post::class], // Intermediate models
['user_id', 'post_id'], // Foreign keys
['id', 'id'] // Local keys
);
Query Constraints:
$this->hasManyDeep(Comment::class, [User::class, Post::class])
->where('posts.published', true)
->where('users.active', true);
Many-to-Many Paths:
$this->hasManyDeep(Permission::class, ['role_user', Role::class, 'permission_role']);
Polymorphic Relationships:
['commentable_type', 'commentable_id']):
$this->hasManyDeep(Comment::class, [Post::class], [null, ['commentable_type', 'commentable_id']]);
withTrashed() or withoutTrashed() on intermediate relationships.with() for optimized queries:
Country::with('comments')->get();
laravel-adjacency-list or compoships for hierarchical data.CompositeKey for multi-column joins:
use \Staudenmeir\EloquentHasManyDeep\CompositeKey;
$this->hasManyDeep(..., [new CompositeKey(['col1', 'col2']), ...]);
Column Ambiguity:
where() clauses if they exist in multiple tables:
->where('posts.published', true) // Not ->where('published', true)
Key Mismatches:
// Pivot table keys: foreign (left) → local (right)
['user_id', 'role_id'] // Correct for role_user pivot
Constraint Propagation:
hasManyDeepFromRelationsWithConstraints() to enforce them:
$this->hasManyDeepFromRelationsWithConstraints(
[$this, 'users'], // Callable returning constrained relationship
[(new User()), 'posts']
);
Polymorphic Quirks:
*_type:
[null, ['commentable_type', 'commentable_id']] // Incorrect: ['commentable_id', 'commentable_type']
Soft Deletes:
->withTrashed() // Include soft-deleted intermediates
->withoutTrashed() // Exclude soft-deleted intermediates
\DB::enableQueryLog();
$comments = $country->comments()->get();
dd(\DB::getQueryLog());
HasManyDeep return type for autocompletion:
public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep { ... }
Custom Constraints:
$this->hasManyDeep(Comment::class, [User::class, Post::class])
->where(function($query) {
$query->where('users.country_id', $this->id);
});
Dynamic Relationships:
if ($this->isAdmin()) {
return $this->hasManyDeep(..., [User::class, Post::class]);
}
Performance:
select() to limit columns:
->select(['comments.id', 'comments.body'])
N+1 queries with eager loading:
Country::with('comments')->get();
Testing:
$country = new Country();
$country->shouldReceive('users')->andReturn(new Collection([$user]));
How can I help you explore Laravel packages today?