kirschbaum-development/eloquent-power-joins
Eloquent Power Joins brings Laravel's magic to joins: define joins using your relationship methods, use model scopes in any context, query relationship existence via joins instead of where exists, and sort by related columns or aggregates with less code.
join('posts', 'posts.user_id', '=', 'users.id') scattered across controllers and repositories is a place where a relationship change (renamed FK, added polymorphic type, new soft-delete requirement) silently breaks. This package makes joins derive from relationship definitions, so the schema contract lives in one place — the model. That is a maintainability decision you can justify on reduced regression surface, not on lines of code saved.orderByPowerJoinsCount, orderByPowerJoinsAvg, joinRelationship('posts.comments'), powerJoinHas). This directly changes what you can put in a sprint. Requests that previously required a reporting table, a denormalized counter column, or a hand-rolled query builder chain become ticket-sized. Expect a backlog of "nice to have" sorting/filtering asks to become newly cheap — re-triage them.has() / whereHas() compile to where exists, which on large tables with poor index coverage can be markedly slower than a join. powerJoinHas / powerJoinWhereHas give you a drop-in alternative. This is a lever for a specific class of slow endpoint — typically admin panels, exports, and any listing where the correlated subquery is evaluated per row. Frame it as an option, not a default: it is a targeted optimization, not a global policy.joinRelationshipUsingAlias, ->as('alias')) exists because you need to join the same table twice — exactly the shape of reporting queries (category + parent category, assignee + reporter, billing + shipping address). Without aliasing, engineers either write raw SQL or duplicate models. This is where the package earns its keep fastest.User::joinRelationship('posts.comments') on day one. Compare against a bespoke query-builder abstraction that only your two senior engineers understand.Adopt when:
->join(...) chains against related tables, the payoff is immediate.imageable_type condition is a correctness feature, not a convenience one — hand-written polymorphic joins are a classic source of silent duplicate-row bugs.SoftDeletes. Auto-applied deleted_at is null on every join removes a whole class of "why is this count wrong" tickets.whereHas is the suspected culprit and a join is semantically acceptable. Prototype one endpoint, measure, decide.Look elsewhere, or do not adopt, when:
where exists and join semantics. The package documentation is explicit about this: results will not always match has()/whereHas(), because a join multiplies parent rows per child. If you sort or paginate on a joined one-to-many, you need distinct or grouping, and pagination counts can shift. This is a real footgun and it is a knowledge problem, not a library problem. Do not hand this to a team that will treat powerJoinHas as a literal rename for has.with()) is still the answer for hydration; this package is about filtering and sorting in the database. Conflating the two will produce a bad rollout.MorphTo joins are central to your model. Only one morphable type per query is supported. If you need to join across multiple morph targets in one result set, this will not carry you — plan a union or separate queries.withGlobalScopes()), and scopes cannot type-hint Eloquent\Builder in apply() or you get errors. Audit your scope definitions before rollout; this is the most likely source of a surprise.Note on the "0 dependents" figure. Treat that as a data artifact rather than a signal — a package with this download volume and star count in the Laravel ecosystem is not actually unused. Do not let that number drive a rejection; validate against Packagist download counts and your own spike instead.
To executives (one paragraph): We can cut a recurring class of database query code out of our Laravel application by adopting a small, free, open-source package. It lets engineers express joins through the model relationships we already maintain, instead of hand-writing SQL table names and foreign keys in every controller. Practically, this means: fewer production bugs when schema changes, faster delivery on the "sort and filter by related data" requests that keep landing in the backlog, and a targeted fix available for slow admin and reporting screens currently using a less efficient query pattern. Cost is a Composer dependency and roughly one sprint of adoption work; risk is contained because the fallback is the code we already write today, and it is MIT-licensed with no vendor lock-in.
To engineering leadership: This is a developer-experience and correctness play, not an architecture change. It replaces error-prone manual join construction with relationship-derived joins — including automatic polymorphic type conditions, automatic soft-delete handling, and support for relationship-level constraints. It also exposes a join-based alternative to whereHas for the specific slow queries where correlated subqueries underperform, plus first-class ordering by related columns and aggregates (COUNT/SUM/AVG/MIN/MAX), which currently require bespoke query builder code. Notable constraints to plan around: join semantics differ from where exists (row multiplication, so distinct/grouping discipline is required), MorphTo supports one morphable type per query, global scopes are opt-in and cannot type-hint Eloquent\Builder, and the package targets Laravel 11/12/13 (pinned majors for older versions). None of these are blockers; all of them are things a short internal style note resolves.
To engineering (the honest version): It will make the common case dramatically more readable — User::joinRelationship('posts.comments') instead of a two-table join chain — and it will let you call model scopes inside join callbacks, which is the feature you'll actually reach for. It will not save you from thinking about whether a join is the right tool. If you join a hasMany and paginate, you own the duplicate-row consequences. Adopt it deliberately in one area first — I'd suggest a single reporting or admin surface where the query is already complex — and let the pattern spread from a working example rather than a mandate.
Suggested adoption motion: Spike one slow or gnarly endpoint (one that already joins two-plus tables, or joins the same table twice and needs an alias). Measure before/after on query time and code volume. If it lands, write a two-paragraph internal convention covering join-vs-where exists semantics and global-scope type-hinting, then open it up. Re-review the backlog afterward — several deferred "sort by related field" items likely just became cheap.
How can I help you explore Laravel packages today?