The Laravel magic you know, now applied to joins.
Joins are very useful in a lot of ways. If you are here, you most likely know about and use them. Eloquent is very powerful, but it lacks a bit of the "Laravel way" when using joins. This package make your joins in a more Laravel way, with more readable with less code while hiding implementation details from places they don't need to be exposed.
A few things we consider is missing when using joins which are very powerful Eloquent features:
- Ability to use relationship definitions to make joins;
- Ability to use model scopes inside different contexts;
- Ability to query relationship existence using joins instead of where exists;
- Ability to easily sort results based on columns or aggregations from related tables;
You can read a more detailed explanation on the problems this package solves on this blog post.
You can install the package via composer:
composer require kirschbaum-development/eloquent-power-joins
For Laravel versions < 10, use the 3.* version. For Laravel versions < 8, use the 2.* version:
composer require kirschbaum-development/eloquent-power-joins:3.*
This package provides a few features.
Let's say you have a User
model with a hasMany
relationship to the Post
model. If you want to join the tables, you would usually write something like:
User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');
This package provides you with a new joinRelationship()
method, which does the exact same thing.
User::joinRelationship('posts');
Both options produce the same results. In terms of code, you didn't save THAT much, but you are now using the relationship between the User
and the Post
models to join the tables. This means that you are now hiding how this relationship works behind the scenes (implementation details). You also don't need to change the code if the relationship type changes. You now have more readable and less overwhelming code.
But, it gets better when you need to join nested relationships. Let's assume you also have a hasMany
relationship between the Post
and Comment
models and you need to join these tables, you can simply write:
User::joinRelationship('posts.comments');
So much better, wouldn't you agree?! You can also left
or right
join the relationships as needed.
User::leftJoinRelationship('posts.comments');
User::rightJoinRelationship('posts.comments');
Let's imagine, you have a Image
model that is a polymorphic relationship (Post -> morphMany -> Image
). Besides the regular join, you would also need to apply the where imageable_type = Post::class
condition, otherwise you could get messy results.
Turns out, if you join a polymorphic relationship, Eloquent Power Joins automatically applies this condition for you. You simply need to call the same method.
Post::joinRelationship('images');
You can also join MorphTo relationships.
Image::joinRelationship('imageable', morphable: Post::class);
Note: Querying morph to relationships only supports one morphable type at a time.
Applying conditions & callbacks to the joins
Now, let's say you want to apply a condition to the join you are making. You simply need to pass a callback as the second parameter to the joinRelationship
method.
User::joinRelationship('posts', fn ($join) => $join->where('posts.approved', true))->toSql();
You can also specify the type of join you want to make in the callback:
User::joinRelationship('posts', fn ($join) => $join->left());
For nested calls, you simply need to pass an array referencing the relationship names.
User::joinRelationship('posts.comments', [
'posts' => fn ($join) => $join->where('posts.published', true),
'comments' => fn ($join) => $join->where('comments.approved', true),
]);
For belongs to many calls, you need to pass an array with the relationship, and then an array with the table names.
User::joinRelationship('groups', [
'groups' => [
'groups' => function ($join) {
// ...
},
// group_members is the intermediary table here
'group_members' => fn ($join) => $join->where('group_members.active', true),
]
]);
We consider this one of the most useful features of this package. Let's say, you have a published
scope on your Post
model:
public function scopePublished($query)
{
$query->where('published', true);
}
When joining relationships, you can use the scopes defined in the model being joined. How cool is this?
User::joinRelationship('posts', function ($join) {
// the $join instance here can access any of the scopes defined in Post 🤯
$join->published();
});
When using model scopes inside a join clause, you can't type hint the $query
parameter in your scope. Also, keep in mind you are inside a join, so you are limited to use only conditions supported by joins.
Sometimes, you are going to need to use table aliases on your joins because you are joining the same table more than once. One option to accomplish this is to use the joinRelationshipUsingAlias
method.
Post::joinRelationshipUsingAlias('category.parent')->get();
In case you need to specify the name of the alias which is going to be used, you can do in two different ways:
- Passing a string as the second parameter (this won't work for nested joins):
Post::joinRelationshipUsingAlias('category', 'category_alias')->get();
- Calling the
as
function inside the join callback.
Post::joinRelationship('category.parent', [
'category' => fn ($join) => $join->as('category_alias'),
'parent' => fn ($join) => $join->as('category_parent'),
])->get()
For belongs to many or has many through calls, you need to pass an array with the relationship, and then an array with the table names.
Group::joinRelationship('posts.user', [
'posts' => [
'posts' => fn ($join) => $join->as('posts_alias'),
'post_groups' => fn ($join) => $join->as('post_groups_alias'),
],
])->toSql();
When making joins, using select * from ...
can be dangerous as fields with the same name between the parent and the joined tables could conflict. Thinking on that, if you call the joinRelationship
method without previously selecting any specific columns, Eloquent Power Joins will automatically include that for you. For instance, take a look at the following examples:
User::joinRelationship('posts')->toSql();
// select users.* from users inner join posts on posts.user_id = users.id
And, if you specify the select statement:
User::select('users.id')->joinRelationship('posts')->toSql();
// select users.id from users inner join posts on posts.user_id = users.id
When joining any models which uses the SoftDeletes
trait, the following condition will be also automatically applied to all your joins:
and "users"."deleted_at" is null
In case you want to include trashed models, you can call the ->withTrashed()
method in the join callback.
UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());
You can also call the onlyTrashed
model as well:
UserProfile::joinRelationship('users', ($join) => $join->onlyTrashed());
If you have extra conditions in your relationship definitions, they will get automatically applied for you.
class User extends Model
{
public function publishedPosts()
{
return $this->hasMany(Post::class)->published();
}
}
If you call User::joinRelationship('publishedPosts')->get()
, it will also apply the additional published scope to the join clause. It would produce an SQL more or less like this:
select users.* from users inner join posts on posts.user_id = posts.id and posts.published = 1
If your model have global scopes applied to it, you can enable the global scopes by calling the withGlobalScopes
method in your join clause, like this:
UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());
There's, though, a gotcha here. Your global scope cannot type-hint the Eloquent\Builder
class in the first parameter of the apply
method, otherwise you will get errors.
Querying relationship existence is a very powerful and convenient feature of Eloquent. However, it uses the where exists
syntax which is not always the best and may not be the more performant choice, depending on how many records you have or the structure of your tables.
This packages implements the same functionality, but instead of using the where exists
syntax, it uses joins. Below, you can see the methods this package implements and also the Laravel equivalent.
Please note that although the methods are similar, you will not always get the same results when using joins, depending on the context of your query. You should be aware of the differences between querying the data with where exists
vs joins
.
Laravel Native Methods
User::has('posts');
User::has('posts.comments');
User::has('posts', '>', 3);
User::whereHas('posts', fn ($query) => $query->where('posts.published', true));
User::whereHas('posts.comments', ['posts' => fn ($query) => $query->where('posts.published', true));
User::doesntHave('posts');
Package equivalent, but using joins
User::powerJoinHas('posts');
User::powerJoinHas('posts.comments');
User::powerJoinHas('posts.comments', '>', 3);
User::powerJoinWhereHas('posts', function ($join) {
$join->where('posts.published', true);
});
User::powerJoinDoesntHave('posts');
When using the powerJoinWhereHas
method with relationships that involves more than 1 table (One to Many, Many to Many, etc.), use the array syntax to pass the callback:
User::powerJoinWhereHas('commentsThroughPosts', [
'comments' => fn ($query) => $query->where('body', 'a')
])->get());
You can also sort your query results using a column from another table using the orderByPowerJoins
method.
User::orderByPowerJoins('profile.city');
If you need to pass some raw values for the order by function, you can do like this:
User::orderByPowerJoins(['profile', DB::raw('concat(city, ", ", state)']);
This query will sort the results based on the city
column on the user_profiles
table. You can also sort your results by aggregations (COUNT
, SUM
, AVG
, MIN
or MAX
).
For instance, to sort users with the highest number of posts, you can do this:
$users = User::orderByPowerJoinsCount('posts.id', 'desc')->get();
Or, to get the list of posts where the comments contain the highest average of votes.
$posts = Post::orderByPowerJoinsAvg('comments.votes', 'desc')->get();
You also have methods for SUM
, MIN
and MAX
:
Post::orderByPowerJoinsSum('comments.votes');
Post::orderByPowerJoinsMin('comments.votes');
Post::orderByPowerJoinsMax('comments.votes');
In case you want to use left joins in sorting, you also can:
Post::orderByLeftPowerJoinsCount('comments.votes');
Post::orderByLeftPowerJoinsAvg('comments.votes');
Post::orderByLeftPowerJoinsSum('comments.votes');
Post::orderByLeftPowerJoinsMin('comments.votes');
Post::orderByLeftPowerJoinsMax('comments.votes');
Please see CONTRIBUTING for details.
If you discover any security related issues, please email security@kirschbaumdevelopment.com instead of using the issue tracker.
Development of this package is sponsored by Kirschbaum Development Group, a developer driven company focused on problem solving, team building, and community. Learn more about us or join us!
The MIT License (MIT). Please see License File for more information.