kirschbaum-development/eloquent-power-joins
Add “Laravel way” joins to Eloquent: join via relationship definitions, reuse model scopes in joined contexts, query relationship existence with joins, and sort by related columns/aggregations—cleaner, more readable join queries with less boilerplate.
Installation:
composer require kirschbaum-development/eloquent-power-joins
For Laravel <10, use 3.* version.
First Use Case: Replace raw joins with relationship-based joins:
// Before
User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');
// After
User::joinRelationship('posts');
Key Files to Review:
app/Models/ (your Eloquent models with defined relationships)app/Providers/AppServiceProvider.php (if extending package behavior)config/database.php (if customizing join defaults)hasMany, belongsTo) in app/Models/.joinRelationship() integrates with existing queries in your controllers/repositories.Relationship-Based Joins:
// Basic join
User::joinRelationship('posts')->get();
// Nested joins
User::joinRelationship('posts.comments')->get();
// Left/Right joins
User::leftJoinRelationship('posts')->get();
Conditional Joins:
// With callbacks
User::joinRelationship('posts', fn($join) => $join->where('posts.published', true));
// Nested conditions
User::joinRelationship('posts.comments', [
'posts' => fn($join) => $join->where('posts.approved', true),
'comments' => fn($join) => $join->where('comments.spam', false),
]);
Scope Integration:
// Using model scopes in joins
User::joinRelationship('posts', fn($join) => $join->published());
Existence Queries:
// Replace `whereHas` with joins
User::powerJoinHas('posts')->get();
User::powerJoinWhereHas('posts', fn($join) => $join->where('posts.views', '>', 100));
Sorting with Joins:
// Sort by related table columns
User::orderByPowerJoins('profile.city')->get();
// Sort by aggregations
User::orderByPowerJoinsCount('posts.id', 'desc')->get();
Repository Pattern: Centralize complex joins in repository methods:
class UserRepository {
public function withActivePosts() {
return User::joinRelationship('posts', fn($join) => $join->where('posts.active', true));
}
}
Service Layer: Use joins in service methods to encapsulate business logic:
class PostService {
public function getPopularPosts() {
return Post::orderByPowerJoinsCount('comments.id', 'desc')->take(10)->get();
}
}
View Composers: Preload joined data for views:
public function compose(Post $post) {
$post->load(['comments' => function($query) {
$query->joinRelationship('users'); // Nested join
}]);
}
API Resources: Shape responses with joined data:
public function toArray($request) {
return [
'user' => $this->user,
'posts' => $this->whenLoaded('posts', fn() => $this->posts),
'profile' => $this->whenLoaded('profile', fn() => $this->profile),
];
}
Testing: Mock joins in unit tests:
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
]);
$this->assertDatabaseHas('posts', [
'user_id' => $user->id,
'published' => true,
]);
$result = User::joinRelationship('posts')->whereHas('posts', fn($q) => $q->where('published', true))->first();
Scope Type-Hinting:
$query parameters (e.g., Builder $query) will fail in join callbacks.$query or Builder $query without type-hinting:
// ❌ Fails
public function scopePublished($query) { ... }
// ✅ Works
public function scopePublished($query) { ... } // No type-hint
Global Scopes in Joins:
withGlobalScopes() in the join callback:
User::joinRelationship('posts', fn($join) => $join->withGlobalScopes());
Polymorphic Relationships:
Image::joinRelationship('imageable', morphable: Post::class);
Soft Deletes:
withTrashed() or onlyTrashed() to include them:
User::joinRelationship('posts', fn($join) => $join->withTrashed());
Alias Conflicts:
posts.posts) require aliases:
Post::joinRelationshipUsingAlias('posts.comments')->get();
as() in callbacks for granular control:
Post::joinRelationship('posts.comments', [
'posts' => fn($join) => $join->as('post_alias'),
]);
BelongsToMany Joins:
User::joinRelationship('groups', [
'groups' => [
'groups' => fn($join) => $join->where(...),
'group_members' => fn($join) => $join->where(...),
],
]);
Aggregation Sorting:
orderByPowerJoins* methods may not work as expected with LEFT JOIN if the joined table has no matches.orderByLeftPowerJoins* for left joins:
Post::orderByLeftPowerJoinsCount('comments.id', 'desc')->get();
Query Caching:
$query = User::joinRelationship('posts')->remember(60);
SQL Inspection:
toSql() to verify generated queries:
$sql = User::joinRelationship('posts')->toSql();
dd($sql);
Binding Analysis:
getQuery()->getBindings():
dd(User::joinRelationship('posts')->getQuery()->getBindings());
Step-by-Step Joins:
$query = User::query();
$query->joinRelationship('posts');
$query->joinRelationship('posts.comments');
dd($query->toSql());
Relationship Debugging:
getRelationships():
dd(User::first()->getRelationships());
Custom Join Macros:
use Illuminate\Database\Query\Builder;
use Kirschbaum\PowerJoins\PowerJoins;
Builder::macro('customJoin', function($relation, $callback) {
return $this->joinRelationship($relation, $callback);
});
Model Observers:
class PostObserver {
public function retrieving(Post $post) {
if (app()->bound('joinRelationshipCallback')) {
$post->setRelation('posts', $post->posts()->joinRelationship(...));
}
}
}
Service Provider:
AppServiceProvider:
public function boot() {
PowerJoins::macro('customAlias', function($relation, $alias) {
return $this->joinRelationshipUsingAlias($relation, $alias);
How can I help you explore Laravel packages today?