Skip to content

Commit d5934f9

Browse files
committed
- Added allowing dynamic fields
1 parent fad46f9 commit d5934f9

File tree

8 files changed

+194
-9
lines changed

8 files changed

+194
-9
lines changed

docs/advanced-usage/supporting-complex-selects.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ For these scenarios, you can override the `modifyQuery` method to call these sco
1414
protected function modifyQuery(\Spatie\QueryBuilder\QueryBuilder $query): \Spatie\QueryBuilder\QueryBuilder
1515
{
1616
if (request()->has('lat') && request()->has('lng')) {
17-
if ($this->sorts()->has('distance') || $this->fields()->has('distance')) {
17+
$fields = $this->fields()->isNotEmpty() ? $this->fields()->get('_') : [];
18+
19+
if ($this->sorts()->contains('distance') || $this->sorts()->contains('-distance') || (is_array($fields) && in_array('distance', $fields))) {
1820
$query->withDistance('islands.coordinates', new Point(request()->input('lat'), request()->input('lng'), Srid::WGS84));
1921
}
2022
}
@@ -39,7 +41,7 @@ protected function modifyQuery(\Spatie\QueryBuilder\QueryBuilder $query): \Spati
3941
{
4042
$fields = $this->fields()->isNotEmpty() ? $this->fields()->get('_') : [];
4143

42-
if ((is_array($fields) && in_array('formatted_name', $fields)) || $this->appends()->has('formatted_name')) {
44+
if ((is_array($fields) && in_array('formatted_name', $fields)) || $this->appends()->contains('formatted_name')) {
4345
$query->with('atoll');
4446
}
4547

@@ -49,20 +51,24 @@ protected function modifyQuery(\Spatie\QueryBuilder\QueryBuilder $query): \Spati
4951

5052
The `$this->fields()->get('_')` will return the fields requested for the main model.
5153

52-
For complex selects, you might need to dynamically set the allowed fields and sorts as well.
54+
For complex selects, use the `getAllowedDynamicFields` method to allow these fields to be included.
5355

5456
```php
55-
public function getAllowedFields(): array
57+
public function getAllowedDynamicFields(): array
5658
{
57-
$fields = \Schema::getColumnListing('islands');
59+
$fields = [];
5860

5961
if (request()->has('lat') && request()->has('lng')) {
6062
$fields[] = 'distance';
6163
}
6264

63-
return array_diff($fields, (new Island)->getHidden());
65+
return $fields;
6466
}
67+
```
6568

69+
You may need to also dynamically set the allowed sorts as well.
70+
71+
```php
6672
public function getAllowedSorts(): array
6773
{
6874
$sorts = [

src/Concerns/IsApiController.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ protected function indexEndpoint(Request $request)
101101
->allowedFilters($this->getAllowedFilters())
102102
->allowedAppends($this->getAllowedAppends())
103103
->fieldsToAlwaysInclude($this->getFieldsToAlwaysInclude())
104-
->allowedFields($this->getAllowedFields())
104+
->allowedDynamicFields($this->getIndexAllowedDynamicFields())
105+
->allowedFields($this->getIndexAllowedFields())
105106
->allowedIncludes($this->getAllowedIncludes());
106107

107108
$query = $this->modifyQuery($query);
@@ -130,6 +131,7 @@ protected function showEndpoint($model_id, Request $request)
130131
$model = $this->getQueryBuilder()
131132
->allowedAppends($this->getAllShowAllowedAppends())
132133
->fieldsToAlwaysInclude($this->getFieldsToAlwaysInclude())
134+
->allowedDynamicFields($this->getAllowedDynamicFields())
133135
->allowedFields($this->getAllowedFields())
134136
->allowedIncludes($this->getAllowedIncludes());
135137

@@ -347,4 +349,20 @@ public function getShowAllowedAppends(): array
347349
{
348350
return [];
349351
}
352+
353+
/**
354+
* Get the dynamic fields
355+
*/
356+
public function getAllowedDynamicFields(): array
357+
{
358+
return [];
359+
}
360+
361+
/**
362+
* Get the dynamic fields
363+
*/
364+
public function getIndexAllowedDynamicFields(): array
365+
{
366+
return $this->getAllowedDynamicFields();
367+
}
350368
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Javaabu\QueryBuilder\Exceptions;
4+
5+
use BadMethodCallException;
6+
7+
class AllowedDynamicFieldsMustBeCalledBeforeAllowedFields extends BadMethodCallException
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct("The QueryBuilder's `allowedDynamicFields` method must be called before the `allowedFields` method.");
12+
}
13+
}

src/Http/Controllers/ApiBaseController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public abstract function getBaseQuery(): Builder;
3636
*/
3737
public abstract function getAllowedFields(): array;
3838

39+
/**
40+
* Get the dynamic fields
41+
*/
42+
public abstract function getAllowedDynamicFields(): array;
43+
3944
/**
4045
* Get the includes
4146
*/

src/QueryBuilder.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Support\Str;
1212
use Javaabu\QueryBuilder\Concerns\AppendsAttributesToResults;
1313
use Javaabu\QueryBuilder\Exceptions\AllowedAppendsMustBeCalledBeforeAllowedFields;
14+
use Javaabu\QueryBuilder\Exceptions\AllowedDynamicFieldsMustBeCalledBeforeAllowedFields;
1415
use Javaabu\QueryBuilder\Exceptions\FieldsToAlwaysIncludeMustBeCalledBeforeAllowedFields;
1516
use Spatie\QueryBuilder\AllowedInclude;
1617
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
@@ -47,6 +48,22 @@ class QueryBuilder extends \Spatie\QueryBuilder\QueryBuilder
4748
*/
4849
protected $allAppends = null;
4950

51+
protected ?Collection $allowedDynamicFields = null;
52+
53+
54+
public function allowedDynamicFields($fields): static
55+
{
56+
if ($this->allowedFields instanceof Collection) {
57+
throw new AllowedDynamicFieldsMustBeCalledBeforeAllowedFields();
58+
}
59+
60+
$fields = is_array($fields) ? $fields : func_get_args();
61+
62+
$this->allowedDynamicFields = collect($fields);
63+
64+
return $this;
65+
}
66+
5067
/**
5168
* Set to ignore invalid filters
5269
*/
@@ -155,7 +172,12 @@ protected function ensureAllFieldsExist(): void
155172

156173
// get rid of any appended fields present
157174
$requestedFields = $requestedFields->diff(
158-
$this->prependFieldsWithTableName(($this->allowedAppends ? $this->allowedAppends->all() : []), $modelTable)
175+
$this->prependFieldsWithTableName(($this->allowedAppends ? $this->allowedAppends->all() : []), $modelTable),
176+
);
177+
178+
// get rid of any dynamic fields present
179+
$requestedFields = $requestedFields->diff(
180+
$this->prependFieldsWithTableName(($this->allowedDynamicFields ? $this->allowedDynamicFields->all() : []), $modelTable)
159181
);
160182

161183
$unknownFields = $requestedFields->diff($this->allowedFields);
@@ -294,6 +316,12 @@ protected function addRequestedModelFieldsToQuery(): void
294316
$this->prependFieldsWithTableName(($this->allowedAppends ? $this->allowedAppends->all() : []), $modelTableName)
295317
);
296318

319+
// get rid of any dynamic fields present
320+
$prependedFields = array_diff(
321+
$prependedFields,
322+
$this->prependFieldsWithTableName(($this->allowedDynamicFields ? $this->allowedDynamicFields->all() : []), $modelTableName)
323+
);
324+
297325
$prependedFields = array_unique($prependedFields);
298326

299327
$this->select($prependedFields);

tests/Controllers/ProductsController.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ public function getBaseQuery(): Builder
1919
return Product::query();
2020
}
2121

22+
protected function modifyQuery(\Spatie\QueryBuilder\QueryBuilder $query): \Spatie\QueryBuilder\QueryBuilder
23+
{
24+
if (request()->has('rating')) {
25+
$fields = $this->fields()->isNotEmpty() ? $this->fields()->get('_') : [];
26+
27+
if ($this->sorts()->contains('rating') || $this->sorts()->contains('-rating') || (is_array($fields) && in_array('rating', $fields))) {
28+
$query->withRating(request()->input('rating'));
29+
}
30+
}
31+
32+
return $query;
33+
}
2234
/**
2335
* Get the allowed fields
2436
*
@@ -29,6 +41,17 @@ public function getAllowedFields(): array
2941
return array_diff(\Schema::getColumnListing('products'), (new Product)->getHidden());
3042
}
3143

44+
public function getAllowedDynamicFields(): array
45+
{
46+
if (request()->has('rating')) {
47+
return [
48+
'rating'
49+
];
50+
}
51+
52+
return [];
53+
}
54+
3255
/**
3356
* Get the allowed includes
3457
*
@@ -62,13 +85,19 @@ public function getAllowedAppends(): array
6285
*/
6386
public function getAllowedSorts(): array
6487
{
65-
return [
88+
$sorts = [
6689
'id',
6790
'name',
6891
'slug',
6992
'created_at',
7093
'updated_at',
7194
];
95+
96+
if (request()->has('rating')) {
97+
$sorts[] = 'rating';
98+
}
99+
100+
return $sorts;
72101
}
73102

74103
/**

tests/Feature/ApiControllerTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Javaabu\QueryBuilder\Tests\Models\Product;
1010
use Javaabu\QueryBuilder\Tests\TestCase;
1111
use PHPUnit\Framework\Attributes\Test;
12+
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
1213

1314
class ApiControllerTest extends TestCase
1415
{
@@ -226,4 +227,78 @@ public function it_can_load_api_model_appends_from_fields_even_if_appends_is_bla
226227
'slug' => 'orange',
227228
]);
228229
}
230+
231+
#[Test]
232+
public function it_can_include_dynamic_fields(): void
233+
{
234+
$this->withoutExceptionHandling();
235+
236+
$product_1 = Product::factory()->create([
237+
'name' => 'Apple'
238+
]);
239+
240+
$product_2 = Product::factory()->create([
241+
'name' => 'Orange'
242+
]);
243+
244+
$this->getJson('/products?fields=id,rating&rating=10')
245+
->assertSuccessful()
246+
->assertJsonFragment([
247+
'id' => $product_1->id,
248+
'rating' => $product_1->id + 10,
249+
])
250+
->assertJsonFragment([
251+
'id' => $product_2->id,
252+
'rating' => $product_2->id + 10,
253+
]);
254+
}
255+
256+
#[Test]
257+
public function it_can_sort_by_dynamic_fields(): void
258+
{
259+
$this->withoutExceptionHandling();
260+
261+
$product_1 = Product::factory()->create([
262+
'name' => 'Apple'
263+
]);
264+
265+
$product_2 = Product::factory()->create([
266+
'name' => 'Orange'
267+
]);
268+
269+
$this->getJson('/products?fields=id,name&rating=10&sort=rating')
270+
->assertSuccessful()
271+
->assertJsonFragment([
272+
'id' => $product_1->id,
273+
])
274+
->assertJsonFragment([
275+
'id' => $product_2->id,
276+
]);
277+
}
278+
279+
#[Test]
280+
public function it_cannot_include_dynamic_fields_if_required_params_are_missing(): void
281+
{
282+
$this->withoutExceptionHandling();
283+
284+
$product_1 = Product::factory()->create([
285+
'name' => 'Apple'
286+
]);
287+
288+
$product_2 = Product::factory()->create([
289+
'name' => 'Orange'
290+
]);
291+
292+
$this->expectException(InvalidFieldQuery::class);
293+
294+
$this->getJson('/products?fields=id,rating')
295+
->assertJsonMissing([
296+
'id' => $product_1->id,
297+
'rating' => $product_1->id + 10,
298+
])
299+
->assertJsonMissing([
300+
'id' => $product_2->id,
301+
'rating' => $product_2->id + 10,
302+
]);
303+
}
229304
}

tests/Models/Product.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,15 @@ public function getFormattedNameAttribute(): string
3636
{
3737
return 'Formatted ' . $this->name;
3838
}
39+
40+
public function scopeWithRating($query, $rating)
41+
{
42+
if (! $query->getQuery()->columns) {
43+
$query->select('*');
44+
}
45+
46+
$rating = (int) $rating;
47+
48+
return $query->selectRaw('(' .$rating . ' + id) as rating');
49+
}
3950
}

0 commit comments

Comments
 (0)