From 76e68a3a91ceeef4e62e0ecb91934e8d11d9b488 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Tue, 8 Dec 2020 12:26:43 +0200 Subject: [PATCH 1/6] Display filters and sortables. --- UPGRADING.md | 2 +- config/config.php | 2 +- docs/docs/4.0/auth/auth.md | 12 ++++- docs/docs/4.0/filtering/filtering.md | 4 +- docs/docs/4.0/quickstart.md | 4 +- .../repository-pattern/repository-pattern.md | 14 +++--- src/Filter.php | 24 ++++++++++ src/Filters/MatchFilter.php | 44 +++++++++++++++++++ src/Filters/SortableFilter.php | 42 ++++++++++++++++++ .../RepositoryFilterController.php | 18 +++++++- src/Repositories/Repository.php | 8 +++- .../RepositoryFilterControllerTest.php | 42 +++++++++++++++--- .../RepositoryIndexControllerTest.php | 24 ++++++++++ tests/Fixtures/Company/CompanyRepository.php | 4 ++ 14 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 src/Filters/MatchFilter.php create mode 100644 src/Filters/SortableFilter.php diff --git a/UPGRADING.md b/UPGRADING.md index 40f43ab4c..7a2189f84 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,7 +6,7 @@ Because there are many breaking changes an upgrade is not that easy. There are m - Dropped support for laravel passport - Now you have to explicitly define the `allowRestify` method in the model policy, by default Restify don't allow you to use repositories. -- `viewAny` policy is not used anymore, you can delete it. +- `viewAny` policy isn't used anymore, you can delete it. - The default exception handler is the Laravel one, see `restify.php -> handler` - `fillCallback` signature has changed - By default it will do not allow you to attach `belongsToMany` and `morphToMany` relationships. You will have to add `BelongsToMany` or `MorphToMany` field into your repository diff --git a/config/config.php b/config/config.php index 99d12688a..8833b174d 100644 --- a/config/config.php +++ b/config/config.php @@ -45,7 +45,7 @@ 'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}', - 'user_verify_url' => env('FRONTEND_APP_URL').'/verify?id={id}&hash={emailHash}', + 'user_verify_url' => env('FRONTEND_APP_URL').'/verify/{id}/{emailHash}', ], /* diff --git a/docs/docs/4.0/auth/auth.md b/docs/docs/4.0/auth/auth.md index e202eb693..22810493e 100644 --- a/docs/docs/4.0/auth/auth.md +++ b/docs/docs/4.0/auth/auth.md @@ -9,10 +9,20 @@ You'll finally enjoy the auth setup (`register`, `login`, `forgot` and `reset pa - Migrate the `personal_access_tokens` table, provided by sanctum. +- Install laravel sanctum. See the docs [here](https://laravel.com/docs/sanctum#installation). You don't need to add `\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,` in your `'api'` middleware group. So you only need to run these 3 commands: + +```shell script +composer require laravel/sanctum +php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" +php artisan migrate +``` + - Make sure your authenticatable entity (usually `App\Models\User`) implements: `Illuminate\Contracts\Auth\Authenticatable` (or simply extends the `Illuminate\Foundation\Auth\User` class as it does into a fresh laravel app.) - Make sure the `App\Models\User` model implements the `Binaryk\LaravelRestify\Contracts\Sanctumable` contract. +- Add `\Laravel\Sanctum\HasApiTokens` trait to your `User` model. + ## Define routes Restify provides you a simple way to add all of your auth routes ready. Simply add in your `routes/api.php`: @@ -137,7 +147,7 @@ This method could look like this: public function sendEmailVerificationNotification() { - $this->notify(new VerifyEmail); + $this->notify(new \Binaryk\LaravelRestify\Notifications\VerifyEmail); } ``` diff --git a/docs/docs/4.0/filtering/filtering.md b/docs/docs/4.0/filtering/filtering.md index d14a7ea2b..50e13bd11 100644 --- a/docs/docs/4.0/filtering/filtering.md +++ b/docs/docs/4.0/filtering/filtering.md @@ -220,7 +220,7 @@ GET: /api/restify/posts?sort=+id ## Eager loading - aka withs When get a repository index or details about a single entity, often we have to get the related entities (we have access to). -This eager loading is configurable by Restify as follow: +This eager loading is configurable by Restify as following: ```php public static $related = ['posts']; @@ -229,7 +229,7 @@ public static $related = ['posts']; This means that we could use `posts` query for eager loading posts: ```http request -GET: /api/restify/users?with=posts +GET: /api/restify/users?related=posts ``` ## Pagination diff --git a/docs/docs/4.0/quickstart.md b/docs/docs/4.0/quickstart.md index 9ac941b1f..270d03b29 100644 --- a/docs/docs/4.0/quickstart.md +++ b/docs/docs/4.0/quickstart.md @@ -72,7 +72,9 @@ One important configuration is the restify default middlewares: ### Sanctum authorization -Usually you want to authorize your api (allow access only to authenticated users). For this purpose you can simply add another middleware. For the `sanctum`, Restify provides `Binaryk\LaravelRestify\Http\Middleware\RestifySanctumAuthenticate` middleware. +Usually you want to authorize your api (allow access only to authenticated users). For this purpose you can simply add another middleware. For the `sanctum`, Restify provides `Binaryk\LaravelRestify\Http\Middleware\RestifySanctumAuthenticate::class` middleware. Make sure you put this right after `api` middleware. + +You may notice that Restify also use the `EnsureJsonApiHeaderMiddleware` middleware, which enforce you to use the `application/vnd.api+json` Accept header for your API requests. So make sure, even when using Postman (or something else) for making requests, that this `Accept header` is applied. ### Exception Handling diff --git a/docs/docs/4.0/repository-pattern/repository-pattern.md b/docs/docs/4.0/repository-pattern/repository-pattern.md index dc5a2cfb3..69af44b0e 100644 --- a/docs/docs/4.0/repository-pattern/repository-pattern.md +++ b/docs/docs/4.0/repository-pattern/repository-pattern.md @@ -56,10 +56,10 @@ Having this in place you're basically ready for the CRUD actions over posts. You | GET | `/api/restify/posts/{post}` | show | | POST | `/api/restify/posts` | store | | POST | `/api/restify/posts/bulk` | store multiple | -| POST | `/api/restify/posts/bulk/update` | store multiple | -| PATCH | `/api/restify/posts/{post}` | update | -| PUT | `/api/restify/posts/{post}` | update | -| POST | `/api/restify/posts/{post}` | update | +| POST | `/api/restify/posts/bulk/update` | update multiple | +| PATCH | `/api/restify/posts/{post}` | partial update | +| PUT | `/api/restify/posts/{post}` | full update | +| POST | `/api/restify/posts/{post}` | partial of full update including attachments | | DELETE | `/api/restify/posts/{post}` | destroy | :::tip Update with files As you can see we provide 3 Verbs for the model update (PUT, PATCH, POST), the reason of that @@ -195,14 +195,14 @@ You can customize the `meta` by creating your own `resolveShowMeta` method: } ``` -:::tip Resource property In the previous example we have used the `$this->resource` call, well, keep in mind, that you +:::tip $resource property +In the previous example we have used the `$this->resource` call, well, keep in mind, that you always have access to the current resource in your not static methods of the repository, were the resource is the actual current model. In the case above, the `$this->resource` represents the `Post` model with the `id=1`, because we're looking for the route: `/api/restify/posts/1`. A similar way to get the model is the `$this->model()` method. ::: -Well, a lot of methods to modify the serialization partials, however, you are free to customize the entire response at -once by defining: +As we saw before, there are many ways to partially modify the response (ie separate way to modify meta), however, you are free to customize the entire response at once by defining: ```php // PostRepository.php diff --git a/src/Filter.php b/src/Filter.php index d498e1898..c18b7c08f 100644 --- a/src/Filter.php +++ b/src/Filter.php @@ -6,6 +6,7 @@ use Binaryk\LaravelRestify\Traits\Make; use Closure; use Illuminate\Http\Request; +use Illuminate\Support\Str; use JsonSerializable; abstract class Filter implements JsonSerializable @@ -18,6 +19,8 @@ abstract class Filter implements JsonSerializable public $canSeeCallback; + public static $uriKey; + public function __construct() { $this->booted(); @@ -73,10 +76,31 @@ public function resolve(RestifyRequest $request, $filter) $this->value = $filter; } + /** + * Get the URI key for the filter. + * + * @return string + */ + public static function uriKey() + { + if (property_exists(static::class, 'uriKey') && is_string(static::$uriKey)) { + return static::$uriKey; + } + + $kebabWithoutRepository = Str::kebab(Str::replaceLast('Filter', '', class_basename(get_called_class()))); + + /** + * e.g. UserRepository => users + * e.g. LaravelEntityRepository => laravel-entities. + */ + return Str::plural($kebabWithoutRepository); + } + public function jsonSerialize() { return [ 'class' => static::class, + 'key' => static::uriKey(), 'type' => $this->getType(), 'options' => collect($this->options(app(Request::class)))->map(function ($value, $key) { return is_array($value) ? ($value + ['property' => $key]) : ['label' => $key, 'property' => $value]; diff --git a/src/Filters/MatchFilter.php b/src/Filters/MatchFilter.php new file mode 100644 index 000000000..8c1ca992a --- /dev/null +++ b/src/Filters/MatchFilter.php @@ -0,0 +1,44 @@ +where($this->column, $value); + } + + public static function makeFromSimple($column, $type): self + { + return tap(new static, function (MatchFilter $filter) use ($column, $type) { + $filter->type = $type; + $filter->column = $column; + }); + } + + public static function makeForRepository(Repository $repository): Collection + { + return collect($repository::getMatchByFields())->map(function ($type, $column) { + return static::makeFromSimple($column, $type); + })->values(); + } + + public function jsonSerialize() + { + return [ + 'class' => static::class, + 'type' => $this->getType(), + 'key' => static::uriKey(), + 'column' => $this->column, + ]; + } +} diff --git a/src/Filters/SortableFilter.php b/src/Filters/SortableFilter.php new file mode 100644 index 000000000..371fa7c74 --- /dev/null +++ b/src/Filters/SortableFilter.php @@ -0,0 +1,42 @@ +orderBy($this->column, $direction); + } + + public static function makeFromSimple($column): self + { + return tap(new static, function (SortableFilter $filter) use ($column) { + $filter->column = $column; + }); + } + + public static function makeForRepository(Repository $repository): Collection + { + return collect($repository::getOrderByFields())->map(function ($column) { + return static::makeFromSimple($column); + }); + } + + public function jsonSerialize() + { + return [ + 'class' => static::class, + 'key' => static::uriKey(), + 'column' => $this->column, + ]; + } +} diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index cdd416e8f..72ac43932 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -2,7 +2,11 @@ namespace Binaryk\LaravelRestify\Http\Controllers; +use Binaryk\LaravelRestify\Filters\MatchFilter; +use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RepositoryFiltersRequest; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; class RepositoryFilterController extends RepositoryController { @@ -10,6 +14,18 @@ public function __invoke(RepositoryFiltersRequest $request) { $repository = $request->repository(); - return $this->response()->data($repository->availableFilters($request)); + return $this->response()->data( + $repository->availableFilters($request) + ->when(Str::contains($request->input('include'), 'matches'), function (Collection $collection) use ($repository) { + return $collection->merge( + MatchFilter::makeForRepository($repository) + ); + }) + ->when(Str::contains($request->input('include'), 'sortable'), function (Collection $collection) use ($repository) { + return $collection->merge( + SortableFilter::makeForRepository($repository) + ); + }) + ); } } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index dff1b3cd3..2459f095b 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -29,6 +29,7 @@ use Illuminate\Http\Resources\DelegatesToResource; use Illuminate\Pagination\AbstractPaginator; use Illuminate\Routing\Router; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -177,7 +178,7 @@ public function model() } /** - * Get the URI key for the resource. + * Get the URI key for the repository. * * @return string */ @@ -522,6 +523,11 @@ public function resolveRelationships($request): array collect(str_getcsv($request->input('related'))) ->filter(fn ($relation) => in_array($relation, static::getRelated())) ->each(function ($relation) use ($request, $withs) { + if (Str::contains($relation, '.')) { + $this->resource->loadMissing($relation); + return $withs->put($key = Str::before($relation, '.'), Arr::get($this->resource->relationsToArray(), $key)); + } + $paginator = $this->resource->relationLoaded($relation) ? $this->resource->{$relation} : $this->resource->{$relation}(); diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index fda845f00..ec082bd19 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -6,6 +6,7 @@ use Binaryk\LaravelRestify\Tests\Fixtures\Post\CreatedAfterDateFilter; use Binaryk\LaravelRestify\Tests\Fixtures\Post\InactiveFilter; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; +use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\SelectCategoryFilter; use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; @@ -14,11 +15,38 @@ class RepositoryFilterControllerTest extends IntegrationTest { public function test_can_get_available_filters() { - $response = $this - ->withoutExceptionHandling() - ->getJson('posts/filters'); + $this->getJson('posts/filters')->assertJsonCount(4, 'data'); + } - $this->assertCount(4, $response->json('data')); + public function test_available_filters_contains_matches() + { + PostRepository::$match = [ + 'title' => 'text', + ]; + + PostRepository::$sort = [ + 'title', + ]; + + $response = $this->getJson('posts/filters?include=matches,sortable') + // 5 custom filters + // 1 match filter + // 1 sort + ->assertJsonCount(6, 'data'); + + + $this->assertSame( + $response->json('data.4.key'), 'matches' + ); + $this->assertSame( + $response->json('data.4.column'), 'title' + ); + $this->assertSame( + $response->json('data.5.key'), 'sortables' + ); + $this->assertSame( + $response->json('data.5.column'), 'title' + ); } public function test_value_filter_doesnt_require_value() @@ -34,7 +62,7 @@ public function test_value_filter_doesnt_require_value() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -56,7 +84,7 @@ public function test_the_boolean_filter_is_applied() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -76,7 +104,7 @@ public function test_the_select_filter_is_applied() $response = $this ->withExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index a93e9edef..ccc1144d7 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -2,8 +2,11 @@ namespace Binaryk\LaravelRestify\Tests\Controllers; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; +use Binaryk\LaravelRestify\Tests\Fixtures\User\User; use Binaryk\LaravelRestify\Tests\IntegrationTest; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -91,6 +94,27 @@ public function test_repository_with_relations() $this->assertArrayNotHasKey('user', $response->json('data.0.attributes')); } + public function test_repository_with_deep_relations() + { + CompanyRepository::$related = ['users.posts']; + + tap(factory(Company::class)->create(), function (Company $company) { + tap($company->users()->create( + array_merge(factory(User::class)->make()->toArray(), [ + 'password' => 'secret', + ]) + ), function (User $user) { + factory(Post::class)->create(['user_id' => $user->id]); + }); + }); + + $response = $this->getJson(CompanyRepository::uriKey() . '?related=users.posts') + ->assertOk(); + + $this->assertCount(1, $response->json('data.0.relationships.users')); + $this->assertCount(1, $response->json('data.0.relationships.users.0.posts')); + } + public function test_paginated_repository_with_relations() { PostRepository::$related = ['user']; diff --git a/tests/Fixtures/Company/CompanyRepository.php b/tests/Fixtures/Company/CompanyRepository.php index f7cf375d9..dc6d7a8d3 100644 --- a/tests/Fixtures/Company/CompanyRepository.php +++ b/tests/Fixtures/Company/CompanyRepository.php @@ -12,6 +12,10 @@ class CompanyRepository extends Repository { public static $model = Company::class; + public static $related = [ + 'users', + ]; + public function fields(RestifyRequest $request) { return [ From 69c99a6354cedbbcfe759c3e541a0f59dbda1a79 Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Tue, 8 Dec 2020 12:27:16 +0200 Subject: [PATCH 2/6] Apply fixes from StyleCI (#296) --- src/Http/Controllers/RepositoryFilterController.php | 4 ++-- src/Repositories/Repository.php | 1 + tests/Controllers/RepositoryFilterControllerTest.php | 7 +++---- tests/Controllers/RepositoryIndexControllerTest.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index 72ac43932..08b8eac18 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -16,12 +16,12 @@ public function __invoke(RepositoryFiltersRequest $request) return $this->response()->data( $repository->availableFilters($request) - ->when(Str::contains($request->input('include'), 'matches'), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), 'matches'), function (Collection $collection) use ($repository) { return $collection->merge( MatchFilter::makeForRepository($repository) ); }) - ->when(Str::contains($request->input('include'), 'sortable'), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), 'sortable'), function (Collection $collection) use ($repository) { return $collection->merge( SortableFilter::makeForRepository($repository) ); diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 2459f095b..d1114971a 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -525,6 +525,7 @@ public function resolveRelationships($request): array ->each(function ($relation) use ($request, $withs) { if (Str::contains($relation, '.')) { $this->resource->loadMissing($relation); + return $withs->put($key = Str::before($relation, '.'), Arr::get($this->resource->relationsToArray(), $key)); } diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index ec082bd19..a866c3715 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -34,7 +34,6 @@ public function test_available_filters_contains_matches() // 1 sort ->assertJsonCount(6, 'data'); - $this->assertSame( $response->json('data.4.key'), 'matches' ); @@ -62,7 +61,7 @@ public function test_value_filter_doesnt_require_value() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -84,7 +83,7 @@ public function test_the_boolean_filter_is_applied() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -104,7 +103,7 @@ public function test_the_select_filter_is_applied() $response = $this ->withExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index ccc1144d7..6389d757f 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -108,7 +108,7 @@ public function test_repository_with_deep_relations() }); }); - $response = $this->getJson(CompanyRepository::uriKey() . '?related=users.posts') + $response = $this->getJson(CompanyRepository::uriKey().'?related=users.posts') ->assertOk(); $this->assertCount(1, $response->json('data.0.relationships.users')); From 1c29a4aa7e27d9c02a753427d312a517286e7c1f Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Tue, 8 Dec 2020 12:34:57 +0200 Subject: [PATCH 3/6] Adding Searchable field --- src/Filters/MatchFilter.php | 2 + src/Filters/SearchableFilter.php | 44 +++++++++++++++++++ src/Filters/SortableFilter.php | 2 + .../RepositoryFilterController.php | 10 ++++- .../RepositoryFilterControllerTest.php | 16 ++++++- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/Filters/SearchableFilter.php diff --git a/src/Filters/MatchFilter.php b/src/Filters/MatchFilter.php index 8c1ca992a..4335e059b 100644 --- a/src/Filters/MatchFilter.php +++ b/src/Filters/MatchFilter.php @@ -11,6 +11,8 @@ class MatchFilter extends Filter { public $column = 'id'; + public static $uriKey = 'matches'; + public function filter(RestifyRequest $request, $query, $value) { //@todo improve this diff --git a/src/Filters/SearchableFilter.php b/src/Filters/SearchableFilter.php new file mode 100644 index 000000000..753cd5bb0 --- /dev/null +++ b/src/Filters/SearchableFilter.php @@ -0,0 +1,44 @@ +where($this->column, 'LIKE', "%{$value}%"); + } + + public static function makeFromSimple($column): self + { + return tap(new static, function (SearchableFilter $filter) use ($column) { + $filter->column = $column; + }); + } + + public static function makeForRepository(Repository $repository): Collection + { + return collect($repository::getSearchableFields())->map(function ($column) { + return static::makeFromSimple($column); + }); + } + + public function jsonSerialize() + { + return [ + 'class' => static::class, + 'key' => static::uriKey(), + 'column' => $this->column, + ]; + } +} diff --git a/src/Filters/SortableFilter.php b/src/Filters/SortableFilter.php index 371fa7c74..aead4026b 100644 --- a/src/Filters/SortableFilter.php +++ b/src/Filters/SortableFilter.php @@ -11,6 +11,8 @@ class SortableFilter extends Filter { public $column = 'id'; + public static $uriKey = 'sortables'; + public function filter(RestifyRequest $request, $query, $direction) { //@todo improve this diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index 72ac43932..e85932950 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -3,6 +3,7 @@ namespace Binaryk\LaravelRestify\Http\Controllers; use Binaryk\LaravelRestify\Filters\MatchFilter; +use Binaryk\LaravelRestify\Filters\SearchableFilter; use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RepositoryFiltersRequest; use Illuminate\Support\Collection; @@ -16,16 +17,21 @@ public function __invoke(RepositoryFiltersRequest $request) return $this->response()->data( $repository->availableFilters($request) - ->when(Str::contains($request->input('include'), 'matches'), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), MatchFilter::uriKey()), function (Collection $collection) use ($repository) { return $collection->merge( MatchFilter::makeForRepository($repository) ); }) - ->when(Str::contains($request->input('include'), 'sortable'), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), SortableFilter::uriKey()), function (Collection $collection) use ($repository) { return $collection->merge( SortableFilter::makeForRepository($repository) ); }) + ->when(Str::contains($request->input('include'), SearchableFilter::uriKey()), function (Collection $collection) use ($repository) { + return $collection->merge( + SearchableFilter::makeForRepository($repository) + ); + }) ); } } diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index ec082bd19..42193d8f1 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -28,11 +28,17 @@ public function test_available_filters_contains_matches() 'title', ]; - $response = $this->getJson('posts/filters?include=matches,sortable') + PostRepository::$search = [ + 'id', + 'title', + ]; + + $response = $this->getJson('posts/filters?include=matches,sortables,searchables') // 5 custom filters // 1 match filter // 1 sort - ->assertJsonCount(6, 'data'); + // 2 searchable + ->assertJsonCount(8, 'data'); $this->assertSame( @@ -47,6 +53,12 @@ public function test_available_filters_contains_matches() $this->assertSame( $response->json('data.5.column'), 'title' ); + $this->assertSame( + $response->json('data.6.key'), 'searchables' + ); + $this->assertSame( + $response->json('data.6.column'), 'id' + ); } public function test_value_filter_doesnt_require_value() From bc85fb23bc7d7f19ae8ae005de51e983c4a1f963 Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Tue, 8 Dec 2020 12:35:33 +0200 Subject: [PATCH 4/6] Apply fixes from StyleCI (#297) --- src/Http/Controllers/RepositoryFilterController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index e85932950..5dfcb8506 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -17,17 +17,17 @@ public function __invoke(RepositoryFiltersRequest $request) return $this->response()->data( $repository->availableFilters($request) - ->when(Str::contains($request->input('include'), MatchFilter::uriKey()), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), MatchFilter::uriKey()), function (Collection $collection) use ($repository) { return $collection->merge( MatchFilter::makeForRepository($repository) ); }) - ->when(Str::contains($request->input('include'), SortableFilter::uriKey()), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), SortableFilter::uriKey()), function (Collection $collection) use ($repository) { return $collection->merge( SortableFilter::makeForRepository($repository) ); }) - ->when(Str::contains($request->input('include'), SearchableFilter::uriKey()), function (Collection $collection) use ($repository) { + ->when(Str::contains($request->input('include'), SearchableFilter::uriKey()), function (Collection $collection) use ($repository) { return $collection->merge( SearchableFilter::makeForRepository($repository) ); From b2f7ff4253a9b3d121ab29b521dc6cafa3230c43 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Tue, 8 Dec 2020 13:02:38 +0200 Subject: [PATCH 5/6] Docs. --- .../docs/4.0/custom-filters/custom-filters.md | 6 +++ docs/docs/4.0/filtering/filtering.md | 28 ++++++++++++++ .../RepositoryFilterController.php | 25 +++++++------ .../RepositoryFilterControllerTest.php | 37 +++++++++++++++++-- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/docs/docs/4.0/custom-filters/custom-filters.md b/docs/docs/4.0/custom-filters/custom-filters.md index fc28180d2..716b71199 100644 --- a/docs/docs/4.0/custom-filters/custom-filters.md +++ b/docs/docs/4.0/custom-filters/custom-filters.md @@ -230,3 +230,9 @@ The response will look like this: ] ``` +Along with custom filters, you can also include in the response the primary filters (as matches), by using `?include` query param: + +```http request +/api/restify/posts/filters?include=matches,searchables,sortables +``` + diff --git a/docs/docs/4.0/filtering/filtering.md b/docs/docs/4.0/filtering/filtering.md index 50e13bd11..eb1b17de2 100644 --- a/docs/docs/4.0/filtering/filtering.md +++ b/docs/docs/4.0/filtering/filtering.md @@ -20,6 +20,14 @@ request: GET: /api/restify/posts?search="Test title" ``` +### Get available searchables + +You can use the following request to available searchable attributes for a repository: + +```http request +/api/restify/posts/filters?only=searchables +``` + ## Match Matching by specific attributes may be useful if you want an exact matching. @@ -187,6 +195,14 @@ The next step is to associate this class with the match key name in your `$match ]; ``` +### Get available matches + +You can use the following request to get all repository matches: + +```http request +/api/restify/posts/filters?only=matches +``` + ## Sort When index query entities, usually we have to sort by specific attributes. This requires the `$sort` configuration: @@ -217,6 +233,18 @@ or with plus sign before the field: GET: /api/restify/posts?sort=+id ``` +### Get available sorts + +You can use the following request to get sortable attributes for a repository: + +```http request +/api/restify/posts/filters?only=sortables +``` + +:::tip All filters +You can use `/api/restify/posts/filters?only=sortables` request, and concatenate: `?only=sortables,matches, searchables` to get all of them at once. +::: + ## Eager loading - aka withs When get a repository index or details about a single entity, often we have to get the related entities (we have access to). diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index e85932950..1b8efc54f 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -2,6 +2,7 @@ namespace Binaryk\LaravelRestify\Http\Controllers; +use Binaryk\LaravelRestify\Filter; use Binaryk\LaravelRestify\Filters\MatchFilter; use Binaryk\LaravelRestify\Filters\SearchableFilter; use Binaryk\LaravelRestify\Filters\SortableFilter; @@ -17,20 +18,22 @@ public function __invoke(RepositoryFiltersRequest $request) return $this->response()->data( $repository->availableFilters($request) - ->when(Str::contains($request->input('include'), MatchFilter::uriKey()), function (Collection $collection) use ($repository) { + // After + ->when($request->has('include'), function (Collection $collection) use ($repository, $request) { return $collection->merge( - MatchFilter::makeForRepository($repository) + collect(str_getcsv($request->input('include')))->map(fn($key) => collect([ + SearchableFilter::uriKey() => SearchableFilter::class, + MatchFilter::uriKey() => MatchFilter::class, + SortableFilter::uriKey() => SortableFilter::class, + ])->get($key))->flatMap(fn($filterable) => $filterable::makeForRepository($repository)) ); }) - ->when(Str::contains($request->input('include'), SortableFilter::uriKey()), function (Collection $collection) use ($repository) { - return $collection->merge( - SortableFilter::makeForRepository($repository) - ); - }) - ->when(Str::contains($request->input('include'), SearchableFilter::uriKey()), function (Collection $collection) use ($repository) { - return $collection->merge( - SearchableFilter::makeForRepository($repository) - ); + ->when($request->has('only'), function (Collection $collection) use ($repository, $request) { + return collect(str_getcsv($request->input('only')))->map(fn($key) => collect([ + SearchableFilter::uriKey() => SearchableFilter::class, + MatchFilter::uriKey() => MatchFilter::class, + SortableFilter::uriKey() => SortableFilter::class, + ])->get($key))->flatMap(fn($filterable) => $filterable::makeForRepository($repository)); }) ); } diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index b8b674364..47cb836b2 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -18,7 +18,7 @@ public function test_can_get_available_filters() $this->getJson('posts/filters')->assertJsonCount(4, 'data'); } - public function test_available_filters_contains_matches() + public function test_available_filters_contains_matches_sortables_searches() { PostRepository::$match = [ 'title' => 'text', @@ -60,6 +60,35 @@ public function test_available_filters_contains_matches() ); } + public function test_available_filters_returns_only_matches_sortables_searches() + { + PostRepository::$match = [ + 'title' => 'text', + ]; + + PostRepository::$sort = [ + 'title', + ]; + + PostRepository::$search = [ + 'id', + 'title', + ]; + + $response = $this->getJson('posts/filters?only=matches,sortables,searchables') + ->assertJsonCount(4, 'data'); + + $response = $this->getJson('posts/filters?only=matches') + ->assertJsonCount(1, 'data'); + + $response = $this->getJson('posts/filters?only=sortables') + ->assertJsonCount(1, 'data'); + + $response = $this->getJson('posts/filters?only=searchables') + ->assertJsonCount(2, 'data'); + + } + public function test_value_filter_doesnt_require_value() { factory(Post::class)->create(['is_active' => false]); @@ -73,7 +102,7 @@ public function test_value_filter_doesnt_require_value() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -95,7 +124,7 @@ public function test_the_boolean_filter_is_applied() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -115,7 +144,7 @@ public function test_the_select_filter_is_applied() $response = $this ->withExceptionHandling() - ->getJson('posts?filters='.$filters) + ->getJson('posts?filters=' . $filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); From e5c3a2409b616c68de78a1f64955f9459f7f900b Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Tue, 8 Dec 2020 13:03:17 +0200 Subject: [PATCH 6/6] Apply fixes from StyleCI (#298) --- src/Http/Controllers/RepositoryFilterController.php | 10 ++++------ tests/Controllers/RepositoryFilterControllerTest.php | 7 +++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Http/Controllers/RepositoryFilterController.php b/src/Http/Controllers/RepositoryFilterController.php index 1b8efc54f..2bf76e61a 100644 --- a/src/Http/Controllers/RepositoryFilterController.php +++ b/src/Http/Controllers/RepositoryFilterController.php @@ -2,13 +2,11 @@ namespace Binaryk\LaravelRestify\Http\Controllers; -use Binaryk\LaravelRestify\Filter; use Binaryk\LaravelRestify\Filters\MatchFilter; use Binaryk\LaravelRestify\Filters\SearchableFilter; use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RepositoryFiltersRequest; use Illuminate\Support\Collection; -use Illuminate\Support\Str; class RepositoryFilterController extends RepositoryController { @@ -21,19 +19,19 @@ public function __invoke(RepositoryFiltersRequest $request) // After ->when($request->has('include'), function (Collection $collection) use ($repository, $request) { return $collection->merge( - collect(str_getcsv($request->input('include')))->map(fn($key) => collect([ + collect(str_getcsv($request->input('include')))->map(fn ($key) => collect([ SearchableFilter::uriKey() => SearchableFilter::class, MatchFilter::uriKey() => MatchFilter::class, SortableFilter::uriKey() => SortableFilter::class, - ])->get($key))->flatMap(fn($filterable) => $filterable::makeForRepository($repository)) + ])->get($key))->flatMap(fn ($filterable) => $filterable::makeForRepository($repository)) ); }) ->when($request->has('only'), function (Collection $collection) use ($repository, $request) { - return collect(str_getcsv($request->input('only')))->map(fn($key) => collect([ + return collect(str_getcsv($request->input('only')))->map(fn ($key) => collect([ SearchableFilter::uriKey() => SearchableFilter::class, MatchFilter::uriKey() => MatchFilter::class, SortableFilter::uriKey() => SortableFilter::class, - ])->get($key))->flatMap(fn($filterable) => $filterable::makeForRepository($repository)); + ])->get($key))->flatMap(fn ($filterable) => $filterable::makeForRepository($repository)); }) ); } diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index 47cb836b2..10f543419 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -86,7 +86,6 @@ public function test_available_filters_returns_only_matches_sortables_searches() $response = $this->getJson('posts/filters?only=searchables') ->assertJsonCount(2, 'data'); - } public function test_value_filter_doesnt_require_value() @@ -102,7 +101,7 @@ public function test_value_filter_doesnt_require_value() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -124,7 +123,7 @@ public function test_the_boolean_filter_is_applied() $response = $this ->withoutExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data')); @@ -144,7 +143,7 @@ public function test_the_select_filter_is_applied() $response = $this ->withExceptionHandling() - ->getJson('posts?filters=' . $filters) + ->getJson('posts?filters='.$filters) ->assertStatus(200); $this->assertCount(1, $response->json('data'));