kirschbaum-development/eloquent-power-joins
Eloquent Power Joins brings Laravel-style joins to Eloquent. Join via relationship definitions, reuse model scopes in join contexts, query relationship existence with joins, and sort by related columns/aggregations—all with cleaner, more readable queries.
Installation:
composer require kirschbaum-development/eloquent-power-joins
Ensure compatibility with your Laravel version (11.x/12.x/13.x).
First Use Case:
Replace a basic join with joinRelationship():
// Traditional join
User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');
// Power Joins
User::joinRelationship('posts');
joinRelationship, powerJoinHas, orderByPowerJoins).Relationship-Based Joins:
// Instead of:
User::join('posts', 'posts.user_id', '=', 'users.id');
// Use:
User::joinRelationship('posts');
User::joinRelationship('posts.comments'); // Joins posts → comments
Conditional Joins:
User::joinRelationship('posts', fn($join) => $join->where('posts.approved', true));
User::joinRelationship('posts.comments', [
'posts' => fn($join) => $join->where('posts.published', true),
'comments' => fn($join) => $join->where('comments.approved', true),
]);
Polymorphic Relationships:
imageable_type:
Post::joinRelationship('images'); // Adds `where imageable_type = 'App\Models\Post'`
Querying Existence:
whereHas/has with join-based equivalents:
// Traditional:
User::has('posts')->whereHas('posts', fn($q) => $q->where('posts.published', true));
// Power Joins:
User::powerJoinHas('posts')
->powerJoinWhereHas('posts', fn($join) => $join->where('posts.published', true));
Sorting by Related Data:
User::orderByPowerJoins('profile.city');
User::orderByPowerJoinsCount('posts.id', 'desc'); // Sort by post count
$query):
User::joinRelationship('posts', fn($join) => $join->published());
UserProfile::joinRelationship('users', fn($join) => $join->withTrashed());
withGlobalScopes() (avoid type-hinting Builder):
UserProfile::joinRelationship('users', fn($join) => $join->withGlobalScopes());
Post::joinRelationshipUsingAlias('category.parent');
Scope Type-Hinting:
$query in model scopes breaks join callbacks.Builder or no type-hint:
// ❌ Broken:
public function scopePublished($query) { ... } // Type-hinted
// ✅ Works:
public function scopePublished($query) { ... } // No type-hint
Global Scope Conflicts:
withGlobalScopes() and avoid Builder type-hinting.Nested Closures in Aliases:
>=2.2.2 or restructure callbacks.Polymorphic Limitations:
Image::joinRelationship('imageable', morphable: Post::class)).Aggregation Quirks:
orderByPowerJoinsCount uses LEFT JOIN by default (use orderByLeftPowerJoinsCount explicitly if needed).toSql() to verify generated queries:
User::joinRelationship('posts')->toSql();
joinRelationshipUsingAlias).deleted_at conditions are present/absent as expected.Custom Join Logic: Extend the package by publishing a macro or creating a trait:
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Builder;
use Kirschbaum\PowerJoins\PowerJoins;
Builder::macro('customJoin', function ($relation) {
return $this->joinRelationship($relation, fn($join) => $join->where(...));
});
Query Builder Integration:
Add methods to QueryBuilder for reusable join patterns:
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Query\Builder;
Builder::macro('joinWithConditions', function ($relation, array $conditions) {
return $this->joinRelationship($relation, $conditions);
});
Testing:
Mock joins in tests using joinRelationship:
$user = User::factory()->create();
Post::factory()->count(3)->create(['user_id' => $user->id]);
$this->assertCount(1, User::joinRelationship('posts')->get());
select('*') to prevent column conflicts:
User::select(['users.id', 'posts.title'])->joinRelationship('posts');
leftJoinRelationship for optional relationships to avoid filtering out records.How can I help you explore Laravel packages today?