staudenmeir/eloquent-has-many-deep
Laravel Eloquent extension for “deep” has-many-through relationships across unlimited intermediate models. Supports many-to-many and polymorphic paths, combinations, and some third-party packages. Define relations by concatenating existing ones or configuring keys manually.
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 (e.g., Country → User → Post → Comment):
public function comments()
{
return $this->hasManyDeep(Comment::class, [User::class, Post::class]);
}
First Use Case: Fetch all comments for a country:
$country = Country::find(1);
$comments = $country->comments()->get();
Chaining Existing Relationships:
// Country → User → Post → Comment
public function comments()
{
return $this->hasManyDeepFromRelations(
$this->users(), // Country → User
fn() => (new Post())->comments() // User → Comment
);
}
where) are not automatically applied unless using hasManyDeepFromRelationsWithConstraints.Manual Path Definition:
public function permissions()
{
return $this->hasManyDeep(
Permission::class,
['role_user', Role::class] // Pivot + intermediate model
);
}
Polymorphic Relationships:
public function postComments()
{
return $this->hasManyDeep(
Comment::class,
[Post::class],
[null, ['commentable_type', 'commentable_id']]
);
}
['type', 'id']).Many-to-Many Paths:
public function permissions()
{
return $this->hasManyDeep(
Permission::class,
['role_user', Role::class, 'permission_role']
);
}
Constraints: Apply constraints after defining the relationship:
$this->comments()->where('comments.approved', true);
For complex constraints, use hasManyDeepFromRelationsWithConstraints with callables.
Eager Loading:
Use with() to avoid N+1 queries:
Country::with('comments')->find(1);
Soft Deletes: Enable soft deletes for intermediate models:
$this->comments()->withTrashed();
Unique Results:
Use distinct() or unique() for duplicate-free results:
$this->comments()->distinct();
Reversing Relationships:
Define inverse relationships manually (e.g., Comment → Post → User → Country).
Key Mismatches:
ManyToMany).
// Wrong: Keys not swapped for pivot table.
$this->hasManyDeep(Permission::class, ['role_user', Role::class], ['user_id', 'id']);
$this->hasManyDeep(
Permission::class,
['role_user', Role::class],
['user_id', 'role_id'], // Swapped for pivot
['id', 'id']
);
Constraint Scope:
where('posts.published', true)) may not work if table aliases are missing.$this->comments()->where('posts.published', true);
Polymorphic Ambiguity:
type column is not correctly specified.[null, ['commentable_type', 'commentable_id']]
Soft Deletes:
withTrashed() is omitted.$this->comments()->withTrashed();
Composite Keys:
Staudenmeir\EloquentHasManyDeep\CompositeKey for multi-column relationships.use Staudenmeir\EloquentHasManyDeep\CompositeKey;
$this->hasManyDeep(..., [new CompositeKey(['col1', 'col2'])]);
Query Logs: Enable query logging to inspect generated SQL:
DB::enableQueryLog();
$comments = $this->comments()->get();
dd(DB::getQueryLog());
Relationship Debugging:
Use toSql() to preview the query:
$query = $this->comments()->toSql();
dd($query);
IDE Helper: Install the IDE Helper for autocompletion:
composer require --dev staudenmeir/eloquent-has-many-deep-ide-helper
Custom Constraints: Extend the relationship class to add custom constraints:
class CustomHasManyDeep extends \Staudenmeir\EloquentHasManyDeep\HasManyDeep
{
public function customConstraint()
{
return $this->where('comments.spam', false);
}
}
Third-Party Integration:
laravel-adjacency-list for tree/graph relationships:
$this->hasManyDeepFromRelations(
$this->children(), // Tree relationship
fn() => (new Post())->comments()
);
Dynamic Relationships: Use closures to define dynamic paths:
public function dynamicComments($level)
{
$path = [$this->users()];
for ($i = 0; $i < $level; $i++) {
$path[] = fn() => (new Post())->comments();
}
return $this->hasManyDeepFromRelations(...$path);
}
Performance Optimization:
load() for selective loading:
$country = Country::find(1);
$country->load('comments');
$this->comments()->cursor()->paginate(20);
How can I help you explore Laravel packages today?