Open-source project
staudenmeir/eloquent-has-many-deep avatar
staudenmeir/eloquent-has-many-deep

staudenmeir/eloquent-has-many-deep: Deep Eloquent Relationships in Laravel

Laravel Eloquent HasManyThrough relationships with unlimited levels

2,873 stars158 forksPHPMIT

At a glance

What is it?
The package extends HasManyThrough to any number of intermediate models, and it does so by concatenating relationships you already have or by taking the models and keys directly. The trade-off is that constraints are dropped unless you opt in, and the query it builds is a join chain rather than a set of nested queries.
Who is it for?
Adopt it when a relationship genuinely crosses three or more tables and you want the result as an Eloquent relationship you can eager load, constrain and paginate. Do not adopt it to hide a schema that should be normalised, and do not expect it to reproduce the constraints of the relationships you concatenate, because by default it does not.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap this fills in Eloquent's relationship set

Laravel ships HasManyThrough for exactly one intermediate model. Country reaches User through nothing, and User reaches Post through nothing; to reach Comment from Country you would need two intermediate models, which the framework does not provide. The usual workaround is a nested whereHas, which is a subquery and does not give you a relationship object, or a manual join, which does not give you eager loading or relationship constraints.

This package adds HasManyDeep and HasOneDeep, which accept an arbitrary number of intermediate models. The README's own example is the Laravel documentation example extended by one level: Country has many User, User has many Post, Post has many Comment. The audience is people with schemas that already look like this, typically reporting or catalogue models sitting several joins away from the data they need. It is not a general query builder and it does not replace joins you write by hand for aggregate work.

Two ways to declare a chain, and what each one costs

The package offers two declaration styles, and the README is explicit that they are alternatives rather than layers.

The first concatenates relationships that already exist. You call hasManyDeepFromRelations() and pass the relationship objects, so the chain Country to Post to Comment is expressed as $this->posts() followed by (new Post())->comments(). If you already have those methods, this is the cheaper option and it keeps the key conventions in one place. The catch is stated in the README: constraints from the concatenated relationships are not transferred. A where('posts.published', true) on the posts() relationship disappears from the deep relationship. To keep it you switch to hasManyDeepFromRelationsWithConstraints() and pass the relationships as callable arrays, [$this, 'posts'] and [new Post(), 'comments']. The README also warns to qualify constraint columns when they appear in multiple tables, writing ->where('posts.published', true) rather than ->where('published', true).

The second style declares the chain manually with hasManyDeep(). The first argument is the related model, the second is an array of intermediate models ordered from the far parent to the related model, and the third and fourth arguments are custom foreign and local keys. This is what you use when the intermediate relationships do not exist as methods, or when the keys do not follow Eloquent conventions. It is more typing and it puts the key knowledge in the relationship definition, but nothing is silently dropped, because there is nothing to drop.

The README lists the relationship types the manual form covers: HasMany, ManyToMany, MorphMany, MorphToMany, MorphedByMany and BelongsTo, plus HasOneDeep for single results and composite keys. It also lists third-party relationship classes that can be concatenated: HasManyMerged from korridor/laravel-has-many-merged, the JSON relationships BelongsToJson, HasManyJson and HasManyThroughJson from staudenmeir/eloquent-json-relations, Tree and Graph from staudenmeir/laravel-adjacency-list, and BelongsTo, HasMany and HasOne from topclaudy/compoships.

Installing it and defining a first deep relationship

Installation is a single Composer command. The README pins the constraint to ^1.7, which is the floor that works across the supported Laravel versions rather than the newest release:

bash
composer require staudenmeir/eloquent-has-many-deep:"^1.7"

If you are in PowerShell on Windows, for example inside VS Code, the README gives a second form because PowerShell treats the caret specially:

bash
composer require staudenmeir/eloquent-has-many-deep:"^^^^1.7"

The package supports Laravel 5.5 and later, and the README carries a version table. Laravel 13.x maps to package 1.22, 12.x to 1.21, 11.x to 1.20, 10.x to 1.18, 9.x to 1.17, 8.x to 1.14, 7.x to 1.12, 6.x to 1.11, 5.8 to 1.8, and 5.5 through 5.7 to 1.7. Check that table before pinning a constraint, because the newest package release is not the right one for an older framework.

Then add the trait to the model where the relationship is defined, and declare the chain. This is the manual form from the README, for Country to User to Post to Comment:

php
class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function comments(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
    {
        return $this->hasManyDeep(Comment::class, [User::class, Post::class]);
    }
}

