Skip to content

Commit 3d8fd2e

Browse files
committed
Added cross joins support
1 parent f2f8d35 commit 3d8fd2e

File tree

6 files changed

+716
-4
lines changed

6 files changed

+716
-4
lines changed

README.md

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Joins are very useful in a lot of ways. If you are here, you most likely know ab
1212

1313
A few things we consider is missing when using joins which are very powerful Eloquent features:
1414

15-
* Ability to use relationship definitions to make joins;
15+
* Ability to use relationship definitions to make joins (inner, left, right, and cross joins);
1616
* Ability to use model scopes inside different contexts;
1717
* Ability to query relationship existence using joins instead of where exists;
1818
* Ability to easily sort results based on columns or aggregations from related tables;
@@ -59,11 +59,12 @@ But, **it gets better** when you need to **join nested relationships**. Let's as
5959
User::joinRelationship('posts.comments');
6060
```
6161

62-
So much better, wouldn't you agree?! You can also `left` or `right` join the relationships as needed.
62+
So much better, wouldn't you agree?! You can also `left`, `right`, or `cross` join the relationships as needed.
6363

6464
```php
6565
User::leftJoinRelationship('posts.comments');
6666
User::rightJoinRelationship('posts.comments');
67+
User::crossJoinRelationship('posts.comments');
6768
```
6869

6970
#### Joining polymorphic relationships
@@ -145,10 +146,11 @@ When using model scopes inside a join clause, you **can't** type hint the `$quer
145146

146147
#### Using aliases
147148

148-
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.
149+
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. This works for all join types including cross joins.
149150

150151
```php
151152
Post::joinRelationshipUsingAlias('category.parent')->get();
153+
Post::crossJoinRelationshipUsingAlias('category.parent')->get();
152154
```
153155

