Skip to content

Commit 7601953

Browse files
authored
Filters (#176)
* filters * Apply fixes from StyleCI (#175) * fix * Apply fixes from StyleCI (#177)
1 parent 65da698 commit 7601953

16 files changed

+373
-1
lines changed

routes/api.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Binaryk\LaravelRestify\Http\Controllers\GlobalSearchController;
44
use Binaryk\LaravelRestify\Http\Controllers\RepositoryDestroyController;
5+
use Binaryk\LaravelRestify\Http\Controllers\RepositoryFilterController;
56
use Binaryk\LaravelRestify\Http\Controllers\RepositoryIndexController;
67
use Binaryk\LaravelRestify\Http\Controllers\RepositoryShowController;
78
use Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreController;
@@ -11,6 +12,9 @@
1112
// Global Search...
1213
Route::get('/search', '\\'.GlobalSearchController::class);
1314

15+
// Filters
16+
Route::get('/{repository}/filters', '\\'.RepositoryFilterController::class);
17+
1418
// API CRUD
1519
Route::get('/{repository}', '\\'.RepositoryIndexController::class);
1620
Route::post('/{repository}', '\\'.RepositoryStoreController::class);

src/BooleanFilter.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify;
4+
5+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
6+
7+
abstract class BooleanFilter extends Filter
8+
{
9+
public $type = 'boolean';
10+
11+
public function resolve(RestifyRequest $request, $filter)
12+
{
13+
$keyValues = collect($this->options($request))->mapWithKeys(function ($key) use ($filter) {
14+
return [$key => data_get($filter, $key)];
15+
})->toArray();
16+
17+
$this->value = $keyValues;
18+
}
19+
}

src/Filter.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify;
4+
5+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
6+
use Binaryk\LaravelRestify\Traits\Make;
7+
use Closure;
8+
use Illuminate\Http\Request;
9+
use JsonSerializable;
10+
11+
abstract class Filter implements JsonSerializable
12+
{
13+
use Make;
14+
15+
public $type;
16+
17+
public $value;
18+
19+
public $canSeeCallback;
20+
21+
public function __construct()
22+
{
23+
$this->booted();
24+
}
25+
26+
protected function booted()
27+
{
28+
//
29+
}
30+
31+
abstract public function filter(RestifyRequest $request, $query, $value);
32+
33+
public function canSee(Closure $callback)
34+
{
35+
$this->canSeeCallback = $callback;
36+
37+
return $this;
38+
}
39+
40+
public function authorizedToSee(RestifyRequest $request)
41+
{
42+
return $this->canSeeCallback ? call_user_func($this->canSeeCallback, $request) : true;
43+
}
44+
45+
public function key()
46+
{
47+
return static::class;
48+
}
49+
50+
protected function getType()
51+
{
52+
return $this->type;
53+
}
54+
55+
public function options(Request $request)
56+
{
57+
// noop
58+
}
59+
60+
public function invalidPayloadValue(Request $request, $value)
61+
{
62+
if (is_array($value)) {
63+
return count($value) < 1;
64+
} elseif (is_string($value)) {
65+
return trim($value) === '';
66+
}
67+
68+
return is_null($value);
69+
}
70+
71+
public function resolve(RestifyRequest $request, $filter)
72+
{
73+
$this->value = $filter;
74+
}
75+
76+
public function jsonSerialize()
77+
{
78+
return [
79+
'class' => static::class,
80+
'type' => $this->getType(),
81+
'options' => collect($this->options(app(Request::class)))->map(function ($value, $key) {
82+
return is_array($value) ? ($value + ['property' => $key]) : ['label' => $key, 'property' => $value];
83+
})->values()->all(),
84+
];
85+
}
86+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Http\Controllers;
4+
5+
use Binaryk\LaravelRestify\Http\Requests\RepositoryFiltersRequest;
6+
7+
class RepositoryFilterController extends RepositoryController
8+
{
9+
public function __invoke(RepositoryFiltersRequest $request)
10+
{
11+
$repository = $request->repository();
12+
13+
return $this->response()->data($repository->availableFilters($request));
14+
}
15+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Http\Requests;
4+
5+
class RepositoryFiltersRequest extends RestifyRequest
6+
{
7+
}

src/Repositories/Repository.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Binaryk\LaravelRestify\Exceptions\InstanceOfException;
88
use Binaryk\LaravelRestify\Fields\Field;
99
use Binaryk\LaravelRestify\Fields\FieldCollection;
10+
use Binaryk\LaravelRestify\Filter;
1011
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
1112
use Binaryk\LaravelRestify\Restify;
1213
use Binaryk\LaravelRestify\Services\Search\RepositorySearchService;
@@ -185,7 +186,7 @@ public static function query(): Builder
185186
}
186187

187188
/**
188-
* Resolvable attributes before storing/updating.
189+
* Resolvable attributes.
189190
*
190191
* @param RestifyRequest $request
191192
* @return array
@@ -195,6 +196,17 @@ public function fields(RestifyRequest $request)
195196
return [];
196197
}
197198

199+
/**
200+
* Resolvable filters for the repository.
201+
*
202+
* @param RestifyRequest $request
203+
* @return array
204+
*/
205+
public function filters(RestifyRequest $request)
206+
{
207+
return [];
208+
}
209+
198210
/**
199211
* @param RestifyRequest $request
200212
* @return FieldCollection
@@ -678,4 +690,10 @@ public static function uriTo(Model $model)
678690
{
679691
return Restify::path().'/'.static::uriKey().'/'.$model->getKey();
680692
}
693+
694+
public function availableFilters(RestifyRequest $request)
695+
{
696+
return collect($this->filter($this->filters($request)))->each(fn (Filter $filter) => $filter->authorizedToSee($request))
697+
->values();
698+
}
681699
}

src/SelectFilter.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify;
4+
5+
abstract class SelectFilter extends Filter
6+
{
7+
public $type = 'select';
8+
}

src/Services/Search/RepositorySearchService.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Binaryk\LaravelRestify\Services\Search;
44

55
use Binaryk\LaravelRestify\Contracts\RestifySearchable;
6+
use Binaryk\LaravelRestify\Filter;
67
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
78
use Binaryk\LaravelRestify\Repositories\Repository;
89
use Illuminate\Database\Eloquent\Builder;
@@ -17,6 +18,8 @@ public function search(RestifyRequest $request, Repository $repository)
1718

1819
$query = $this->prepareMatchFields($request, $this->prepareSearchFields($request, $repository::query(), $this->fixedInput), $this->fixedInput);
1920

21+
$query = $this->applyFilters($request, $repository, $query);
22+
2023
return tap($this->prepareRelations($request, $this->prepareOrders($request, $query), $this->fixedInput), $this->applyIndexQuery($request, $repository));
2124
}
2225

@@ -175,4 +178,35 @@ protected function applyIndexQuery(RestifyRequest $request, Repository $reposito
175178
{
176179
return fn ($query) => $repository::indexQuery($request, $query);
177180
}
181+
182+
protected function applyFilters(RestifyRequest $request, Repository $repository, $query)
183+
{
184+
if (! empty($request->filters)) {
185+
$filters = json_decode(base64_decode($request->filters), true);
186+
187+
collect($filters)
188+
->map(function ($filter) use ($request, $repository) {
189+
/** * @var Filter $matchingFilter */
190+
$matchingFilter = $repository->availableFilters($request)->first(function ($availableFilter) use ($filter) {
191+
return $filter['class'] === $availableFilter->key();
192+
});
193+
194+
if (is_null($matchingFilter)) {
195+
return false;
196+
}
197+
198+
if ($matchingFilter->invalidPayloadValue($request, $filter['value'])) {
199+
return false;
200+
}
201+
202+
$matchingFilter->resolve($request, $filter['value']);
203+
204+
return $matchingFilter;
205+
})
206+
->filter()
207+
->each(fn (Filter $filter) => $filter->filter($request, $query, $filter->value));
208+
}
209+
210+
return $query;
211+
}
178212
}

