Skip to content

Commit 0547f11

Browse files
committed
add relationships sorts
1 parent 53cf0e2 commit 0547f11

File tree

4 files changed

+169
-5
lines changed

4 files changed

+169
-5
lines changed

src/Builder.php

+25
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,29 @@ public function jsonApiPaginate()
4848
]));
4949
};
5050
}
51+
52+
public function hasJoin()
53+
{
54+
/**
55+
* Check wether join is already on the query instance.
56+
*
57+
* @param string $joinTable
58+
* @return bool
59+
*/
60+
return function ($joinTable) {
61+
$joins = $this->getQuery()->joins;
62+
63+
if ($joins === null) {
64+
return false;
65+
}
66+
67+
foreach ($joins as $join) {
68+
if ($join->table === $joinTable) {
69+
return true;
70+
}
71+
}
72+
73+
return false;
74+
};
75+
}
5176
}

src/Http/ApplyFieldsToQuery.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class ApplyFieldsToQuery implements HandlesRequestQueries
1717
*/
1818
public function from(RequestQueryObject $request, Closure $next)
1919
{
20+
$request->query->select($request->query->getModel()->qualifyColumn('*'));
21+
2022
if (empty($request->fields()) || empty($request->getAllowedFields())) {
2123
return $next($request);
2224
}
@@ -33,7 +35,7 @@ public function from(RequestQueryObject $request, Closure $next)
3335
*/
3436
protected function applyFields(Builder $query, array $fields)
3537
{
36-
/** @var \OpenSoutheners\LaravelApiable\Contracts\JsonApiable|\Illuminate\Database\Eloquent\Model */
38+
/** @var \OpenSoutheners\LaravelApiable\Contracts\JsonApiable|\Illuminate\Database\Eloquent\Model $mainQueryModel */
3739
$mainQueryModel = $query->getModel();
3840
$mainQueryResourceType = Apiable::getResourceType($mainQueryModel);
3941
$queryEagerLoaded = $query->getEagerLoads();
@@ -45,10 +47,10 @@ protected function applyFields(Builder $query, array $fields)
4547
$matchedFn = match (true) {
4648
$mainQueryResourceType === $type => function () use ($query, $mainQueryModel, $columns) {
4749
if (! in_array($mainQueryModel->getKeyName(), $columns)) {
48-
$columns[] = $mainQueryModel->getKeyName();
50+
$columns[] = $mainQueryModel->getQualifiedKeyName();
4951
}
5052

51-
$query->select($columns);
53+
$query->select($mainQueryModel->qualifyColumns($columns));
5254
},
5355
in_array($typeModel, $queryEagerLoaded) => fn () => $query->with($type, function (Builder $query) use ($queryEagerLoaded, $type, $columns) {
5456
$relatedModel = $query->getModel();
@@ -57,7 +59,7 @@ protected function applyFields(Builder $query, array $fields)
5759
$columns[] = $relatedModel->getKeyName();
5860
}
5961

60-
$queryEagerLoaded[$type]($query->select($columns));
62+
$queryEagerLoaded[$type]($query->select($relatedModel->qualifyColumns($columns)));
6163
}),
6264
default => fn () => null,
6365
};

src/Http/ApplySortsToQuery.php

+62-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Closure;
66
use Illuminate\Database\Eloquent\Builder;
7+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
78
use OpenSoutheners\LaravelApiable\Contracts\HandlesRequestQueries;
89

910
class ApplySortsToQuery implements HandlesRequestQueries
@@ -39,9 +40,69 @@ public function from(RequestQueryObject $request, Closure $next)
3940
protected function applySorts(Builder $query, array $sorts)
4041
{
4142
foreach ($sorts as $attribute => $direction) {
42-
$query->orderBy($attribute, $direction);
43+
$query->orderBy($this->getQualifiedAttribute($query, $attribute, $direction), $direction);
4344
}
4445

4546
return $query;
4647
}
48+
49+
/**
50+
* Get attribute adding a join when sorting by relationship or a column sort.
51+
*
52+
* @param \Illuminate\Database\Eloquent\Builder $query
53+
* @param string $attribute
54+
* @param string $direction
55+
* @return string|\Closure|\Illuminate\Database\Eloquent\Builder
56+
*/
57+
protected function getQualifiedAttribute(Builder $query, string $attribute, string $direction)
58+
{
59+
$queryModel = $query->getModel();
60+
61+
if (! str_contains($attribute, '.')) {
62+
return $queryModel->qualifyColumn($attribute);
63+
}
64+
65+
[$relationship, $column] = explode('.', $attribute);
66+
67+
if (! method_exists($queryModel, $relationship)) {
68+
return $queryModel->qualifyColumn($column);
69+
}
70+
71+
/** @var \Illuminate\Database\Eloquent\Relations\HasOneOrMany|\Illuminate\Database\Eloquent\Relations\BelongsTo|\Illuminate\Database\Eloquent\Relations\BelongsToMany $relationshipMethod */
72+
$relationshipMethod = call_user_func([$queryModel, $relationship]);
73+
$relationshipModel = $relationshipMethod->getRelated();
74+
75+
if (is_a($relationshipMethod, BelongsToMany::class)) {
76+
return $relationshipModel->newQuery()
77+
->select($column)
78+
->join($relationshipMethod->getTable(), $relationshipMethod->getRelatedPivotKeyName(), $relationshipModel->getQualifiedKeyName())
79+
->whereColumn($relationshipMethod->getQualifiedForeignPivotKeyName(), $queryModel->getQualifiedKeyName())
80+
->orderBy($column, $direction)
81+
->limit(1);
82+
}
83+
84+
$relationshipTable = $relationshipModel->getTable();
85+
$joinAsRelationshipTable = $relationshipTable;
86+
87+
if ($relationshipTable === $queryModel->getTable()) {
88+
$joinAsRelationshipTable = "{$relationship}_{$relationshipTable}";
89+
}
90+
91+
$joinName = $relationshipTable . ($joinAsRelationshipTable !== $relationshipTable ? " as {$joinAsRelationshipTable}" : '');
92+
93+
$query->select($queryModel->qualifyColumn('*'));
94+
95+
$query->when(
96+
! $query->hasJoin($joinName),
97+
fn (Builder $query) => $query->join(
98+
$joinName,
99+
"{$joinAsRelationshipTable}.{$relationshipMethod->getOwnerKeyName()}",
100+
'=',
101+
$relationshipMethod->getQualifiedForeignKeyName()
102+
)
103+
);
104+
105+
return "{$joinAsRelationshipTable}.{$column}";
106+
107+
}
47108
}

