Skip to content

Commit e5760c9

Browse files
committed
feat: ability to filter and sort relation entities by pivot entry fields
1 parent 44626e4 commit e5760c9

File tree

4 files changed

+133
-6
lines changed

4 files changed

+133
-6
lines changed

src/Drivers/Standard/QueryBuilder.php

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,16 @@ public function applyFiltersToQuery($query, Request $request): void
111111
$relation = $this->relationsResolver->relationFromParamConstraint($filterDescriptor['field']);
112112
$relationField = $this->relationsResolver->relationFieldFromParamConstraint($filterDescriptor['field']);
113113

114-
$query->{$or ? 'orWhereHas' : 'whereHas'}(
115-
$relation,
116-
function ($relationQuery) use ($relationField, $filterDescriptor) {
117-
$this->buildFilterQueryWhereClause($relationField, $filterDescriptor, $relationQuery);
118-
}
119-
);
114+
if ($relation === 'pivot') {
115+
$this->buildPivotFilterQueryWhereClause($relationField, $filterDescriptor, $query, $or);
116+
} else {
117+
$query->{$or ? 'orWhereHas' : 'whereHas'}(
118+
$relation,
119+
function ($relationQuery) use ($relationField, $filterDescriptor) {
120+
$this->buildFilterQueryWhereClause($relationField, $filterDescriptor, $relationQuery);
121+
}
122+
);
123+
}
120124
} else {
121125
$this->buildFilterQueryWhereClause($this->getQualifiedFieldName($filterDescriptor['field']), $filterDescriptor, $query, $or);
122126
}
@@ -143,6 +147,26 @@ protected function buildFilterQueryWhereClause(string $field, array $filterDescr
143147
return $query;
144148
}
145149

150+
/**
151+
* Builds filter's pivot query where clause based on the given filterable.
152+
*
153+
* @param string $field
154+
* @param array $filterDescriptor
155+
* @param Builder|Relation $query
156+
* @param bool $or
157+
* @return Builder|Relation
158+
*/
159+
protected function buildPivotFilterQueryWhereClause(string $field, array $filterDescriptor, $query, bool $or = false)
160+
{
161+
if (!is_array($filterDescriptor['value'])) {
162+
$query->{$or ? 'orWherePivot' : 'wherePivot'}($field, $filterDescriptor['operator'], $filterDescriptor['value']);
163+
} else {
164+
$query->{$or ? 'orWherePivotIn' : 'wherePivotIn'}($field, $filterDescriptor['value'], 'and', $filterDescriptor['operator'] === 'not in');
165+
}
166+
167+
return $query;
168+
}
169+
146170
/**
147171
* Builds a complete field name with table.
148172
*
@@ -218,6 +242,11 @@ public function applySortingToQuery($query, Request $request): void
218242
$relation = $this->relationsResolver->relationFromParamConstraint($sortableField);
219243
$relationField = $this->relationsResolver->relationFieldFromParamConstraint($sortableField);
220244

245+
if ($relation === 'pivot') {
246+
$query->orderByPivot($relationField, $direction);
247+
continue;
248+
}
249+
221250
/**
222251
* @var Relation $relationInstance
223252
*/
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Orion\Tests\Feature\Relations\BelongsToMany;
6+
7+
use Illuminate\Support\Facades\Gate;
8+
use Orion\Tests\Feature\TestCase;
9+
use Orion\Tests\Fixtures\App\Models\Role;
10+
use Orion\Tests\Fixtures\App\Models\User;
11+
use Orion\Tests\Fixtures\App\Policies\GreenPolicy;
12+
13+
class BelongsToManyRelationStandardIndexFilteringOperationsTest extends TestCase
14+
{
15+
/** @test */
16+
public function getting_a_list_of_relation_resources_filtered_by_pivot_field(): void
17+
{
18+
/** @var User $user */
19+
$user = factory(User::class)->create();
20+
21+
$roleWithCustomName = factory(Role::class)->create();
22+
$roleWithoutCustomName = factory(Role::class)->create();
23+
24+
$user->roles()->attach($roleWithCustomName, ['custom_name' => 'test-name']);
25+
$user->roles()->attach($roleWithoutCustomName);
26+
27+
Gate::policy(User::class, GreenPolicy::class);
28+
Gate::policy(Role::class, GreenPolicy::class);
29+
30+
$response = $this->post("/api/users/{$user->id}/roles/search", [
31+
'filters' => [
32+
['field' => 'pivot.custom_name', 'operator' => '=', 'value' => 'test-name']
33+
]
34+
]);
35+
36+
$this->assertResourcesPaginated(
37+
$response,
38+
$this->makePaginator([$user->roles()->first()->toArray()], "users/{$user->id}/roles/search")
39+
);
40+
}
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Orion\Tests\Feature\Relations\BelongsToMany;
6+
7+
use Illuminate\Support\Facades\Gate;
8+
use Orion\Tests\Feature\TestCase;
9+
use Orion\Tests\Fixtures\App\Models\Role;
10+
use Orion\Tests\Fixtures\App\Models\User;
11+
use Orion\Tests\Fixtures\App\Policies\GreenPolicy;
12+
13+
class BelongsToManyRelationStandardIndexSortingOperationsTest extends TestCase
14+
{
15+
/** @test */
16+
public function getting_a_list_of_relation_resources_desc_sorted_by_pivot_field(): void
17+
{
18+
if ((float) app()->version() <= 8.0) {
19+
$this->markTestSkipped('Unsupported framework version');
20+
}
21+
22+
/** @var User $user */
23+
$user = factory(User::class)->create();
24+
25+
$roleA = factory(Role::class)->create();
26+
$roleB = factory(Role::class)->create();
27+
$roleC = factory(Role::class)->create();
28+
29+
$user->roles()->attach($roleA, ['custom_name' => 'a']);
30+
$user->roles()->attach($roleB, ['custom_name' => 'b']);
31+
$user->roles()->attach($roleC, ['custom_name' => 'c']);
32+
33+
Gate::policy(User::class, GreenPolicy::class);
34+
Gate::policy(Role::class, GreenPolicy::class);
35+
36+
$response = $this->withoutExceptionHandling()->post("/api/users/{$user->id}/roles/search", [
37+
'sort' => [
38+
['field' => 'pivot.custom_name', 'direction' => 'desc']
39+
]
40+
]);
41+
42+
$this->assertResourcesPaginated(
43+
$response,
44+
$this->makePaginator($user->roles()->get()->reverse()->toArray(), "users/{$user->id}/roles/search")
45+
);
46+
}
47+
}

tests/Fixtures/app/Http/Controllers/UserRolesController.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ class UserRolesController extends RelationController
1717

1818
protected $pivotFillable = ['custom_name', 'references'];
1919

20+
public function filterableBy(): array
21+
{
22+
return ['pivot.custom_name'];
23+
}
24+
25+
public function sortableBy(): array
26+
{
27+
return ['pivot.custom_name'];
28+
}
29+
2030
public function includes(): array
2131
{
2232
return ['users'];

0 commit comments

Comments
 (0)