diff --git a/src/Contracts/RestifySearchable.php b/src/Contracts/RestifySearchable.php new file mode 100644 index 00000000..e761cdd7 --- /dev/null +++ b/src/Contracts/RestifySearchable.php @@ -0,0 +1,56 @@ + + */ +interface RestifySearchable +{ + const DEFAULT_PER_PAGE = 15; + + const MATCH_TEXT = 'text'; + const MATCH_BOOL = 'bool'; + const MATCH_INTEGER = 'integer'; + + /** + * @param Request $request + * @param array $fields + * @return array + */ + public function serializeForIndex(Request $request, array $fields = []); + + /** + * @return array + */ + public static function getSearchableFields(); + + /** + * @return array + */ + public static function getWiths(); + + /** + * @return array + */ + public static function getInFields(); + + /** + * Find matches in the table by given value + * Returns an array like: + * [ 'table_column_name' => 'type' ], type can be: text, bool, boolean, int, integer, number + * e.g. [ 'id' => 'int' ]. + * + * To use this filter we have to send in query: + * [ 'match' => [ 'id' => 1 ] ] + * @return array + */ + public static function getMatchByFields(); + + /** + * @return array + */ + public static function getOrderByFields(); +} diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index 02ba963a..eae58dfc 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -2,10 +2,15 @@ namespace Binaryk\LaravelRestify\Controllers; +use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Exceptions\Guard\EntityNotFoundException; use Binaryk\LaravelRestify\Exceptions\Guard\GatePolicy; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Services\Search\SearchService; +use Binaryk\LaravelRestify\Traits\PerformsQueries; use Illuminate\Config\Repository; use Illuminate\Config\Repository as Config; +use Illuminate\Container\Container; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Guard; @@ -15,7 +20,6 @@ use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Password; @@ -30,7 +34,7 @@ */ abstract class RestController extends BaseController { - use AuthorizesRequests, DispatchesJobs, ValidatesRequests; + use AuthorizesRequests, DispatchesJobs, ValidatesRequests, PerformsQueries; /** * @var RestResponse @@ -43,7 +47,7 @@ abstract class RestController extends BaseController protected $gate; /** - * @var Request + * @var RestifyRequest */ protected $request; @@ -58,13 +62,15 @@ abstract class RestController extends BaseController protected $guard; /** - * @return Request + * @return RestifyRequest * @throws BindingResolutionException */ public function request() { - if (($this->request instanceof Request) === false) { - $this->request = app()->make(Request::class); + $container = Container::getInstance(); + + if (($this->request instanceof RestifyRequest) === false) { + $this->request = $container->make(RestifyRequest::class); } return $this->request; @@ -76,8 +82,10 @@ public function request() */ public function config() { + $container = Container::getInstance(); + if (($this->config instanceof Repository) === false) { - $this->config = app()->make(Repository::class); + $this->config = $container->make(Repository::class); } return $this->config; @@ -86,10 +94,11 @@ public function config() /** * Returns a generic response to the client. * - * @param mixed $data - * @param int $httpCode + * @param mixed $data + * @param int $httpCode * * @return JsonResponse + * @throws BindingResolutionException */ protected function respond($data = null, $httpCode = 200) { @@ -102,9 +111,9 @@ protected function respond($data = null, $httpCode = 200) /** * Get Response object. * - * @param null $data - * @param int $status - * @param array $headers + * @param null $data + * @param int $status + * @param array $headers * @return RestResponse */ protected function response($data = null, $status = 200, array $headers = []) @@ -116,6 +125,29 @@ protected function response($data = null, $status = 200, array $headers = []) return $this->response; } + /** + * @param $modelClass + * @param array $filters + * @return array + * @throws BindingResolutionException + */ + public function search($modelClass, $filters = []) + { + $results = SearchService::instance() + ->setPredefinedFilters($filters) + ->search($this->request(), new $modelClass); + $results->tap(function ($query) { + static::indexQuery($this->request(), $query); + }); + + $paginator = $results->paginate($this->request()->get('perPage') ?? ($modelClass::$defaultPerPage ?? RestifySearchable::DEFAULT_PER_PAGE)); + $items = $paginator->getCollection()->map->serializeForIndex($this->request()); + + return array_merge($paginator->toArray(), [ + 'data' => $items, + ]); + } + /** * @param $policy * @param $objects @@ -172,6 +204,7 @@ public function broker() * Returns with a message. * @param $msg * @return JsonResponse + * @throws BindingResolutionException */ public function message($msg) { @@ -183,7 +216,9 @@ public function message($msg) /** * Returns with a list of errors. * + * @param array $errors * @return JsonResponse + * @throws BindingResolutionException */ protected function errors(array $errors) { diff --git a/src/Controllers/RestIndexController.php b/src/Controllers/RestIndexController.php new file mode 100644 index 00000000..108bef6a --- /dev/null +++ b/src/Controllers/RestIndexController.php @@ -0,0 +1,10 @@ + + */ +trait RestIndexController +{ +} diff --git a/src/Exceptions/RestifyHandler.php b/src/Exceptions/RestifyHandler.php index 8acc0e69..2e3d1e82 100644 --- a/src/Exceptions/RestifyHandler.php +++ b/src/Exceptions/RestifyHandler.php @@ -10,6 +10,7 @@ use Binaryk\LaravelRestify\Restify; use Closure; use Exception; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; @@ -93,6 +94,7 @@ public function render($request, Exception $exception) case $exception instanceof UnauthorizedHttpException: case $exception instanceof UnauthenticateException: case $exception instanceof ActionUnauthorizedException: + case $exception instanceof AuthorizationException: case $exception instanceof GatePolicy: case $exception instanceof AuthenticationException: $response->addError($exception->getMessage())->auth(); diff --git a/src/Http/Controllers/RepositoryIndexController.php b/src/Http/Controllers/RepositoryIndexController.php index 8a2e02a2..894733d1 100644 --- a/src/Http/Controllers/RepositoryIndexController.php +++ b/src/Http/Controllers/RepositoryIndexController.php @@ -9,11 +9,18 @@ */ class RepositoryIndexController extends RepositoryController { + /** + * @param RestifyRequest $request + * @return \Illuminate\Http\JsonResponse + * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException + * @throws \Binaryk\LaravelRestify\Exceptions\UnauthorizedException + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ public function handle(RestifyRequest $request) { $resource = $request->repository(); - $data = $resource::query()->get(); + $data = $this->search($resource::newModel()); return $this->respond($data); } diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php new file mode 100644 index 00000000..16482797 --- /dev/null +++ b/src/Http/Requests/InteractWithRepositories.php @@ -0,0 +1,66 @@ + + */ +trait InteractWithRepositories +{ + /** + * @var Model + */ + public $model; + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize() + { + return true; + } + + /** + * Get the class name of the repository being requested. + * + * @return Repository + * @throws EntityNotFoundException + * @throws UnauthorizedException + */ + public function repository() + { + return tap(Restify::repositoryForKey($this->route('repository')), function ($repository) { + if (is_null($repository)) { + throw new EntityNotFoundException(__('Repository :name not found.', [ + 'name' => $repository, + ]), 404); + } + + if (! $repository::authorizedToViewAny($this)) { + throw new UnauthorizedException(__('Unauthorized to view repository :name.', [ + 'name' => $repository, + ]), 403); + } + }); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php index e2e0b1ab..711031f4 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -2,9 +2,6 @@ namespace Binaryk\LaravelRestify\Http\Requests; -use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; -use Binaryk\LaravelRestify\Exceptions\UnauthorizedException; -use Binaryk\LaravelRestify\Restify; use Illuminate\Foundation\Http\FormRequest; /** @@ -12,37 +9,5 @@ */ class RestifyRequest extends FormRequest { - /** - * Get the class name of the repository being requested. - * - * @return mixed - */ - public function repository() - { - return tap(Restify::repositoryForKey($this->route('repository')), function ($repository) { - if (is_null($repository)) { - throw new EntityNotFoundException(__('Repository :name not found.', [ - 'name' => $repository, - ]), 404); - } - - if (! $repository::authorizedToViewAny($this)) { - throw new UnauthorizedException(__('Unauthorized to view repository :name.', [ - 'name' => $repository, - ]), 403); - } - }); - } - - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() - { - return [ - // - ]; - } + use InteractWithRepositories; } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 9e59a1fe..492b51a9 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -18,7 +18,7 @@ abstract class Repository /** * @var Model */ - public $resource; + public $modelInstance; /** * Create a new resource instance. @@ -27,7 +27,7 @@ abstract class Repository */ public function __construct($resource) { - $this->resource = $resource; + $this->modelInstance = $resource; } /** @@ -45,7 +45,7 @@ abstract public function fields(Request $request); */ public function model() { - return $this->resource; + return $this->modelInstance; } /** diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php new file mode 100644 index 00000000..6f180eb2 --- /dev/null +++ b/src/Services/Search/SearchService.php @@ -0,0 +1,327 @@ +request = $request; + $this->model = $model; + + $this->builder = $model->newQuery(); + + $this->prepare(); + + return $this->builder; + } + + /** + * Will prepare the eloquent array to return. + * + * @return array + */ + protected function prepare() + { + $this->prepareSearchFields($this->request->get('search', data_get($this->fixedInput, 'search', ''))) + ->prepareMatchFields($this->request->get('match', [])) + ->prepareIn($this->request->get('in', [])) + ->prepareOperator($this->request->get('operator', [])) + ->prepareOrders($this->request->get('sort', '')) + ->prepareRelations(); + + $results = $this->builder->get(); + + return [ + 'data' => $results, + 'aggregations' => null, + ]; + } + + /** + * Prepare eloquent exact fields. + * + * @param $fields + * + * @return $this + */ + protected function prepareIn($fields) + { + if (isset($this->fixedInput['in']) === true) { + $fields = $this->fixedInput['in']; + } + + if (is_array($fields) === true) { + foreach ($fields as $key => $value) { + $field = $key; + + if ($field === null) { + continue; + } + + if (is_array($value) === true && isset($this->model::getInFields()[$key]) === true) { + foreach ($value as $val) { + switch ($this->model::getInFields()[$key]) { + case 'integer': + default: + $this->builder->whereIn($this->model->qualifyColumn($field), explode(',', $val)); + break; + } + } + } elseif (is_array($value) === false && isset($this->model::getInFields()[$key]) === true) { + switch ($this->model::getInFields()[$key]) { + case 'integer': + default: + $this->builder->whereIn($this->model->qualifyColumn($field), explode(',', $value)); + break; + } + } + } + } + + return $this; + } + + /** + * Prepare eloquent exact fields. + * + * @param $fields + * + * @return $this + */ + protected function prepareOperator($fields) + { + if (isset($this->fixedInput['operator']) === true) { + $fields = $this->fixedInput['operator']; + } + + if (is_array($fields) === true) { + foreach ($fields as $key => $values) { + foreach ($values as $field => $value) { + $qualifiedField = $this->model->qualifyColumn($field); + switch ($key) { + case 'gte': + $this->builder->where($qualifiedField, '>=', $value); + break; + case 'gt': + $this->builder->where($qualifiedField, '>', $value); + break; + case 'lte': + $this->builder->where($qualifiedField, '<=', $value); + break; + case 'lt': + $this->builder->where($qualifiedField, '<', $value); + break; + } + } + } + } + + return $this; + } + + /** + * Prepare eloquent exact fields. + * + * @param $fields + * + * @return $this + */ + protected function prepareMatchFields($fields) + { + if (isset($this->fixedInput['match']) === true) { + if (is_array($fields) === false) { + $fields = $this->fixedInput['match']; + } else { + $fields = array_merge($this->fixedInput['match'], $fields); + } + } + + if (is_array($fields) === true) { + foreach ($fields as $key => $value) { + if (isset($this->model::getMatchByFields()[$key]) === true) { + $field = $this->model->qualifyColumn($key); + + $values = explode(',', $value); + foreach ($values as $match) { + switch ($this->model::getMatchByFields()[$key]) { + case RestifySearchable::MATCH_TEXT: + $this->builder->where($field, '=', $match); + break; + case RestifySearchable::MATCH_BOOL: + case 'boolean': + if ($match === 'false') { + $this->builder->where(function ($query) use ($field) { + return $query->where($field, '=', false)->orWhereNull($field); + }); + break; + } + $this->builder->where($field, '=', true); + break; + case RestifySearchable::MATCH_INTEGER: + case 'number': + case 'int': + $this->builder->where($field, '=', (int) $match); + break; + } + } + } + } + } + + return $this; + } + + /** + * Prepare eloquent order by. + * + * @param $sort + * + * @return $this + */ + protected function prepareOrders($sort) + { + if (isset($this->fixedInput['sort'])) { + $sort = $this->fixedInput['sort']; + } + + $params = explode(',', $sort); + + if (is_array($params) === true && empty($params) === false) { + foreach ($params as $param) { + $this->setOrder($param); + } + } + + if (empty($params) === true) { + $this->setOrder('+id'); + } + + return $this; + } + + /** + * Prepare relations. + * + * @return $this + */ + protected function prepareRelations() + { + $relations = null; + + if (isset($this->fixedInput['relations']) === true) { + $relations = $this->fixedInput['relations']; + } + + if (isset($this->fixedInput['relations']) === false) { + $relations = $this->request->get('with', null); + } + + if (empty($relations) === false) { + $foundRelations = explode(',', $relations); + foreach ($foundRelations as $relation) { + if (in_array($relation, $this->model->getWiths())) { + $this->builder->with($relation); + } + } + } + + return $this; + } + + /** + * Prepare search. + * + * @param $search + * @return $this + */ + protected function prepareSearchFields($search) + { + $this->builder->where(function (Builder $query) use ($search) { + /** + * @var RestifySearchable|Model + */ + $model = $query->getModel(); + + $connectionType = $query->getModel()->getConnection()->getDriverName(); + + $canSearchPrimaryKey = is_numeric($search) && + in_array($query->getModel()->getKeyType(), ['int', 'integer']) && + ($connectionType != 'pgsql' || $search <= PHP_INT_MAX) && + in_array($query->getModel()->getKeyName(), $model::getSearchableFields()); + + if ($canSearchPrimaryKey) { + $query->orWhere($query->getModel()->getQualifiedKeyName(), $search); + } + + $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like'; + + foreach ($this->model::getSearchableFields() as $column) { + $query->orWhere($model->qualifyColumn($column), $likeOperator, '%'.$search.'%'); + } + }); + + return $this; + } + + /** + * Set order. + * + * @param $param + * + * @return $this + */ + public function setOrder($param) + { + if ($param === 'random') { + $this->builder->inRandomOrder(); + + return $this; + } + + $order = substr($param, 0, 1); + + if ($order === '-') { + $field = substr($param, 1); + } + + if ($order === '+') { + $field = substr($param, 1); + } + + if ($order !== '-' && $order !== '+') { + $order = '+'; + $field = $param; + } + + if (in_array($field, $this->model::getOrderByFields()) === true) { + if ($order === '-') { + $this->builder->orderBy($field, 'desc'); + } + if ($order === '+') { + $this->builder->orderBy($field, 'asc'); + } + } + + if ($field === 'random') { + $this->builder->orderByRaw('RAND()'); + } + + return $this; + } +} diff --git a/src/Services/Search/Searchable.php b/src/Services/Search/Searchable.php new file mode 100644 index 00000000..35ca074c --- /dev/null +++ b/src/Services/Search/Searchable.php @@ -0,0 +1,47 @@ +fixedInput = $input; + } + + return $this; + } + + /** + * @return static + */ + public static function instance() + { + return new static; + } +} diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index a7eefbd9..9580f238 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -2,14 +2,27 @@ namespace Binaryk\LaravelRestify\Traits; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; /** + * Could be used as a trait in a model class and in a repository class. + * * @author Eduard Lupacescu */ trait AuthorizableModels { + /** + * @return static + */ + public static function newModel() + { + return new static; + } + /** * Determine if the given resource is authorizable. * @@ -20,6 +33,23 @@ public static function authorizable() return ! is_null(Gate::getPolicyFor(static::newModel())); } + /** + * Determine if the resource should be available for the given request. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizeToViewAny(Request $request) + { + if (! static::authorizable()) { + return; + } + + if (method_exists(Gate::getPolicyFor(static::newModel()), 'viewAny')) { + $this->authorizeTo($request, 'viewAny'); + } + } + /** * Determine if the resource should be available for the given request. * @@ -36,4 +66,242 @@ public static function authorizedToViewAny(Request $request) ? Gate::check('viewAny', get_class(static::newModel())) : true; } + + /** + * Determine if the current user can view the given resource or throw an exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function authorizeToView(Request $request) + { + return $this->authorizeTo($request, 'view') && $this->authorizeToViewAny($request); + } + + /** + * Determine if the current user can view the given resource. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizedToView(Request $request) + { + return $this->authorizedTo($request, 'view') && $this->authorizedToViewAny($request); + } + + /** + * Determine if the current user can create new resources or throw an exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public static function authorizeToCreate(Request $request) + { + throw_unless(static::authorizedToCreate($request), AuthorizationException::class); + } + + /** + * Determine if the current user can create new resources. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public static function authorizedToCreate(Request $request) + { + if (static::authorizable()) { + return Gate::check('create', get_class(static::newModel())); + } + + return true; + } + + /** + * Determine if the current user can update the given resource or throw an exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function authorizeToUpdate(Request $request) + { + return $this->authorizeTo($request, 'update'); + } + + /** + * Determine if the current user can update the given resource. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizedToUpdate(Request $request) + { + return $this->authorizedTo($request, 'update'); + } + + /** + * Determine if the current user can delete the given resource or throw an exception. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function authorizeToDelete(Request $request) + { + return $this->authorizeTo($request, 'delete'); + } + + /** + * Determine if the current user can delete the given resource. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizedToDelete(Request $request) + { + return $this->authorizedTo($request, 'delete'); + } + + /** + * Determine if the current user can restore the given resource. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizedToRestore(Request $request) + { + return $this->authorizedTo($request, 'restore'); + } + + /** + * Determine if the current user can force delete the given resource. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + public function authorizedToForceDelete(Request $request) + { + return $this->authorizedTo($request, 'forceDelete'); + } + + /** + * Determine if the user can add / associate models of the given type to the resource. + * + * @param Request $request + * @param \Illuminate\Database\Eloquent\Model|string $model + * @return bool + */ + public function authorizedToAdd(Request $request, $model) + { + if (! static::authorizable()) { + return true; + } + + $method = 'add'.class_basename($model); + + return method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, $this->model()) + : true; + } + + /** + * Determine if the user can attach any models of the given type to the resource. + * + * @param Request $request + * @param \Illuminate\Database\Eloquent\Model|string $model + * @return bool + */ + public function authorizedToAttachAny(Request $request, $model) + { + if (! static::authorizable()) { + return true; + } + + $method = 'attachAny'.Str::singular(class_basename($model)); + + return method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, [$this->model()]) + : true; + } + + /** + * Determine if the user can attach models of the given type to the resource. + * + * @param Request $request + * @param \Illuminate\Database\Eloquent\Model|string $model + * @return bool + */ + public function authorizedToAttach(Request $request, $model) + { + if (! static::authorizable()) { + return true; + } + + $method = 'attach'.Str::singular(class_basename($model)); + + return method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, [$this->model(), $model]) + : true; + } + + /** + * Determine if the user can detach models of the given type to the resource. + * + * @param Request $request + * @param \Illuminate\Database\Eloquent\Model|string $model + * @param string $relationship + * @return bool + */ + public function authorizedToDetach(Request $request, $model, $relationship) + { + if (! static::authorizable()) { + return true; + } + + $method = 'detach'.Str::singular(class_basename($model)); + + return method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, [$this->model(), $model]) + : true; + } + + /** + * Determine if the current user has a given ability. + * + * @param \Illuminate\Http\Request $request + * @param string $ability + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Throwable + */ + public function authorizeTo(Request $request, $ability) + { + throw_unless($this->authorizedTo($request, $ability), AuthorizationException::class); + } + + /** + * Determine if the current user can view the given resource. + * + * @param \Illuminate\Http\Request $request + * @param string $ability + * @return bool + */ + public function authorizedTo(Request $request, $ability) + { + return static::authorizable() ? Gate::check($ability, $this->determineModel()) : true; + } + + /** + * @return AuthorizableModels|Model|mixed + */ + public function determineModel() + { + return $this instanceof Model ? $this : $this->modelInstance; + } } diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php new file mode 100644 index 00000000..59002a6c --- /dev/null +++ b/src/Traits/InteractWithSearch.php @@ -0,0 +1,72 @@ + + */ +trait InteractWithSearch +{ + use AuthorizableModels; + + public static $defaultPerPage = 15; + + /** + * @return array + */ + public static function getSearchableFields() + { + return static::$search ?? []; + } + + /** + * @return array + */ + public static function getWiths() + { + return static::$withs ?? []; + } + + /** + * @return array + */ + public static function getInFields() + { + return static::$in ?? []; + } + + /** + * @return array + */ + public static function getMatchByFields() + { + return static::$match ?? []; + } + + /** + * @return array + */ + public static function getOrderByFields() + { + return static::$order ?? []; + } + + /** + * Prepare the resource for JSON serialization. + * + * @param Request $request + * @param array $fields + * @return array + */ + public function serializeForIndex(Request $request, array $fields = null) + { + return array_merge($fields ?: $this->toArray(), [ + 'authorizedToView' => $this->authorizedToView($request), + 'authorizedToCreate' => $this->authorizedToCreate($request), + 'authorizedToUpdate' => $this->authorizedToUpdate($request), + 'authorizedToDelete' => $this->authorizedToDelete($request), + ]); + } +} diff --git a/src/Traits/PerformsQueries.php b/src/Traits/PerformsQueries.php new file mode 100644 index 00000000..9dc99d84 --- /dev/null +++ b/src/Traits/PerformsQueries.php @@ -0,0 +1,46 @@ +withExceptionHandling() ->getJson('/restify-api/users'); - $response->assertJsonCount(3, 'data'); + $response->assertJsonCount(3, 'data.data'); + } + + public function test_the_rest_controller_can_paginate() + { + $this->mockUsers(50); + + $class = (new class extends RestController { + public function users() + { + return $this->respond($this->search(User::class)); + } + }); + + $response = $class->search(User::class, [ + 'match' => [ + 'id' => 1, + ], + ]); + $this->assertIsArray($class->search(User::class)); + $this->assertCount(1, $response['data']); + $this->assertEquals(count($class->users()->getData()->data->data), User::$defaultPerPage); + } + + public function test_per_page() + { + User::$defaultPerPage = 40; + $this->mockUsers(50); + + $class = (new class extends RestController { + public function users() + { + return $this->respond($this->search(User::class)); + } + }); + + $response = $class->search(User::class, [ + 'match' => [ + 'id' => 1, + ], + ]); + $this->assertIsArray($class->search(User::class)); + $this->assertCount(1, $response['data']); + $this->assertEquals(count($class->users()->getData()->data->data), 40); + User::$defaultPerPage = RestifySearchable::DEFAULT_PER_PAGE; } } diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php index 18d94355..f747abfa 100644 --- a/tests/Fixtures/User.php +++ b/tests/Fixtures/User.php @@ -3,6 +3,8 @@ namespace Binaryk\LaravelRestify\Tests\Fixtures; use Binaryk\LaravelRestify\Contracts\Passportable; +use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Binaryk\LaravelRestify\Traits\InteractWithSearch; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -12,10 +14,16 @@ /** * @author Eduard Lupacescu */ -class User extends Authenticatable implements Passportable, MustVerifyEmail +class User extends Authenticatable implements Passportable, MustVerifyEmail, RestifySearchable { use \Illuminate\Auth\MustVerifyEmail; - use Notifiable; + use Notifiable, + InteractWithSearch; + + public static $search = ['id']; + public static $match = [ + 'id' => 'int', + ]; /** * The attributes that are mass assignable. diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 023f54b5..98f86b27 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -16,6 +16,7 @@ */ abstract class IntegrationTest extends TestCase { + use InteractWithModels; /** * @var mixed */ diff --git a/tests/InteractWithModels.php b/tests/InteractWithModels.php new file mode 100644 index 00000000..970625aa --- /dev/null +++ b/tests/InteractWithModels.php @@ -0,0 +1,23 @@ + + */ +trait InteractWithModels +{ + public function mockUsers($count = 1) + { + $users = collect([]); + $i = 0; + while ($i < $count) { + $users->push(factory(User::class)->create()); + $i++; + } + + return $users; + } +} diff --git a/tests/RestControllerTest.php b/tests/RestControllerTest.php index 45eadf0c..6cac2603 100644 --- a/tests/RestControllerTest.php +++ b/tests/RestControllerTest.php @@ -5,6 +5,7 @@ use Binaryk\LaravelRestify\Controllers\RestResponse; use Binaryk\LaravelRestify\Exceptions\Guard\EntityNotFoundException; use Binaryk\LaravelRestify\Exceptions\Guard\GatePolicy; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Tests\Fixtures\User; use Binaryk\LaravelRestify\Tests\Fixtures\UserController; use Illuminate\Auth\Passwords\PasswordBroker; @@ -111,7 +112,7 @@ public function test_can_access_config_repository() public function test_can_access_request() { - $this->assertInstanceOf(Request::class, $this->controller->request()); + $this->assertInstanceOf(RestifyRequest::class, $this->controller->request()); } public function test_broker_exists()