tests/Http/JsonApiResponseTest.php

+76
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,82 @@ public function testSortingFieldsAsAscendant()
247247
});
248248
}
249249

250+
public function testSortingBelongsToManyRelationshipFieldAsAscendant()
251+
{
252+
Route::get('/', function () {
253+
return JsonApiResponse::from(Post::class)
254+
->allowing([
255+
AllowedSort::ascendant('tags.name'),
256+
]);
257+
});
258+
259+
$response = $this->getJson('/?sort=tags.name');
260+
261+
$response->assertJsonApi(function (AssertableJsonApi $assert) {
262+
$assert->isCollection();
263+
264+
$assert->at(0)->hasAttribute('title', 'Hola mundo');
265+
$assert->at(1)->hasAttribute('title', 'My first test');
266+
});
267+
}
268+
269+
public function testSortingBelongsToManyRelationshipFieldAsDescendant()
270+
{
271+
Route::get('/', function () {
272+
return JsonApiResponse::from(Post::class)
273+
->allowing([
274+
AllowedSort::descendant('tags.name'),
275+
]);
276+
});
277+
278+
$response = $this->getJson('/?sort=-tags.name');
279+
280+
$response->assertJsonApi(function (AssertableJsonApi $assert) {
281+
$assert->isCollection();
282+
283+
$assert->at(0)->hasAttribute('title', 'Hello world');
284+
$assert->at(1)->hasAttribute('title', 'Y esto en español');
285+
});
286+
}
287+
288+
public function testSortingBelongsToRelationshipFieldAsAscendant()
289+
{
290+
Route::get('/', function () {
291+
return JsonApiResponse::from(Post::class)
292+
->allowing([
293+
AllowedSort::ascendant('author.name'),
294+
]);
295+
});
296+
297+
$response = $this->getJson('/?sort=author.name');
298+
299+
$response->assertJsonApi(function (AssertableJsonApi $assert) {
300+
$assert->isCollection();
301+
302+
$assert->at(0)->hasAttribute('title', 'My first test');
303+
$assert->at(1)->hasAttribute('title', 'Y esto en español');
304+
});
305+
}
306+
307+
public function testSortingBelongsToRelationshipFieldAsDescendant()
308+
{
309+
Route::get('/', function () {
310+
return JsonApiResponse::from(Post::class)
311+
->allowing([
312+
AllowedSort::descendant('author.name'),
313+
]);
314+
});
315+
316+
$response = $this->getJson('/?sort=-author.name');
317+
318+
$response->assertJsonApi(function (AssertableJsonApi $assert) {
319+
$assert->isCollection();
320+
321+
$assert->at(0)->hasAttribute('title', 'Hello world');
322+
$assert->at(1)->hasAttribute('title', 'Y esto en español');
323+
});
324+
}
325+
250326
public function testAddingFieldsAsModelAppendedAttributes()
251327
{
252328
Route::get('/', function () {

0 commit comments

Comments
 (0)