diff --git a/config/config.php b/config/config.php index 28945127..fbbfefea 100644 --- a/config/config.php +++ b/config/config.php @@ -38,4 +38,16 @@ DispatchRestifyStartingEvent::class, AuthorizeRestify::class, ], + + /* + |-------------------------------------------------------------------------- + | Restify Exception Handler + |-------------------------------------------------------------------------- + | + | These will override the main application exception handler, + | set to null, it will not override it. + | Having RestifyHandler as a global exception handler is a good approach, since it + | will return the exceptions in an API pretty format. + */ + 'exception_handler' => \Binaryk\LaravelRestify\Exceptions\RestifyHandler::class, ]; diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 759b345b..c38bc2c7 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -19,6 +19,14 @@ module.exports = { title: 'Quick Start', path: '/docs/' }, + { + title: 'Repository', + path: '/docs/repository-pattern/repository-pattern', + }, + { + title: 'Field', + path: '/docs/repository-pattern/field', + }, { title: 'REST methods', path: '/docs/rest-methods/rest-methods', @@ -31,10 +39,6 @@ module.exports = { title: 'Auth service', path: '/docs/auth/auth', }, - { - title: 'Repository', - path: '/docs/repository-pattern/repository-pattern', - }, ] }, plugins: [ diff --git a/docs/docs/repository-pattern/field.md b/docs/docs/repository-pattern/field.md new file mode 100644 index 00000000..16ced33a --- /dev/null +++ b/docs/docs/repository-pattern/field.md @@ -0,0 +1 @@ +# Field diff --git a/docs/docs/repository-pattern/repository-pattern.md b/docs/docs/repository-pattern/repository-pattern.md index 51417c5e..fbebd7c3 100644 --- a/docs/docs/repository-pattern/repository-pattern.md +++ b/docs/docs/repository-pattern/repository-pattern.md @@ -1,3 +1,256 @@ -# Repository pattern +# Repository -## Implementing contract +[[toc]] + +## Introduction + +The Repository is the main core of the Laravel Restify, included with Laravel provides the easiest way of +CRUD over your resources than you can imagine. It works along with +[Laravel API Resource](https://laravel.com/docs/6.x/eloquent-resources), which means you can use all helpers from there right away. + +## Quick start +The follow command will generate you the Repository which will take the control over the post resource. + +```shell script +php artisan restify:repository Post +``` + +The newly created repository could be found in the `app/Restify` directory. + +## Defining Repositories + +```php + +use Binaryk\LaravelRestify\Repositories\Repository; + +class Post extends Repository +{ + /** + * The model the repository corresponds to. + * + * @var string + */ + public static $model = 'App\\Post'; +} +``` + +### Actions handled by the Repository + +Having this in place you're basically ready for the CRUD actions over posts. +You have available the follow endpoints: + +| Verb | URI | Action | +| :------------- |:--------------------------- | :-------| +| GET | `/restify-api/posts` | index | +| GET | `/restify-api/posts/{post}` | show | +| POST | `/restify-api/posts` | store | +| PATCH | `/restify-api/posts/{post}` | update | +| DELETE | `/restify-api/posts/{post}` | destroy | + +### Fields +When storing or updating a repository Restify will retrieve from the request all attributes defined in the `fillable` +array of the model and will fill all of these fields as they are sent through the request. +If you want to customize some fields before they are filled to the model `attribute`, +you can interact with fields by defining them in the `fields` method: +```php +use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Repositories\Repository; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; + +class Post extends Repository +{ + /** + * The model the repository corresponds to. + * + * @var string + */ + public static $model = 'App\\Post'; + + /** + * Resolvable attributes before storing/updating + * + * @param RestifyRequest $request + * @return array + */ + public function fields(RestifyRequest $request) + { + return [ + Field::make('title')->storeCallback(function ($requestValue) { + return is_string($requestValue) ? $requestValue : `N/A`; + }) + ]; + } +} +``` + +:::tip + +`Field` class has many mutations, validators and interactions you can use, these are documented [here](/laravel-restify/docs/repository-pattern/field) + +::: + + +### Dependency injection + +The Laravel [service container](https://laravel.com/docs/6.x/container) is used to resolve all Laravel Restify repositories. +As a result, you are able to type-hint any dependencies your `Repository` may need in its constructor. +The declared dependencies will automatically be resolved and injected into the repository instance: + +:::tip +Don't forget to to call the parent `contructor` +::: + +```php +use Binaryk\LaravelRestify\Repositories\Repository; + +class Post extends Repository +{ + /** + * The model the repository corresponds to. + * + * @var string + */ + public static $model = 'App\\Post'; + + /** + * @var PostService + */ + private $postService; + + /** + * Post constructor. + * @param PostService $service + */ + public function __construct(PostService $service) + { + parent::__construct(); + $this->postService = $service; + } + +} +``` + +## Restify Repository Conventions +Let's diving deeper into the repository, and take step by step each of its available tools and customizable +modules. Since this is just a helper, it should not break your normal development flow. + +### Model name +As we already noticed, each repository basically works as a wrapper over a specific resource. +The fancy naming `resource` is nothing more than a database entity (posts, users etc.). Well, to make the +repository aware of the entity it should take care of, we have to define the model property: + +```php +/** +* The model the repository corresponds to. +* +* @var string +*/ +public static $model = 'App\\Post'; +``` + +## CRUD Methods overriding + +Laravel Restify magically made all "CRUD" operations for you. But sometimes you may want to intercept, or override the +entire logic of a specific action. Let's say your `save` method has to do something different than just storing +the newly created entity in the database. In this case you can easily override each action ([defined here](#actions-handled-by-the-repository)) from the repository: + +### index + +```php + public function index(RestifyRequest $request, Paginator $paginated) + { + // Custom response + } +``` + +### show + +```php + public function show(RestifyRequest $request, $repositoryId) + { + // Custom finding + } +``` + +### store + +```php + /** + * @param RestifyRequest $request + * @return \Illuminate\Http\JsonResponse|void + */ + public function store(Binaryk\LaravelRestify\Http\Requests\RestifyRequest $request) + { + // custom storing + + return $this->response(); + } +``` + +### update + +```php + public function update(RestifyRequest $request, $model) + { + // Custom updating + } +``` + +### destroy + +```php + public function destroy(RestifyRequest $request, $repositoryId) + { + } +``` + +## Transformation layer + +When you call the `posts/{post}` endpoint, the repository will return the following primary +data for a single resource object: + +```json +{ + "data": { + "type": "post", + "id": "1", + "attributes": { + // ... this post's attributes + }, + "meta": { + // ... by default meta includes information about user authorizations over the entity + "authorizedToView": true, + "authorizedToCreate": true, + "authorizedToUpdate": true, + "authorizedToDelete": true + } + } +} +``` + +This response is made according to [JSON:API format](https://jsonapi.org/format/). You can change it for all +repositories at once by modifying the `resolveDetails` method of the abstract Repository: + +```php +/** + * Resolve the response for the details + * + * @param $request + * @param $serialized + * @return array + */ +public function resolveDetails($request, $serialized) +{ + return $serialized; +} +``` + +Since the repository extends the [Laravel Resource](https://laravel.com/docs/6.x/eloquent-resources) you may +may conditionally return a field: + +```php + +``` +## Response customization + +## Scaffolding repository diff --git a/src/Commands/BaseRepositoryCommand.php b/src/Commands/BaseRepositoryCommand.php new file mode 100644 index 00000000..69d0f1b2 --- /dev/null +++ b/src/Commands/BaseRepositoryCommand.php @@ -0,0 +1,67 @@ + + */ +class PolicyCommand extends GeneratorCommand +{ + /** + * The console command name. + * + * @var string + */ + protected $name = 'restify:policy'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Create a new policy for a specific model.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'Policy'; + + /** + * Execute the console command. + * + * @return bool|null + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() + { + parent::handle(); + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function buildClass($name) + { + $namespacedModel = null; + $model = $this->option('model'); + + if (is_null($model)) { + $model = $this->argument('name'); + } + + if ($model && ! Str::startsWith($model, [$this->laravel->getNamespace(), '\\'])) { + $namespacedModel = $this->laravel->getNamespace().$model; + } + + $name .= 'Policy'; + + $rendered = str_replace( + 'UseDummyModel', $namespacedModel ?? $model, parent::buildClass($name) + ); + + $rendered = str_replace( + 'DummyModel', $model, $rendered + ); + + return $rendered; + } + + public function nameWithEnd() + { + $model = $this->option('model'); + + if (is_null($model)) { + $model = $this->argument('name'); + } + + return $model.'Policy'; + } + + protected function getPath($name) + { + return $this->laravel['path'].'/Policies/'.$this->nameWithEnd().'.php'; + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__.'/stubs/policy.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Restify'; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['model', 'm', InputOption::VALUE_REQUIRED, 'The model class being protected.'], + ]; + } +} diff --git a/src/Commands/RepositoryCommand.php b/src/Commands/RepositoryCommand.php index 81ebe8af..cc922986 100644 --- a/src/Commands/RepositoryCommand.php +++ b/src/Commands/RepositoryCommand.php @@ -41,6 +41,10 @@ class RepositoryCommand extends GeneratorCommand public function handle() { parent::handle(); + + $this->callSilent('restify:base-repository', [ + 'name' => 'Repository', + ]); } /** @@ -70,8 +74,8 @@ protected function buildClass($name) '--controller' => true, ]); - $this->call('make:policy', [ - 'name' => $this->argument('name').'Policy', + $this->call('restify:policy', [ + 'name' => $this->argument('name'), ]); } diff --git a/src/Commands/SetupCommand.php b/src/Commands/SetupCommand.php new file mode 100644 index 00000000..a0944d38 --- /dev/null +++ b/src/Commands/SetupCommand.php @@ -0,0 +1,97 @@ +comment('Publishing Restify Service Provider...'); + $this->callSilent('vendor:publish', ['--tag' => 'restify-provider']); + + $this->comment('Publishing Restify config...'); + $this->call('vendor:publish', [ + '--tag' => 'restify-config', + ]); + + $this->registerRestifyServiceProvider(); + + $this->comment('Generating User Repository...'); + $this->callSilent('restify:repository', ['name' => 'User']); + copy(__DIR__.'/stubs/user-repository.stub', app_path('Restify/User.php')); + + $this->setAppNamespace(); + + $this->info('Restify setup successfully.'); + } + + /** + * Register the Restify service provider in the application configuration file. + * + * @return void + */ + protected function registerRestifyServiceProvider() + { + $namespace = Str::replaceLast('\\', '', $this->getAppNamespace()); + + file_put_contents(config_path('app.php'), str_replace( + "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL, + "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL." {$namespace}\Providers\RestifyServiceProvider::class,".PHP_EOL, + file_get_contents(config_path('app.php')) + )); + } + + /** + * Set the proper application namespace on the installed files. + * + * @return void + */ + protected function setAppNamespace() + { + $namespace = $this->getAppNamespace(); + + $this->setAppNamespaceOn(app_path('Restify/User.php'), $namespace); + $this->setAppNamespaceOn(app_path('Providers/RestifyServiceProvider.php'), $namespace); + } + + /** + * Set the namespace on the given file. + * + * @param string $file + * @param string $namespace + * @return void + */ + protected function setAppNamespaceOn($file, $namespace) + { + file_put_contents($file, str_replace( + 'App\\', + $namespace, + file_get_contents($file) + )); + } +} diff --git a/src/Commands/stubs/base-repository.stub b/src/Commands/stubs/base-repository.stub new file mode 100644 index 00000000..287a40c9 --- /dev/null +++ b/src/Commands/stubs/base-repository.stub @@ -0,0 +1,95 @@ +allowToUpdate($request); + return parent::update($request, $repositoryId); + } + + public function destroy(RestifyRequest $request, $repositoryId) + { + return parent::destroy($request, $repositoryId); + } + +} diff --git a/src/Commands/stubs/policy.stub b/src/Commands/stubs/policy.stub index 6780f3de..bb77f0ac 100644 --- a/src/Commands/stubs/policy.stub +++ b/src/Commands/stubs/policy.stub @@ -1,9 +1,10 @@ storingRules('required')->messages([ - // 'required' => 'This field is required bro.', - // ]), ]; - - } - - /** - * @param RestifyRequest $request - * @param Paginator $paginated - * @return \Illuminate\Http\JsonResponse - */ - public function index(RestifyRequest $request, Paginator $paginated) - { - return parent::index($request, $paginated); - } - - /** - * @param RestifyRequest $request - * @return \Illuminate\Http\JsonResponse - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Throwable - */ - public function show(RestifyRequest $request) - { - return parent::show($request); - } - - /** - * @param RestifyRequest $request - * @return \Illuminate\Http\JsonResponse - */ - public function store(RestifyRequest $request) - { - return parent::store($request); - } - - /** - * @param RestifyRequest $request - * @param $model - * @return \Illuminate\Http\JsonResponse|void - */ - public function update(RestifyRequest $request, $model) - { - return parent::update($request, $model); - } - - /** - * @param RestifyRequest $request - * @return \Illuminate\Http\JsonResponse - */ - public function destroy(RestifyRequest $request) - { - return parent::destroy($request); } } diff --git a/src/Commands/stubs/user-repository.stub b/src/Commands/stubs/user-repository.stub new file mode 100644 index 00000000..77f410e5 --- /dev/null +++ b/src/Commands/stubs/user-repository.stub @@ -0,0 +1,33 @@ +rules('required')->storingRules('unique:users')->messages([ + 'required' => 'This field is required.', + ]), + Field::make('password')->storeCallback(function ($value) { + return Hash::make($value); + })->rules('required')->storingRules('confirmed'), + ]; + } +} diff --git a/src/Contracts/RestifySearchable.php b/src/Contracts/RestifySearchable.php index 9b232045..a518ab4f 100644 --- a/src/Contracts/RestifySearchable.php +++ b/src/Contracts/RestifySearchable.php @@ -8,6 +8,7 @@ interface RestifySearchable { const DEFAULT_PER_PAGE = 15; + const DEFAULT_RELATABLE_PER_PAGE = 15; const MATCH_TEXT = 'text'; const MATCH_BOOL = 'bool'; diff --git a/src/Fields/BaseField.php b/src/Fields/BaseField.php index 77e8e1c5..f3a1d8e3 100644 --- a/src/Fields/BaseField.php +++ b/src/Fields/BaseField.php @@ -7,4 +7,34 @@ */ abstract class BaseField { + /** + * Conditionally load the field. + * + * @var bool|callable + */ + public $when = true; + + /** + * Conditionally load the field. + * + * @param callable|bool $condition + * @param bool $default + * @return $this + */ + public function when($condition, $default = false) + { + $this->when = $condition ?? $default; + + return $this; + } + + /** + * Conditionally load the field. + * + * @return bool|callable|mixed + */ + public function filter() + { + return is_callable($this->when) ? call_user_func($this->when) : $this->when; + } } diff --git a/src/Http/Controllers/RepositoryDestroyController.php b/src/Http/Controllers/RepositoryDestroyController.php index 384f456d..9d26663b 100644 --- a/src/Http/Controllers/RepositoryDestroyController.php +++ b/src/Http/Controllers/RepositoryDestroyController.php @@ -30,10 +30,8 @@ public function handle(RepositoryDestroyRequest $request) /** * @var Repository */ - $repository = $request->newRepository(); + $repository = $request->newRepositoryWith($request->findModelQuery()->firstOrFail()); - $repository->authorizeToDelete($request); - - return $repository->destroy($request); + return $repository->destroy($request, request('repositoryId')); } } diff --git a/src/Http/Controllers/RepositoryShowController.php b/src/Http/Controllers/RepositoryShowController.php index 0815f7c4..f9b78be6 100644 --- a/src/Http/Controllers/RepositoryShowController.php +++ b/src/Http/Controllers/RepositoryShowController.php @@ -17,6 +17,6 @@ class RepositoryShowController extends RepositoryController */ public function handle(RestifyRequest $request) { - return $request->newRepositoryWith($request->findModelQuery())->show($request); + return $request->newRepositoryWith($request->findModelQuery())->show($request, request('repositoryId')); } } diff --git a/src/Http/Controllers/RepositoryUpdateController.php b/src/Http/Controllers/RepositoryUpdateController.php index 547b9c16..22394c3b 100644 --- a/src/Http/Controllers/RepositoryUpdateController.php +++ b/src/Http/Controllers/RepositoryUpdateController.php @@ -34,13 +34,7 @@ public function handle(RepositoryUpdateRequest $request) * @var Repository */ $repository = $request->newRepositoryWith($model); - $repository->authorizeToUpdate($request); - $validator = $repository::validatorForUpdate($request, $repository); - if ($validator->fails()) { - return $this->response()->invalid()->errors($validator->errors()->toArray())->respond(); - } - - return $repository->update($request, $model); + return $repository->update($request, request('repositoryId')); } } diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php index bc8f20c2..54716682 100644 --- a/src/Http/Requests/InteractWithRepositories.php +++ b/src/Http/Requests/InteractWithRepositories.php @@ -84,9 +84,7 @@ public function newRepository() { $repository = $this->repository(); - return resolve($repository, [ - 'model' => $repository::newModel(), - ])->withResource($repository::newModel()); + return $repository::resolveWith($repository::newModel()); } /** @@ -117,9 +115,7 @@ public function newRepositoryWith($model) { $repository = $this->repository(); - return resolve($repository, [ - 'model' => $model, - ])->withResource($model); + return $repository::resolveWith($model); } /** diff --git a/src/LaravelRestifyServiceProvider.php b/src/LaravelRestifyServiceProvider.php index 527ac19f..968880b8 100644 --- a/src/LaravelRestifyServiceProvider.php +++ b/src/LaravelRestifyServiceProvider.php @@ -2,8 +2,11 @@ namespace Binaryk\LaravelRestify; +use Binaryk\LaravelRestify\Commands\BaseRepositoryCommand; use Binaryk\LaravelRestify\Commands\CheckPassport; +use Binaryk\LaravelRestify\Commands\PolicyCommand; use Binaryk\LaravelRestify\Commands\RepositoryCommand; +use Binaryk\LaravelRestify\Commands\SetupCommand; use Binaryk\LaravelRestify\Http\Middleware\RestifyInjector; use Illuminate\Contracts\Http\Kernel as HttpKernel; use Illuminate\Support\ServiceProvider; @@ -18,6 +21,9 @@ public function boot() if ($this->app->runningInConsole()) { $this->commands([ CheckPassport::class, + SetupCommand::class, + PolicyCommand::class, + BaseRepositoryCommand::class, ]); $this->registerPublishing(); diff --git a/src/Repositories/Crudable.php b/src/Repositories/Crudable.php index 14f505f6..ac857127 100644 --- a/src/Repositories/Crudable.php +++ b/src/Repositories/Crudable.php @@ -5,16 +5,22 @@ use Binaryk\LaravelRestify\Controllers\RestResponse; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Restify; -use Binaryk\LaravelRestify\Services\Search\SearchService; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; /** * @author Eduard Lupacescu */ trait Crudable { + /** + * @param null $request + * @return \Illuminate\Http\JsonResponse + */ + abstract public function response($request = null); + /** * @param RestifyRequest $request * @param Paginator $paginated @@ -22,9 +28,7 @@ trait Crudable */ public function index(RestifyRequest $request, Paginator $paginated) { - return resolve(static::class, [ - 'model' => $paginated, - ])->withResource($paginated)->response(); + return static::resolveWith($paginated)->response(); } /** @@ -33,15 +37,18 @@ public function index(RestifyRequest $request, Paginator $paginated) * @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Throwable */ - public function show(RestifyRequest $request) + public function show(RestifyRequest $request, $repositoryId) { - $repository = $request->newRepositoryWith(tap(SearchService::instance()->prepareRelations($request, $request->findModelQuery()), function ($query) use ($request) { + /** + * Dive into the Search service to attach relations. + */ + $this->withResource(tap($this->resource, function ($query) use ($request) { $request->newRepository()->detailQuery($request, $query); })->firstOrFail()); - $repository->authorizeToView($request); + $this->authorizeToView($request); - return $repository->response(); + return $this->response($request); } /** @@ -70,11 +77,15 @@ public function store(RestifyRequest $request) * @param RestifyRequest $request * @param $model * @return JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws ValidationException */ - public function update(RestifyRequest $request, $model) + public function update(RestifyRequest $request, $repositoryId) { - DB::transaction(function () use ($request, $model) { - $model = static::fillWhenUpdate($request, $model); + $this->allowToUpdate($request); + + DB::transaction(function () use ($request) { + $model = static::fillWhenUpdate($request, $this->resource); $model->save(); @@ -87,13 +98,14 @@ public function update(RestifyRequest $request, $model) /** * @param RestifyRequest $request * @return JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function destroy(RestifyRequest $request) + public function destroy(RestifyRequest $request, $repositoryId) { - DB::transaction(function () use ($request) { - $model = $request->findModelQuery(); + $this->allowToDestroy($request); - return $model->delete(); + DB::transaction(function () use ($request) { + return $this->resource->delete(); }); return $this->response() @@ -101,8 +113,26 @@ public function destroy(RestifyRequest $request) } /** - * @param null $request + * @param RestifyRequest $request * @return mixed + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws ValidationException */ - abstract public function response($request = null); + public function allowToUpdate(RestifyRequest $request) + { + $this->authorizeToUpdate($request); + + $validator = static::validatorForUpdate($request, $this); + + $validator->validate(); + } + + /** + * @param RestifyRequest $request + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function allowToDestroy(RestifyRequest $request) + { + $this->authorizeToDelete($request); + } } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 7c0cd953..fb5c69f0 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -3,10 +3,10 @@ namespace Binaryk\LaravelRestify\Repositories; use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Binaryk\LaravelRestify\Fields\Field; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Traits\InteractWithSearch; use Binaryk\LaravelRestify\Traits\PerformsQueries; -use Illuminate\Container\Container; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; @@ -25,7 +25,6 @@ abstract class Repository extends RepositoryCollection implements RestifySearcha ValidatingTrait, RepositoryFillFields, PerformsQueries, - ResponseResolver, Crudable; /** @@ -92,12 +91,9 @@ public static function query() /** * @return array - * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function toArray($request) { - $request = Container::getInstance()->make('request'); - if ($this->isRenderingCollection()) { return $this->toArrayForCollection($request); } @@ -106,16 +102,25 @@ public function toArray($request) 'id' => $this->when($this->isRenderingRepository(), function () { return $this->getKey(); }), - 'type' => method_exists($this, 'uriKey') ? static::uriKey() : Str::plural(Str::kebab(class_basename(get_called_class()))), + 'type' => self::model()->getTable(), 'attributes' => $this->resolveDetailsAttributes($request), 'relationships' => $this->when(value($this->resolveDetailsRelationships($request)), $this->resolveDetailsRelationships($request)), 'meta' => $this->when(value($this->resolveDetailsMeta($request)), $this->resolveDetailsMeta($request)), ]; - return $this->resolveDetails($serialized); + return $this->serializeDetails($request, $serialized); } - abstract public function fields(RestifyRequest $request); + /** + * Resolvable attributes before storing/updating. + * + * @param RestifyRequest $request + * @return array + */ + public function fields(RestifyRequest $request) + { + return []; + } /** * @param RestifyRequest $request @@ -123,7 +128,9 @@ abstract public function fields(RestifyRequest $request); */ public function collectFields(RestifyRequest $request) { - return collect($this->fields($request)); + return collect($this->fields($request))->filter(function (Field $field) { + return $field->filter(); + }); } /** @@ -136,4 +143,19 @@ public function withResource($resource) return $this; } + + /** + * Resolve repository with given model. + * @param $model + * @return Repository + */ + public static function resolveWith($model) + { + /** + * @var Repository + */ + $self = resolve(static::class); + + return $self->withResource($model); + } } diff --git a/src/Repositories/RepositoryCollection.php b/src/Repositories/RepositoryCollection.php index 1debf3c4..103b1d08 100644 --- a/src/Repositories/RepositoryCollection.php +++ b/src/Repositories/RepositoryCollection.php @@ -14,6 +14,8 @@ */ class RepositoryCollection extends Resource { + use ResponseResolver; + /** * When the repository is used as a response for a collection list (index controller). * @@ -27,7 +29,7 @@ public function toArrayForCollection($request) $currentRepository = Restify::repositoryForModel(get_class($this->model())); if (is_null($currentRepository)) { - return parent::toArray($request); + return Arr::only(parent::toArray($request), 'data'); } $data = collect([]); @@ -39,16 +41,14 @@ public function toArrayForCollection($request) } $response = $data->map(function ($value) use ($currentRepository) { - return resolve($currentRepository, [ - 'model' => $value, - ])->withResource($value); + return static::resolveWith($value); })->toArray($request); - return [ + return $this->serializeIndex($request, [ 'meta' => $this->when($this->isRenderingPaginated(), $this->meta($paginated)), 'links' => $this->when($this->isRenderingPaginated(), $this->paginationLinks($paginated)), 'data' => $response, - ]; + ]); } /** diff --git a/src/Repositories/RepositoryFillFields.php b/src/Repositories/RepositoryFillFields.php index df4fdfe0..26e793f9 100644 --- a/src/Repositories/RepositoryFillFields.php +++ b/src/Repositories/RepositoryFillFields.php @@ -40,13 +40,10 @@ public static function fillWhenStore(RestifyRequest $request, $model) */ public static function fillWhenUpdate(RestifyRequest $request, $model) { - static::fillFields( - $request, $model, - (new static($model))->collectFields($request) - ); - static::fillExtra($request, $model, - (new static($model))->collectFields($request) - ); + $fields = static::resolveWith($model)->collectFields($request); + + static::fillFields($request, $model, $fields); + static::fillExtra($request, $model, $fields); return $model; } diff --git a/src/Repositories/ResponseResolver.php b/src/Repositories/ResponseResolver.php index 00ca9556..827c8a57 100644 --- a/src/Repositories/ResponseResolver.php +++ b/src/Repositories/ResponseResolver.php @@ -2,6 +2,8 @@ namespace Binaryk\LaravelRestify\Repositories; +use Binaryk\LaravelRestify\Contracts\RestifySearchable; + /** * @author Eduard Lupacescu */ @@ -40,16 +42,48 @@ public function resolveDetailsMeta($request) */ public function resolveDetailsRelationships($request) { - return []; + if (is_null($request->get('with'))) { + return []; + } + + $withs = []; + + if ($this->resource instanceof RestifySearchable) { + with(explode(',', $request->get('with')), function ($relations) use ($request, &$withs) { + foreach ($relations as $relation) { + if (in_array($relation, $this->resource::getWiths())) { + $relatable = static::resolveWith($this->resource->{$relation}()->paginate($request->get('relatablePerPage') ?? ($this->resource::$defaultRelatablePerPage ?? RestifySearchable::DEFAULT_RELATABLE_PER_PAGE)))->toArray($request); + unset($relatable['meta']); + unset($relatable['links']); + $withs[$relation] = $relatable; + } + } + }); + } + + return $withs; } /** - * Triggered after toArray. + * Resolve the response for the details. * + * @param $request + * @param $serialized + * @return array + */ + public function serializeDetails($request, $serialized) + { + return $serialized; + } + + /** + * Resolve the response for the index request. + * + * @param $request * @param $serialized * @return array */ - public function resolveDetails($serialized) + public function serializeIndex($request, $serialized) { return $serialized; } diff --git a/src/Repositories/ValidatingTrait.php b/src/Repositories/ValidatingTrait.php index dbebf566..3ffecd0a 100644 --- a/src/Repositories/ValidatingTrait.php +++ b/src/Repositories/ValidatingTrait.php @@ -63,7 +63,7 @@ public static function validateForUpdate(RestifyRequest $request, $resource = nu */ public static function validatorForUpdate(RestifyRequest $request, $resource = null) { - $on = $resource ?? (new static(static::newModel())); + $on = $resource ?? static::resolveWith(static::newModel()); $messages = $on->collectFields($request)->flatMap(function ($k) { $messages = []; diff --git a/src/RestifyApplicationServiceProvider.php b/src/RestifyApplicationServiceProvider.php index fe0c7b64..cbde5dca 100644 --- a/src/RestifyApplicationServiceProvider.php +++ b/src/RestifyApplicationServiceProvider.php @@ -2,7 +2,6 @@ namespace Binaryk\LaravelRestify; -use Binaryk\LaravelRestify\Exceptions\RestifyHandler; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; @@ -26,6 +25,10 @@ public function boot() */ protected function repositories() { + if (false === is_dir(app_path('Restify'))) { + mkdir(app_path('Restify')); + } + Restify::repositoriesFrom(app_path('Restify')); } @@ -36,7 +39,9 @@ protected function repositories() */ protected function registerExceptionHandler() { - $this->app->bind(ExceptionHandler::class, RestifyHandler::class); + if (config('restify.exception_handler') && class_exists(value(config('restify.exception_handler')))) { + $this->app->bind(ExceptionHandler::class, value(config('restify.exception_handler'))); + } } /** diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index 553837db..bd7d3836 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -132,6 +132,7 @@ public static function authorizedToCreate(Request $request) * @return void * * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Throwable */ public function authorizeToUpdate(Request $request) { @@ -311,7 +312,7 @@ public function determineModel() { $model = $this instanceof Model ? $this : ($this->resource ?? null); - throw_if(is_null($model), new ModelNotFoundException(__('Model does not declared in :class', ['class' => self::class]))); + throw_if(is_null($model), new ModelNotFoundException(__('Model is not declared in :class', ['class' => self::class]))); return $model; } diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php index b834508c..385fc701 100644 --- a/src/Traits/InteractWithSearch.php +++ b/src/Traits/InteractWithSearch.php @@ -10,6 +10,7 @@ trait InteractWithSearch use AuthorizableModels; public static $defaultPerPage = 15; + public static $defaultRelatablePerPage = 15; /** * @return array