154156
In case you need to specify the name of the alias which is going to be used, you can do in two different ways:
@@ -203,16 +205,18 @@ When joining any models which uses the `SoftDeletes` trait, the following condit
203205
and "users"."deleted_at" is null
204206
```
205207

206-
In case you want to include trashed models, you can call the `->withTrashed()` method in the join callback.
208+
In case you want to include trashed models, you can call the `->withTrashed()` method in the join callback. This works for all join types:
207209

208210
```php
209211
UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());
212+
UserProfile::crossJoinRelationship('users', fn ($join) => $join->withTrashed());
210213
```
211214

212215
You can also call the `onlyTrashed` model as well:
213216

214217
```php
215218
UserProfile::joinRelationship('users', ($join) => $join->onlyTrashed());
219+
UserProfile::crossJoinRelationship('users', ($join) => $join->onlyTrashed());
216220
```
217221

218222
#### Extra conditions defined in relationships
@@ -245,6 +249,76 @@ UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());
245249

246250
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.
247251

252+
#### Cross Joins
253+
254+
Cross joins generate a cartesian product between the first table and the joined table. This package provides cross join support for relationships, following the same patterns as other join types.
255+
256+
```php
257+
// Basic cross join
258+
User::crossJoinRelationship('posts');
259+
260+
// Cross join with nested relationships
261+
User::crossJoinRelationship('posts.comments');
262+
263+
// Cross join with aliases
264+
User::crossJoinRelationshipUsingAlias('posts');
265+
```
266+
267+
**Cross joins with conditions and scopes**
268+
269+
Even though cross joins don't typically have join conditions, you can still apply conditions and use model scopes in the callback. These conditions will be applied to the main query's WHERE clause:
270+
271+
```php
272+
// Using model scopes
273+
User::crossJoinRelationship('posts', function ($join) {
274+
$join->published();
275+
});
276+
277+
// Using custom conditions
278+
User::crossJoinRelationship('posts', function ($join) {
279+
$join->where('posts.created_at', '>', now()->subDays(30));
280+
});
281+
282+
// Using aliases in callback
283+
User::crossJoinRelationship('posts', function ($join) {
284+
$join->as('p');
285+
});
286+
```
287+
288+
**Cross joins with soft deletes**
289+
290+
Cross joins automatically handle soft deletes, just like other join types:
291+
292+
```php
293+
// Excludes soft deleted posts (default behavior)
294+
User::crossJoinRelationship('posts');
295+
296+
// Includes soft deleted posts
297+
User::crossJoinRelationship('posts', function ($join) {
298+
$join->withTrashed();
299+
});
300+
```
301+
302+
**Cross joins with BelongsToMany relationships**
303+
304+
Cross joins work with all relationship types, including many-to-many relationships:
305+
306+
```php
307+
// Cross join posts with their tags (through pivot table)
308+
Post::crossJoinRelationship('tags');
309+
```
310+
311+
**Understanding cartesian products**
312+
313+
Cross joins create a cartesian product, meaning every row from the first table is combined with every row from the second table:
314+
315+
```php
316+
// If you have 2 users and 3 posts, this will return 6 rows (2 × 3)
317+
$results = User::crossJoinRelationship('posts')
318+
->select('users.name', 'posts.title')
319+
->get();
320+
```
321+
248322
### 2 - Querying relationship existence (Using Joins)
249323

250324
[Querying relationship existence](https://laravel.com/docs/7.x/eloquent-relationships#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.

src/JoinsHelper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static function make($model): static
5858
'join' => 'powerJoin',
5959
'leftJoin' => 'leftPowerJoin',
6060
'rightJoin' => 'rightPowerJoin',
61+
'crossJoin' => 'crossPowerJoin',
6162
];
6263

6364
/**

src/Mixins/JoinRelationship.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ public function rightPowerJoin(): Closure
7979
};
8080
}
8181

82+
/**
83+
* New clause for making cross joins, where we pass the model to the joiner class.
84+
*/
85+
public function crossPowerJoin(): Closure
86+
{
87+
return function ($table, $first = null, $operator = null, $second = null) {
88+
// For cross joins, we need to handle the model parameter differently
89+
// The model can be passed as $operator (3rd parameter) when $first is a Closure
90+
$model = null;
91+
if ($first instanceof Model) {
92+
$model = $first;
93+
} elseif ($operator instanceof Model) {
94+
$model = $operator;
95+
}
96+
97+
$join = $this->newPowerJoinClause($this->query, 'cross', $table, $model);
98+
99+
// Cross joins don't have conditions by default, but we can still apply callbacks
100+
if ($first instanceof Closure) {
101+
$first($join);
102+
}
103+
104+
$this->query->joins[] = $join;
105+
$this->query->addBinding($join->getBindings(), 'join');
106+
107+
return $this;
108+
};
109+
}
110+
82111
public function newPowerJoinClause(): Closure
83112
{
84113
return function (QueryBuilder $parentQuery, string $type, string $table, ?Model $model = null) {
@@ -204,6 +233,16 @@ public function rightJoinRelationshipUsingAlias(): Closure
204233
};
205234
}
206235

236+
/**
237+
* Cross join the relationship(s) using table aliases.
238+
*/
239+
public function crossJoinRelationshipUsingAlias(): Closure
240+
{
241+
return function (string $relationName, Closure|array|string|null $callback = null, bool $disableExtraConditions = false, ?string $morphable = null) {
242+
return $this->joinRelationship($relationName, $callback, 'crossJoin', true, $disableExtraConditions, morphable: $morphable);
243+
};
244+
}
245+
207246
public function joinRelation(): Closure
208247
{
209248
return function (
@@ -246,6 +285,20 @@ public function rightJoinRelation(): Closure
246285
};
247286
}
248287

288+
public function crossJoinRelationship(): Closure
289+
{
290+
return function (string $relation, Closure|array|string|null $callback = null, bool $useAlias = false, bool $disableExtraConditions = false, ?string $morphable = null) {
291+
return $this->joinRelationship($relation, $callback, 'crossJoin', $useAlias, $disableExtraConditions, morphable: $morphable);
292+
};
293+
}
294+
295+
public function crossJoinRelation(): Closure
296+
{
297+
return function (string $relation, Closure|array|string|null $callback = null, bool $useAlias = false, bool $disableExtraConditions = false, ?string $morphable = null) {
298+
return $this->joinRelationship($relation, $callback, 'crossJoin', $useAlias, $disableExtraConditions, morphable: $morphable);
299+
};
300+
}
301+
249302
/**
250303
* Join nested relationships.
251304
*/

0 commit comments

Comments
 (0)