From fcbecaf48c64e36f9eae055f68cfce7a194fbe85 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Sat, 22 Jan 2022 17:20:47 +0200 Subject: [PATCH 1/4] getters --- routes/api.php | 8 ++ src/Bootstrap/RoutesBoot.php | 35 +++++- src/Getters/Getter.php | 108 ++++++++++++++++++ .../Controllers/ListGettersController.php | 16 +++ .../ListRepositoryGettersController.php | 16 +++ .../Controllers/PerformGetterController.php | 15 +++ .../PerformRepositoryGetterController.php | 15 +++ src/Http/Requests/GetterRequest.php | 68 +++++++++++ src/Http/Requests/RepositoryGetterRequest.php | 7 ++ src/Repositories/Concerns/Testing.php | 7 ++ src/Repositories/Repository.php | 1 + src/Repositories/ResolvesGetters.php | 47 ++++++++ src/Traits/AuthorizedToRun.php | 42 +++++++ src/helpers.php | 17 ++- tests/Actions/ListActionsControllerTest.php | 27 ++--- .../Post/Getters/PostsIndexGetter.php | 15 +++ .../Fixtures/Post/Getters/PostsShowGetter.php | 18 +++ .../Getters/UnauthenticatedActionGetter.php | 17 +++ tests/Fixtures/Post/PostRepository.php | 21 +++- tests/Getters/ListGettersControllerTest.php | 36 ++++++ tests/Getters/PerformGetterControllerTest.php | 60 ++++++++++ 21 files changed, 565 insertions(+), 31 deletions(-) create mode 100644 src/Getters/Getter.php create mode 100644 src/Http/Controllers/ListGettersController.php create mode 100644 src/Http/Controllers/ListRepositoryGettersController.php create mode 100644 src/Http/Controllers/PerformGetterController.php create mode 100644 src/Http/Controllers/PerformRepositoryGetterController.php create mode 100644 src/Http/Requests/GetterRequest.php create mode 100644 src/Http/Requests/RepositoryGetterRequest.php create mode 100644 src/Repositories/ResolvesGetters.php create mode 100644 src/Traits/AuthorizedToRun.php create mode 100644 tests/Fixtures/Post/Getters/PostsIndexGetter.php create mode 100644 tests/Fixtures/Post/Getters/PostsShowGetter.php create mode 100644 tests/Fixtures/Post/Getters/UnauthenticatedActionGetter.php create mode 100644 tests/Getters/ListGettersControllerTest.php create mode 100644 tests/Getters/PerformGetterControllerTest.php diff --git a/routes/api.php b/routes/api.php index 921268dfb..80d6c29f9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,8 @@ name('restify.actions.repository.perform'); Route::post('/{repository}/{repositoryId}/actions', \Binaryk\LaravelRestify\Http\Controllers\PerformRepositoryActionController::class); // alias to the previous route +// Getters +Route::get('/{repository}/getters', \Binaryk\LaravelRestify\Http\Controllers\ListGettersController::class)->name('restify.getters.index'); +Route::get('/{repository}/{repositoryId}/getters', \Binaryk\LaravelRestify\Http\Controllers\ListRepositoryGettersController::class)->name('restify.getters.repository.index'); +Route::get('/{repository}/getters/{getter}', \Binaryk\LaravelRestify\Http\Controllers\PerformGetterController::class)->name('restify.getters.perform'); +Route::get('/{repository}/{repositoryId}/getters/{getter}', \Binaryk\LaravelRestify\Http\Controllers\PerformRepositoryGetterController::class)->name('restify.getters.repository.perform'); + // API CRUD Route::get('/{repository}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryIndexController::class)->name('restify.index'); Route::post('/{repository}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreController::class)->name('restify.store'); diff --git a/src/Bootstrap/RoutesBoot.php b/src/Bootstrap/RoutesBoot.php index b5a8a3188..5d02a9a2a 100644 --- a/src/Bootstrap/RoutesBoot.php +++ b/src/Bootstrap/RoutesBoot.php @@ -2,7 +2,10 @@ namespace Binaryk\LaravelRestify\Bootstrap; +use Binaryk\LaravelRestify\Getters\Getter; +use Binaryk\LaravelRestify\Http\Controllers\PerformGetterController; use Binaryk\LaravelRestify\Http\Controllers\RepositoryIndexController; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Restify; use Illuminate\Contracts\Foundation\CachesRoutes; use Illuminate\Foundation\Application; @@ -24,7 +27,9 @@ public function boot(): void 'middleware' => config('restify.middleware', []), ]; - $this->defaultRoutes($config) + $this +// ->registerCustomGettersPerforms($config) + ->defaultRoutes($config) ->registerPrefixed($config) ->registerIndexPrefixed($config); } @@ -41,7 +46,7 @@ public function defaultRoutes($config): self public function registerPrefixed($config): self { collect(Restify::$repositories) - ->filter(fn ($repository) => $repository::prefix()) + ->filter(fn($repository) => $repository::prefix()) ->each(function (string $repository) use ($config) { $config['prefix'] = $repository::prefix(); Route::group($config, function () { @@ -55,7 +60,7 @@ public function registerPrefixed($config): self public function registerIndexPrefixed($config): self { collect(Restify::$repositories) - ->filter(fn ($repository) => $repository::hasIndexPrefix()) + ->filter(fn($repository) => $repository::hasIndexPrefix()) ->each(function ($repository) use ($config) { $config['prefix'] = $repository::indexPrefix(); Route::group($config, function () { @@ -68,10 +73,32 @@ public function registerIndexPrefixed($config): self private function loadRoutesFrom(string $path): self { - if (! ($this->app instanceof CachesRoutes && $this->app->routesAreCached())) { + if (!($this->app instanceof CachesRoutes && $this->app->routesAreCached())) { require $path; } return $this; } + + // @deprecated + public function registerCustomGettersPerforms($config): self + { + collect(Restify::$repositories) + ->filter(function ($repository) use ($config) { + return collect(app($repository) + ->getters(app(RestifyRequest::class))) + ->each(function (Getter $getter) use ($config, $repository) { + if (count($excludedMiddleware = $getter->excludedMiddleware())) { + Route::group($config, function () use ($excludedMiddleware, $repository, $getter) { + $getterKey = $getter->uriKey(); + + Route::get("/{repository}/getters/$getterKey", PerformGetterController::class) + ->withoutMiddleware($excludedMiddleware); + }); + } + }); + }); + + return $this; + } } diff --git a/src/Getters/Getter.php b/src/Getters/Getter.php new file mode 100644 index 000000000..63749b0ba --- /dev/null +++ b/src/Getters/Getter.php @@ -0,0 +1,108 @@ +name(), '-', null); + } + + /** + * @throws Throwable + */ + public function handleRequest(GetterRequest $request): JsonResponse + { + throw_unless(method_exists($this, 'handle'), new Exception('Missing handle method from the getter.')); + + if ($request->isForRepositoryRequest()) { + return $this->handle( + $request, + tap($request->modelQuery(), fn(Builder $query) => static::indexQuery($request, $query) + )->firstOrFail() + ); + } + + return $this->handle($request); + } + + public function withoutMiddleware(string|array $middleware): self + { + $this->action['excluded_middleware'] = array_merge( + (array) ($this->action['excluded_middleware'] ?? []), Arr::wrap($middleware) + ); + + return $this; + } + + public function excludedMiddleware(): array + { + return (array) ($this->action['excluded_middleware'] ?? []); + } + + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return array_merge([ + 'name' => $this->name(), + 'uriKey' => $this->uriKey(), + ]); + } +} diff --git a/src/Http/Controllers/ListGettersController.php b/src/Http/Controllers/ListGettersController.php new file mode 100644 index 000000000..d8f234643 --- /dev/null +++ b/src/Http/Controllers/ListGettersController.php @@ -0,0 +1,16 @@ +repository()->availableGetters($request) + ); + } +} diff --git a/src/Http/Controllers/ListRepositoryGettersController.php b/src/Http/Controllers/ListRepositoryGettersController.php new file mode 100644 index 000000000..2bcbe858b --- /dev/null +++ b/src/Http/Controllers/ListRepositoryGettersController.php @@ -0,0 +1,16 @@ +repository()->availableGetters($request) + ); + } +} diff --git a/src/Http/Controllers/PerformGetterController.php b/src/Http/Controllers/PerformGetterController.php new file mode 100644 index 000000000..ea3487f85 --- /dev/null +++ b/src/Http/Controllers/PerformGetterController.php @@ -0,0 +1,15 @@ +getter()?->handleRequest( + $request, + ); + } +} diff --git a/src/Http/Controllers/PerformRepositoryGetterController.php b/src/Http/Controllers/PerformRepositoryGetterController.php new file mode 100644 index 000000000..59f232702 --- /dev/null +++ b/src/Http/Controllers/PerformRepositoryGetterController.php @@ -0,0 +1,15 @@ +getter()?->handleRequest( + $request, + ); + } +} diff --git a/src/Http/Requests/GetterRequest.php b/src/Http/Requests/GetterRequest.php new file mode 100644 index 000000000..aa0d795aa --- /dev/null +++ b/src/Http/Requests/GetterRequest.php @@ -0,0 +1,68 @@ +repository()->availableGetters($this)); + } + + public function getter(): Getter + { + return once(function () { + return $this->availableGetters()->first(function ($getter) { + dd($this->route('getter') ?? $this->query('getter')); + return $this->route('getter') ?? $this->query('getter') === $getter->uriKey(); + }) ?: abort( + $this->getterExists() ? 403 : 404, + 'Getter does not exists or you don\'t have enough permissions to perform it.' + ); + }); + } + + protected function getterExists(): bool + { + return $this->availableGetters()->contains(function (Getter $getter) { + return $getter->uriKey() === $this->route('getter') ?? $this->query('getter'); + }); + } + + public function builder(Getter $getter, int $size): Builder + { + return tap(RepositorySearchService::make()->search($this, $this->repository()), function ($query) use ($getter) { + $getter::indexQuery($this, $query); + }) + ->when($this->input('repositories') !== 'all', function ($query) { + $query->whereKey($this->input('repositories', [])); + }) + ->latest($this->model()->getKeyName()); + } + + public function collectRepositories(Getter $getter, $count, Closure $callback): array + { + $output = []; + + if (($query = $this->builder($getter, $count))->count() === 0) { + $output[] = $callback(Collection::make([])); + } + + $query->chunk($count, function ($chunk) use ($callback, &$output) { + $output[] = $callback(Collection::make($chunk)); + }); + + return $output; + } + + public function isForRepositoryRequest(): bool + { + return $this instanceof RepositoryGetterRequest; + } +} diff --git a/src/Http/Requests/RepositoryGetterRequest.php b/src/Http/Requests/RepositoryGetterRequest.php new file mode 100644 index 000000000..a553d487c --- /dev/null +++ b/src/Http/Requests/RepositoryGetterRequest.php @@ -0,0 +1,7 @@ +uriKey()); + } + public function dd(string $prop = null): void { if (is_null($prop)) { diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index c42349b7b..e3b9bd807 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -47,6 +47,7 @@ abstract class Repository implements RestifySearchable, JsonSerializable use ConditionallyLoadsAttributes; use DelegatesToResource; use ResolvesActions; + use ResolvesGetters; use RepositoryEvents; use WithRoutePrefix; use InteractWithFields; diff --git a/src/Repositories/ResolvesGetters.php b/src/Repositories/ResolvesGetters.php new file mode 100644 index 000000000..3fcb45a4a --- /dev/null +++ b/src/Repositories/ResolvesGetters.php @@ -0,0 +1,47 @@ +isForRepositoryRequest() + ? $this->resolveShowGetters($request) + : $this->resolveIndexGetters($request); + + return $getters->filter->authorizedToSee($request)->values(); + } + + public function resolveIndexGetters(GetterRequest $request): Collection + { + return $this->resolveGetters($request)->filter(fn ($getter) => $getter->isShownOnIndex( + $request, + $request->repository() + ))->values(); + } + + public function resolveShowGetters(GetterRequest $request): Collection + { + return $this->resolveGetters($request)->filter(fn ($getter) => $getter->isShownOnShow( + $request, + $request->repositoryWith( + $request->findModelOrFail() + ) + ))->values(); + } + + public function resolveGetters(RestifyRequest $request): Collection + { + return collect(array_values($this->filter($this->getters($request)))); + } + + public function getters(RestifyRequest $request): array + { + return []; + } +} diff --git a/src/Traits/AuthorizedToRun.php b/src/Traits/AuthorizedToRun.php new file mode 100644 index 000000000..c9fcd2281 --- /dev/null +++ b/src/Traits/AuthorizedToRun.php @@ -0,0 +1,42 @@ +runCallback ? call_user_func($this->runCallback, $request, $model) : true; + } + + /** + * Set the callback to be run to authorize running the action. + * + * @param Closure $callback + * @return $this + */ + public function canRun(Closure $callback) + { + $this->runCallback = $callback; + + return $this; + } +} diff --git a/src/helpers.php b/src/helpers.php index 5487aec96..3775b6730 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,21 +4,21 @@ use Binaryk\LaravelRestify\Restify; use Illuminate\Http\JsonResponse; -if (! function_exists('field')) { +if (!function_exists('field')) { function field(...$args): Field { return Field::new(...$args); } } -if (! function_exists('isRestify')) { +if (!function_exists('isRestify')) { function isRestify(\Illuminate\Http\Request $request): bool { return Restify::isRestify($request); } } -if (! function_exists('data')) { +if (!function_exists('data')) { function data(mixed $data = [], int $status = 200, array $headers = [], $options = 0): JsonResponse { return response()->json([ @@ -27,14 +27,19 @@ function data(mixed $data = [], int $status = 200, array $headers = [], $options } } -if (! function_exists('ok')) { - function ok() +if (!function_exists('ok')) { + function ok(string $message = null, int $code = 200) { + if (!is_null($message)) { + return response()->json([ + 'message' => $message, + ], $code); + } return response()->json([], 204); } } -if (! function_exists('id')) { +if (!function_exists('id')) { function id(): Field { return field('id')->readonly(); diff --git a/tests/Actions/ListActionsControllerTest.php b/tests/Actions/ListActionsControllerTest.php index 4a92699d8..6a291732a 100644 --- a/tests/Actions/ListActionsControllerTest.php +++ b/tests/Actions/ListActionsControllerTest.php @@ -2,31 +2,24 @@ namespace Binaryk\LaravelRestify\Tests\Actions; +use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; +use Illuminate\Testing\Fluent\AssertableJson; +// TODO: Please refactor all tests using assertJson (as the first test does). class ListActionsControllerTest extends IntegrationTest { - protected function setUp(): void - { - parent::setUp(); - $this->authenticate(); - } - public function test_could_list_actions_for_repository(): void { $_SERVER['actions.posts.invalidate'] = false; - $this->withoutExceptionHandling()->getJson('posts/actions') - ->assertSuccessful() - ->assertJsonCount(1, 'data') - ->assertJsonStructure([ - 'data' => [ - [ - 'name', - 'uriKey', - ], - ], - ]); + $this->getJson(PostRepository::to('actions')) + ->assertOk() + ->assertJson(fn(AssertableJson $json) => $json + ->count('data', 1) + ->where('data.0.uriKey', 'publish-post-action') + ->etc() + ); } public function test_could_list_actions_for_given_repository(): void diff --git a/tests/Fixtures/Post/Getters/PostsIndexGetter.php b/tests/Fixtures/Post/Getters/PostsIndexGetter.php new file mode 100644 index 000000000..50f6aee52 --- /dev/null +++ b/tests/Fixtures/Post/Getters/PostsIndexGetter.php @@ -0,0 +1,15 @@ +canSee(fn () => true), + ActiveBooleanFilter::new()->canSee(fn() => true), SelectCategoryFilter::new(), CreatedAfterDateFilter::new(), InactiveFilter::new(), @@ -102,9 +106,9 @@ public function actions(RestifyRequest $request): array { return [ PublishPostAction::new() - ->onlyOnShow( - $_SERVER['actions.posts.publish.onlyOnShow'] ?? false, - ), + ->onlyOnShow( + $_SERVER['actions.posts.publish.onlyOnShow'] ?? false, + ), InvalidatePostAction::new() ->onlyOnShow( $_SERVER['actions.posts.onlyOnShow'] ?? true @@ -114,4 +118,13 @@ public function actions(RestifyRequest $request): array }), ]; } + + public function getters(RestifyRequest $request): array + { + return [ + PostsIndexGetter::make(), + PostsShowGetter::make()->onlyOnShow(), + UnauthenticatedActionGetter::make()->withoutMiddleware(AuthorizeRestify::class), + ]; + } } diff --git a/tests/Getters/ListGettersControllerTest.php b/tests/Getters/ListGettersControllerTest.php new file mode 100644 index 000000000..8115237fb --- /dev/null +++ b/tests/Getters/ListGettersControllerTest.php @@ -0,0 +1,36 @@ +getJson(PostRepository::to('getters')) + ->assertOk() + ->assertJson(fn(AssertableJson $json) => $json + ->has('data') + ->where('data.0.uriKey', 'posts-index-getter') + ->count('data', 2) + ->etc() + ); + } + + public function test_could_list_getters_for_given_repository(): void + { + $this->mockPosts(1, 2); + + $this->getJson(PostRepository::to('1/getters')) + ->assertOk() + ->assertJson(fn(AssertableJson $json) => $json + ->has('data') + ->where('data.1.uriKey', 'posts-show-getter') + ->count('data', 3) + ->etc() + ); + } +} diff --git a/tests/Getters/PerformGetterControllerTest.php b/tests/Getters/PerformGetterControllerTest.php new file mode 100644 index 000000000..a6df99dab --- /dev/null +++ b/tests/Getters/PerformGetterControllerTest.php @@ -0,0 +1,60 @@ +getJson(PostRepository::getter(PostsIndexGetter::class)) + ->assertOk() + ->assertJson(fn(AssertableJson $json) => $json + ->where('message', 'it works') + ->etc() + ); + } + + public function test_could_perform_repository_getter(): void + { + $this->mockPosts(1, 2); + + $this + ->getJson(PostRepository::getter(PostsShowGetter::class, 1)) + ->assertSuccessful() + ->assertJson(fn(AssertableJson $json) => $json + ->where('message', 'show works') + ->etc() + ); + } + + public function test_unauthenticated_user_can_access_middleware_when_except_auth(): void + { + $this->markTestSkipped('will implement sometime'); + + Restify::$authUsing = static function (Request $request) { + return ! is_null($request->user()); + }; + + $this + ->withoutExceptionHandling() + ->getJson(PostRepository::getter(UnauthenticatedActionGetter::class)) + ->dump() + ->assertSuccessful() + ->assertJson(fn(AssertableJson $json) => $json + ->etc() + ); + + Restify::$authUsing = static function () { + return true; + }; + } +} From 762a98fc0d8bda4f5d439bc9ac18060bb01e8d48 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Sat, 22 Jan 2022 17:41:40 +0200 Subject: [PATCH 2/4] documentation for getters --- docs-v2/content/en/api/getters.md | 221 ++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs-v2/content/en/api/getters.md diff --git a/docs-v2/content/en/api/getters.md b/docs-v2/content/en/api/getters.md new file mode 100644 index 000000000..915fff0fa --- /dev/null +++ b/docs-v2/content/en/api/getters.md @@ -0,0 +1,221 @@ +--- +title: Getters +menuTitle: Getters +category: API +position: 10 +--- + +## Motivation + +Restify already provides powerful filters and get routes with relationships. However, sometimes you might want to get some extra data for your repositories. + +Let's say you have a stripe user. This is how you retrieve the stripe user information through a get request: + +```php +Route::get('users/stripe-information', UserStripeController::class); + +// UserStripeController.php + +public function __invoke(Request $request) +{ + ... +} +``` + +The `classic` approach is good, however, it has a few limitations. Firstly, you have to manually take care of the route `middleware`, the testability for these endpoints should be done separately which is hard to maintain. And finally, the endpoint is disconnected from the repository, which makes it feel out of context so has a bad readability. + +So, code readability, testability and maintainability become hard. + +## Getter definition + +Getters are very similar to getters. The big difference, is that getters only allow get requests, and should not perform any kind of DB data writing: + +The getter is nothing more than a class, that extends the `Binaryk\LaravelRestify\Getters\Getter` abstract class. + +An example of a getter class: + +```php +namespace App\Restify\Getters; + +use Binaryk\LaravelRestify\Getters\Getter; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request;use Illuminate\Support\Collection; + +class StripeInformationGetter extends Getter +{ + public static $uriKey = 'stripe-information'; + + public function handle(Request $request): JsonResponse + { + return response()->json([ + 'data' => $request->user()->asStripeUser() + ]); + } +} +``` + +### Register getter + +Then add the getter instance to the repository `getters` method: + +```php +// UserRepository.php + +public function getters(RestifyRequest $request): array +{ + return [ + StripeInformationGetter::new() + ]; +} +``` + +### Authorize getter + +You can authorize certain getters to be active for specific users: + +```php +public function getters(RestifyRequest $request): array +{ + return [ + StripeInformationGetter::new()->canSee(function (Request $request) { + return $request->user()->can('seeStripeInfo), + }), + ]; +} +``` + +### Call getters + +To call a getter, you simply access: + +```http request +POST: api/restify/posts/getters/stripe-information +``` + +The `getter` query param value is the `ke-bab` form of the filter class name by default, or a custom `$uriKey` [defined in the getter](#custom-uri-key) + + +### Handle getter + +As soon the getter is called, the handled method will be invoked with the `$request`: + +```php +public function handle(Request $request) +{ + // + + return ok(); +} +``` + +## Getter customizations + +Getters could be easily customized. + +### Custom uri key + +Since your class names could change along the way, you can define a `$uriKey` property to your getters, so the frontend will use always the same `getter` query when applying an getter: + +```php +class StripeInformationGetter extends Getter +{ + public static $uriKey = 'stripe-information'; + //... + +}; +``` + +## Getters scope + +By default, any getter could be used on [index](#index-getters) as well as on [show](#show-getters). However, you can choose to instruct your getter to be displayed to a specific scope. + +## Show getters + +Show getters are used when you have to apply it for a single item. + +### Show getter definition + +The show getter definition is different in the way it receives arguments for the `handle` method. + +Restify automatically resolves Eloquent models defined in the route id and passes it to the getter's handle method: + +```php +public function handle(Request $request, User $user): JsonResponse +{ + +} + +``` + +### Show getter registration + +To register a show getter, we have to use the `->onlyOnShow()` accessor: + +```php +public function getters(RestifyRequest $request) +{ + return [ + StripeInformationGetter::new()->onlyOnShow(), + ]; +} +``` + +### Show getter call + +The post URL should include the key of the model we want Restify to resolve: + +```http request +POST: api/restfiy/users/1/getters/stripe-information +``` +### List show getters + +To get the list of available getters only for a specific model key: + +```http request +GET: api/api/restify/posts/1/getters +``` + +## Index getters + +Index getters are used when you have to apply it for a many items. + +### Index getter definition + +The index getter definition is different in the way it receives arguments for the `handle` method. + +```php +public function handle(Request $request): JsonResponse +{ + // +} + +``` + +### Index getter registration + +To register an index getter, we have to use the `->onlyOnIndex()` accessor: + +```php +public function getters(RestifyRequest $request) +{ + return [ + StripeInformationGetter::new()->onlyOnIndex(), + ]; +} +``` + +### Index getter call + +The post URL: + +```http request +POST: api/restfiy/posts/getters/stripe-information +``` + +### List index getters + +To get the list of available getters: + +```http request +GET: api/api/restify/posts/getters +``` From 9e205beacda999cf7cba457778a59abe376b0ab9 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Wed, 2 Feb 2022 19:04:21 +0200 Subject: [PATCH 3/4] fix: getters --- src/Commands/GetterCommand.php | 61 +++++++++++++++++++++++++++ src/Commands/stubs/action.stub | 2 +- src/Commands/stubs/getter.stub | 16 +++++++ src/LaravelRestifyServiceProvider.php | 2 + 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Commands/GetterCommand.php create mode 100644 src/Commands/stubs/getter.stub diff --git a/src/Commands/GetterCommand.php b/src/Commands/GetterCommand.php new file mode 100644 index 000000000..089e1459e --- /dev/null +++ b/src/Commands/GetterCommand.php @@ -0,0 +1,61 @@ +option('force')) { + return false; + } + } + + /** + * Build the class with the given name. + * This method should return the file class content. + * + * @param string $name + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function buildClass($name) + { + if (false === Str::endsWith($name, 'Getter')) { + $name .= 'Getter'; + } + + return parent::buildClass($name); + } + + protected function getStub() + { + return __DIR__.'/stubs/getter.stub'; + } + + protected function getPath($name) + { + if (false === Str::endsWith($name, 'Getter')) { + $name .= 'Getter'; + } + + return parent::getPath($name); + } + + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Restify\Getters'; + } +} diff --git a/src/Commands/stubs/action.stub b/src/Commands/stubs/action.stub index 2133904a3..b8f1744e0 100644 --- a/src/Commands/stubs/action.stub +++ b/src/Commands/stubs/action.stub @@ -11,6 +11,6 @@ class DummyClass extends Action { public function handle(ActionRequest $request, Collection $models): JsonResponse { - return $this->response()->respond(); + return ok(); } } diff --git a/src/Commands/stubs/getter.stub b/src/Commands/stubs/getter.stub new file mode 100644 index 000000000..b344bad2e --- /dev/null +++ b/src/Commands/stubs/getter.stub @@ -0,0 +1,16 @@ +commands([ RepositoryCommand::class, ActionCommand::class, + GetterCommand::class, StoreCommand::class, FilterCommand::class, DevCommand::class, From 43fa568f9a55ee5f80ea60b408e2d679e25a3e09 Mon Sep 17 00:00:00 2001 From: binaryk Date: Wed, 2 Feb 2022 17:04:48 +0000 Subject: [PATCH 4/4] Fix styling --- src/Bootstrap/RoutesBoot.php | 6 +++--- src/Getters/Getter.php | 10 ++++++---- src/Http/Requests/GetterRequest.php | 1 + src/helpers.php | 13 +++++++------ tests/Actions/ListActionsControllerTest.php | 3 ++- tests/Fixtures/Post/PostRepository.php | 2 +- tests/Getters/ListGettersControllerTest.php | 6 ++++-- tests/Getters/PerformGetterControllerTest.php | 9 ++++++--- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Bootstrap/RoutesBoot.php b/src/Bootstrap/RoutesBoot.php index 5d02a9a2a..b97027cd9 100644 --- a/src/Bootstrap/RoutesBoot.php +++ b/src/Bootstrap/RoutesBoot.php @@ -46,7 +46,7 @@ public function defaultRoutes($config): self public function registerPrefixed($config): self { collect(Restify::$repositories) - ->filter(fn($repository) => $repository::prefix()) + ->filter(fn ($repository) => $repository::prefix()) ->each(function (string $repository) use ($config) { $config['prefix'] = $repository::prefix(); Route::group($config, function () { @@ -60,7 +60,7 @@ public function registerPrefixed($config): self public function registerIndexPrefixed($config): self { collect(Restify::$repositories) - ->filter(fn($repository) => $repository::hasIndexPrefix()) + ->filter(fn ($repository) => $repository::hasIndexPrefix()) ->each(function ($repository) use ($config) { $config['prefix'] = $repository::indexPrefix(); Route::group($config, function () { @@ -73,7 +73,7 @@ public function registerIndexPrefixed($config): self private function loadRoutesFrom(string $path): self { - if (!($this->app instanceof CachesRoutes && $this->app->routesAreCached())) { + if (! ($this->app instanceof CachesRoutes && $this->app->routesAreCached())) { require $path; } diff --git a/src/Getters/Getter.php b/src/Getters/Getter.php index 63749b0ba..ab0a73e30 100644 --- a/src/Getters/Getter.php +++ b/src/Getters/Getter.php @@ -14,14 +14,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Str; use JsonSerializable; use ReturnTypeWillChange; -use Throwable; use function tap; use function throw_unless; +use Throwable; /** * Class Getter @@ -75,7 +74,9 @@ public function handleRequest(GetterRequest $request): JsonResponse if ($request->isForRepositoryRequest()) { return $this->handle( $request, - tap($request->modelQuery(), fn(Builder $query) => static::indexQuery($request, $query) + tap( + $request->modelQuery(), + fn (Builder $query) => static::indexQuery($request, $query) )->firstOrFail() ); } @@ -86,7 +87,8 @@ public function handleRequest(GetterRequest $request): JsonResponse public function withoutMiddleware(string|array $middleware): self { $this->action['excluded_middleware'] = array_merge( - (array) ($this->action['excluded_middleware'] ?? []), Arr::wrap($middleware) + (array) ($this->action['excluded_middleware'] ?? []), + Arr::wrap($middleware) ); return $this; diff --git a/src/Http/Requests/GetterRequest.php b/src/Http/Requests/GetterRequest.php index aa0d795aa..9280a5f70 100644 --- a/src/Http/Requests/GetterRequest.php +++ b/src/Http/Requests/GetterRequest.php @@ -20,6 +20,7 @@ public function getter(): Getter return once(function () { return $this->availableGetters()->first(function ($getter) { dd($this->route('getter') ?? $this->query('getter')); + return $this->route('getter') ?? $this->query('getter') === $getter->uriKey(); }) ?: abort( $this->getterExists() ? 403 : 404, diff --git a/src/helpers.php b/src/helpers.php index 3775b6730..9122940ac 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,21 +4,21 @@ use Binaryk\LaravelRestify\Restify; use Illuminate\Http\JsonResponse; -if (!function_exists('field')) { +if (! function_exists('field')) { function field(...$args): Field { return Field::new(...$args); } } -if (!function_exists('isRestify')) { +if (! function_exists('isRestify')) { function isRestify(\Illuminate\Http\Request $request): bool { return Restify::isRestify($request); } } -if (!function_exists('data')) { +if (! function_exists('data')) { function data(mixed $data = [], int $status = 200, array $headers = [], $options = 0): JsonResponse { return response()->json([ @@ -27,19 +27,20 @@ function data(mixed $data = [], int $status = 200, array $headers = [], $options } } -if (!function_exists('ok')) { +if (! function_exists('ok')) { function ok(string $message = null, int $code = 200) { - if (!is_null($message)) { + if (! is_null($message)) { return response()->json([ 'message' => $message, ], $code); } + return response()->json([], 204); } } -if (!function_exists('id')) { +if (! function_exists('id')) { function id(): Field { return field('id')->readonly(); diff --git a/tests/Actions/ListActionsControllerTest.php b/tests/Actions/ListActionsControllerTest.php index 6a291732a..4e9bff153 100644 --- a/tests/Actions/ListActionsControllerTest.php +++ b/tests/Actions/ListActionsControllerTest.php @@ -15,7 +15,8 @@ public function test_could_list_actions_for_repository(): void $this->getJson(PostRepository::to('actions')) ->assertOk() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->count('data', 1) ->where('data.0.uriKey', 'publish-post-action') ->etc() diff --git a/tests/Fixtures/Post/PostRepository.php b/tests/Fixtures/Post/PostRepository.php index a8fe42ade..741a15481 100644 --- a/tests/Fixtures/Post/PostRepository.php +++ b/tests/Fixtures/Post/PostRepository.php @@ -87,7 +87,7 @@ public function fieldsForUpdateBulk(RestifyRequest $request) public function filters(RestifyRequest $request): array { return [ - ActiveBooleanFilter::new()->canSee(fn() => true), + ActiveBooleanFilter::new()->canSee(fn () => true), SelectCategoryFilter::new(), CreatedAfterDateFilter::new(), InactiveFilter::new(), diff --git a/tests/Getters/ListGettersControllerTest.php b/tests/Getters/ListGettersControllerTest.php index 8115237fb..1933e4c5e 100644 --- a/tests/Getters/ListGettersControllerTest.php +++ b/tests/Getters/ListGettersControllerTest.php @@ -12,7 +12,8 @@ public function test_could_list_getters_for_repository(): void { $this->getJson(PostRepository::to('getters')) ->assertOk() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->has('data') ->where('data.0.uriKey', 'posts-index-getter') ->count('data', 2) @@ -26,7 +27,8 @@ public function test_could_list_getters_for_given_repository(): void $this->getJson(PostRepository::to('1/getters')) ->assertOk() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->has('data') ->where('data.1.uriKey', 'posts-show-getter') ->count('data', 3) diff --git a/tests/Getters/PerformGetterControllerTest.php b/tests/Getters/PerformGetterControllerTest.php index a6df99dab..b77f9c1ea 100644 --- a/tests/Getters/PerformGetterControllerTest.php +++ b/tests/Getters/PerformGetterControllerTest.php @@ -17,7 +17,8 @@ public function test_could_perform_getter(): void { $this->getJson(PostRepository::getter(PostsIndexGetter::class)) ->assertOk() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->where('message', 'it works') ->etc() ); @@ -30,7 +31,8 @@ public function test_could_perform_repository_getter(): void $this ->getJson(PostRepository::getter(PostsShowGetter::class, 1)) ->assertSuccessful() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->where('message', 'show works') ->etc() ); @@ -49,7 +51,8 @@ public function test_unauthenticated_user_can_access_middleware_when_except_auth ->getJson(PostRepository::getter(UnauthenticatedActionGetter::class)) ->dump() ->assertSuccessful() - ->assertJson(fn(AssertableJson $json) => $json + ->assertJson( + fn (AssertableJson $json) => $json ->etc() );