The intermediate array runs from the far parent to the related model, which is the opposite of how the chain reads from the related side. Once the method exists you use it like any other relationship: $country->comments returns a collection, and Country::with('comments') eager loads it. If you only ever want one row, declare hasOneDeep() instead.

Where the abstraction stops being the right tool

The deepest limitation is the one the README states plainly: constraints are not inherited from concatenated relationships. That is not a bug report, it is the default, and it means a relationship that reads correctly can return a wider set of rows than the relationships it was built from. If your posts() method filters published posts, the deep comments relationship built with hasManyDeepFromRelations() will not filter them. You either move to hasManyDeepFromRelationsWithConstraints() or you re-declare the chain manually.

There is a second cost that the README does not discuss. A deep relationship is a chain of joins, so the number of joined tables grows with the number of intermediate models. On wide tables, or on a chain of four or five models, that is a lot of rows for the database to combine before your limit or your where clause narrows anything. The README does not document query plans, index expectations or any guidance on chain length, so treat depth as something to measure against your own data rather than something the package manages for you.

The wrong tool cases follow from that. If you need aggregates over the deep set, a hand-written join with a group by is clearer and easier to reason about than a relationship object. If the chain exists only because a table was never normalised, the relationship hides the problem instead of fixing it. And if you need the intermediate rows themselves rather than the far end, this package returns the far end; the README covers intermediate and pivot data, but the relationship's subject is still the related model.

How it differs from laravel-has-many-merged

The closest thing to an alternative in the README's own list is korridor/laravel-has-many-merged, and the difference is in what each one merges. That package provides HasManyMerged, which combines several sibling relationships on the same model into one. This package provides HasManyDeep, which walks a chain of different models. They solve different shapes of problem: one is about breadth at a single level, the other about depth across levels.

The interesting part is that they compose. The README lists HasManyMerged among the third-party relationship classes you can concatenate, so a merged relationship can be one link in a deep chain. If you are choosing between them, the question is whether your missing relationship is several parallel paths from one model or one path through several models. If it is both, you may end up with the two packages in the same relationship definition.

Maintenance, licensing and the upgrade path

The repository is not archived, and the last push was on 2026-03-14, which is the same timestamp as the v1.22.1 release. The release history around it is close together: v1.22 on 2026-03-01, then v1.21.3 and v1.22.1 on 2026-03-14. That pattern suggests version bumps tied to framework support rather than a stream of feature work. The README's version table is the practical upgrade document, and it is the thing to read before a Laravel major upgrade, because the package version you need changes with the framework version.

The licence is MIT, so the practical implication is that you can use it in closed-source applications and modify it, provided the copyright notice and permission notice are kept. That is a summary of the identifier, not legal advice; read the LICENSE file in the repository if the terms matter to your organisation. The README does not document a rollback procedure, so reverting a package version bump means reverting your Composer constraint and lock file yourself.

Editorial conclusion

Adopt it when a relationship genuinely crosses three or more tables and you want the result as an Eloquent relationship you can eager load, constrain and paginate. Do not adopt it to hide a schema that should be normalised, and do not expect it to reproduce the constraints of the relationships you concatenate, because by default it does not. Before rolling it out, verify two things on your own schema: that the generated SQL is the join chain you expect, and that every column name in your constraints is table-qualified, since an unqualified name that exists in more than one joined table will not resolve the way you intended.

Frequently asked questions

What is staudenmeir/eloquent-has-many-deep used for?

It extends Laravel's HasManyThrough so a relationship can have unlimited intermediate models, and it also supports many-to-many and polymorphic chains. You use it when the related model sits three or more tables away from the model where you are defining the relationship.

How do I install staudenmeir/eloquent-has-many-deep?

Run composer require staudenmeir/eloquent-has-many-deep:"^1.7" from your project root, or the quoted ^^^^1.7 form if you are in PowerShell on Windows. Then add the HasRelationships trait to the model that declares the deep relationship.

Does staudenmeir/eloquent-has-many-deep carry over constraints from the relationships I concatenate?

No. The README states that constraints from concatenated relationships are not transferred by default. Use hasManyDeepFromRelationsWithConstraints() with callable arrays instead, and qualify column names that appear in more than one table.

Which Laravel versions does staudenmeir/eloquent-has-many-deep support?

The README says Laravel 5.5 and later, with a version table mapping each framework version to a package version. For example Laravel 13.x maps to package 1.22 and Laravel 12.x maps to 1.21.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. staudenmeir/eloquent-has-many-deep on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/staudenmeir-eloquent-has-many-deep.svg)](https://hysenlabs.com/projects/staudenmeir-eloquent-has-many-deep)