src/TimestampFilter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify;
4+
5+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
6+
use Carbon\Carbon;
7+
8+
abstract class TimestampFilter extends Filter
9+
{
10+
public $type = 'timestamp';
11+
12+
public function resolve(RestifyRequest $request, $value)
13+
{
14+
$this->value = Carbon::parse($value);
15+
}
16+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Tests\Controllers;
4+
5+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\ActiveBooleanFilter;
6+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\CreatedAfterDateFilter;
7+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post;
8+
use Binaryk\LaravelRestify\Tests\Fixtures\Post\SelectCategoryFilter;
9+
use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository;
10+
use Binaryk\LaravelRestify\Tests\IntegrationTest;
11+
12+
class RepositoryFilterControllerTest extends IntegrationTest
13+
{
14+
public function test_can_get_available_filters()
15+
{
16+
$response = $this
17+
->withoutExceptionHandling()
18+
->getJson('restify-api/posts/filters')
19+
->dump()
20+
->assertStatus(200);
21+
22+
$this->assertCount(3, $response->json('data'));
23+
}
24+
25+
public function test_the_boolean_filter_is_applied()
26+
{
27+
factory(Post::class)->create(['is_active' => false]);
28+
factory(Post::class)->create(['is_active' => true]);
29+
30+
$filters = base64_encode(json_encode([
31+
[
32+
'class' => ActiveBooleanFilter::class,
33+
'value' => [
34+
'is_active' => false,
35+
],
36+
],
37+
]));
38+
39+
$response = $this
40+
->withoutExceptionHandling()
41+
->getJson('restify-api/posts?filters='.$filters)
42+
->dump()
43+
->assertStatus(200);
44+
45+
$this->assertCount(1, $response->json('data'));
46+
}
47+
48+
public function test_the_select_filter_is_applied()
49+
{
50+
factory(Post::class)->create(['category' => 'movie']);
51+
factory(Post::class)->create(['category' => 'article']);
52+
53+
$filters = base64_encode(json_encode([
54+
[
55+
'class' => SelectCategoryFilter::class,
56+
'value' => 'article',
57+
],
58+
]));
59+
60+
$response = $this
61+
->withExceptionHandling()
62+
->getJson('restify-api/posts?filters='.$filters)
63+
->assertStatus(200);
64+
65+
$this->assertCount(1, $response->json('data'));
66+
}
67+
68+
public function test_the_timestamp_filter_is_applied()
69+
{
70+
factory(Post::class)->create(['created_at' => now()->addYear()]);
71+
factory(Post::class)->create(['created_at' => now()->subYear()]);
72+
73+
$filters = base64_encode(json_encode([
74+
[
75+
'class' => UserRepository::class,
76+
'value' => now()->addWeek()->timestamp,
77+
],
78+
[
79+
'class' => CreatedAfterDateFilter::class,
80+
'value' => now()->addWeek()->timestamp,
81+
],
82+
]));
83+
84+
$response = $this
85+
->withExceptionHandling()
86+
->getJson('restify-api/posts?filters='.$filters)
87+
->assertStatus(200);
88+
89+
$this->assertCount(1, $response->json('data'));
90+
}
91+
}

0 commit comments

Comments
 (0)