Skip to content

Commit 6cc031f

Browse files
committed
fix(core): filtering using deeply nested relations
1 parent bdc8d41 commit 6cc031f

File tree

6 files changed

+71
-5
lines changed

6 files changed

+71
-5
lines changed

src/Contracts/RelationsResolver.php

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ public function __construct(array $includableRelations, array $alwaysIncludedRel
1313

1414
public function requestedRelations(Request $request): array;
1515

16+
public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation;
17+
18+
public function rootRelationFromParamConstraint(string $paramConstraint): string;
19+
1620
public function relationFromParamConstraint(string $paramConstraint): string;
1721

1822
public function relationFieldFromParamConstraint(string $paramConstraint): string;

src/Drivers/Standard/QueryBuilder.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ public function applyFiltersToQuery($query, Request $request, array $filterDescr
134134
if ($relation === 'pivot') {
135135
$this->buildPivotFilterQueryWhereClause($relationField, $filterDescriptor, $query, $or);
136136
} else {
137-
$relationInstance = (new $this->resourceModelClass)->{$relation}();
138-
137+
$relationInstance = $this->relationsResolver->relationInstanceFromParamConstraint($this->resourceModelClass, $filterDescriptor['field']);
139138
$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);
140139

141140
$query->{$or ? 'orWhereHas' : 'whereHas'}(

src/Drivers/Standard/RelationsResolver.php

+34-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Database\Eloquent\Relations\BelongsTo;
77
use Illuminate\Database\Eloquent\Relations\HasOne;
8+
use Illuminate\Database\Eloquent\Relations\MorphMany;
89
use Illuminate\Database\Eloquent\Relations\MorphOne;
910
use Illuminate\Database\Eloquent\Relations\MorphTo;
11+
use Illuminate\Database\Eloquent\Relations\MorphToMany;
1012
use Illuminate\Database\Eloquent\Relations\Relation;
1113
use Illuminate\Support\Arr;
1214
use Illuminate\Support\Collection;
@@ -79,6 +81,37 @@ public function requestedRelations(Request $request): array
7981
return $validatedIncludes;
8082
}
8183

84+
public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation
85+
{
86+
$resourceModel = new $resourceModelClass();
87+
88+
do {
89+
$relationName = $this->rootRelationFromParamConstraint($paramConstraint);
90+
$paramConstraint = str_replace("{$relationName}.", '', $paramConstraint);
91+
92+
$relation = $resourceModel->{$relationName}();
93+
94+
if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
95+
break;
96+
}
97+
98+
$resourceModel = $relation->getModel();
99+
} while (str_contains($paramConstraint, '.'));
100+
101+
return $relation;
102+
}
103+
104+
/**
105+
* Resolves relation name from the given param constraint.
106+
*
107+
* @param string $paramConstraint
108+
* @return string
109+
*/
110+
public function rootRelationFromParamConstraint(string $paramConstraint): string
111+
{
112+
return Arr::first(explode('.', $paramConstraint));
113+
}
114+
82115
/**
83116
* Resolves relation name from the given param constraint.
84117
*
@@ -139,7 +172,7 @@ public function relationForeignKeyFromRelationInstance(Relation $relationInstanc
139172
*/
140173
public function getQualifiedRelationFieldName(Relation $relation, string $field): string
141174
{
142-
if ($relation instanceof MorphTo) {
175+
if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
143176
return $field;
144177
}
145178

tests/Feature/StandardIndexFilteringOperationsTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,35 @@ public function getting_a_list_of_resources_filtered_by_relation_field_resources
351351
);
352352
}
353353

354+
/** @test */
355+
public function getting_a_list_of_resources_filtered_by_deep_relation_field_resources(): void
356+
{
357+
$matchingPostUser = factory(User::class)->create(['name' => 'match']);
358+
$matchingPostUser->roles()->create(['name' => 'matching-role-name']);
359+
$matchingPost = factory(Post::class)->create(['user_id' => $matchingPostUser->id])->fresh();
360+
361+
$nonMatchingPostUser = factory(User::class)->create(['name' => 'not match']);
362+
$nonMatchingPostUser->roles()->create(['name' => 'non-matching-role-name']);
363+
factory(Post::class)->create(['user_id' => $nonMatchingPostUser->id])->fresh();
364+
365+
Gate::policy(Post::class, GreenPolicy::class);
366+
367+
$response = $this->post(
368+
'/api/posts/search',
369+
[
370+
'filters' => [
371+
['field' => 'user.name', 'operator' => '=', 'value' => 'match'],
372+
['field' => 'user.roles.name', 'operator' => '=', 'value' => 'matching-role-name'],
373+
],
374+
]
375+
);
376+
377+
$this->assertResourcesPaginated(
378+
$response,
379+
$this->makePaginator([$matchingPost], 'posts/search')
380+
);
381+
}
382+
354383
/** @test */
355384
public function getting_a_list_of_resources_filtered_by_not_whitelisted_field(): void
356385
{

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

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function filterableBy(): array
3737
'position',
3838
'publish_at',
3939
'user.name',
40+
'user.roles.name',
4041
'meta.name',
4142
'meta.title',
4243
'meta->nested_field',

tests/Fixtures/app/Traits/AppliesDefaultOrder.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ protected static function boot()
1111
parent::boot();
1212
// Order by name ASC
1313
static::addGlobalScope('order', function (Builder $builder) {
14-
$builder->orderBy('id', 'asc');
14+
$builder->orderBy($builder->getModel()->getTable().'.id', 'asc');
1515
});
1616
}
17-
}
17+
}

0 commit comments

Comments
 (0)