From e304026b65d0ae13cec3f100e19a217668c6e3d9 Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Sat, 21 Dec 2019 22:51:42 +0200 Subject: [PATCH 1/4] Search service --- src/Contracts/RestifySearchable.php | 57 +++ src/Controllers/RestController.php | 48 ++- src/Controllers/RestIndexController.php | 15 + src/Exceptions/RestifyHandler.php | 2 + .../Controllers/RepositoryIndexController.php | 6 +- src/Http/Requests/RestifyRequest.php | 3 +- src/Services/Search/SearchService.php | 324 ++++++++++++++++++ src/Services/Search/Searchable.php | 42 +++ src/Traits/AuthorizableModels.php | 259 +++++++++++++- src/Traits/InteractWithSearch.php | 72 ++++ tests/Controllers/PaginationTest.php | 64 ++++ tests/Fixtures/User.php | 13 +- tests/IntegrationTest.php | 1 + tests/InteractWithModels.php | 25 ++ 14 files changed, 920 insertions(+), 11 deletions(-) create mode 100644 src/Contracts/RestifySearchable.php create mode 100644 src/Controllers/RestIndexController.php create mode 100644 src/Services/Search/SearchService.php create mode 100644 src/Services/Search/Searchable.php create mode 100644 src/Traits/InteractWithSearch.php create mode 100644 tests/Controllers/PaginationTest.php create mode 100644 tests/InteractWithModels.php diff --git a/src/Contracts/RestifySearchable.php b/src/Contracts/RestifySearchable.php new file mode 100644 index 00000000..6575d1c6 --- /dev/null +++ b/src/Contracts/RestifySearchable.php @@ -0,0 +1,57 @@ + + */ +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 function getSearchableFields(); + + /** + * @return array + */ + public function getWiths(); + + /** + * @return array + */ + public 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 function getMatchByFields(); + + /** + * @return array + */ + public function getOrderByFields(); +} diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index 02ba963a..56ca60e0 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -2,20 +2,25 @@ namespace Binaryk\LaravelRestify\Controllers; +use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Exceptions\Guard\EntityNotFoundException; use Binaryk\LaravelRestify\Exceptions\Guard\GatePolicy; +use Binaryk\LaravelRestify\Services\Search\SearchService; 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; use Illuminate\Contracts\Auth\PasswordBroker; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Password; @@ -86,8 +91,8 @@ public function config() /** * Returns a generic response to the client. * - * @param mixed $data - * @param int $httpCode + * @param mixed $data + * @param int $httpCode * * @return JsonResponse */ @@ -102,9 +107,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 +121,39 @@ 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 = []) + { + $container = Container::getInstance(); + + /** * @var SearchService $searchService */ + $searchService = $container->make(SearchService::class); + $results = $searchService + ->setPredefinedFilters($filters) + ->search($this->request(), ($modelClass instanceof Model ? $modelClass : $container->make($modelClass))); + + $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, + ]); + } + + public function index(Request $request, $model = null) + { + $data = $this->paginator($model)->getCollection()->map->serializeForIndex($this->request()); + + return $this->respond($data); + } + + /** * @param $policy * @param $objects diff --git a/src/Controllers/RestIndexController.php b/src/Controllers/RestIndexController.php new file mode 100644 index 00000000..55c5afec --- /dev/null +++ b/src/Controllers/RestIndexController.php @@ -0,0 +1,15 @@ + + */ +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..6c6536c3 100644 --- a/src/Http/Controllers/RepositoryIndexController.php +++ b/src/Http/Controllers/RepositoryIndexController.php @@ -13,8 +13,10 @@ public function handle(RestifyRequest $request) { $resource = $request->repository(); - $data = $resource::query()->get(); + $paginator = $resource::query() + ->where('id', '>', 10) + ->simplePaginate(); - return $this->respond($data); + return $this->respond($paginator); } } diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php index e2e0b1ab..68810e34 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -4,6 +4,7 @@ use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; use Binaryk\LaravelRestify\Exceptions\UnauthorizedException; +use Binaryk\LaravelRestify\Repositories\Repository; use Binaryk\LaravelRestify\Restify; use Illuminate\Foundation\Http\FormRequest; @@ -15,7 +16,7 @@ class RestifyRequest extends FormRequest /** * Get the class name of the repository being requested. * - * @return mixed + * @return Repository */ public function repository() { diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php new file mode 100644 index 00000000..943e6dc6 --- /dev/null +++ b/src/Services/Search/SearchService.php @@ -0,0 +1,324 @@ +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($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($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) { + switch ($key) { + case "gte": + $this->builder->where($field, '>=', $value); + break; + case "gt": + $this->builder->where($field, '>', $value); + break; + case "lte": + $this->builder->where($field, '<=', $value); + break; + case "lt": + $this->builder->where($field, '<', $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 = $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 ($query) use ($search) { + $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::$search); + + + 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 (isset($this->model->getOrderByFields()[$field]) === 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..43864326 --- /dev/null +++ b/src/Services/Search/Searchable.php @@ -0,0 +1,42 @@ +fixedInput = $input; + } + + return $this; + } +} diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index a7eefbd9..64d837ed 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -2,14 +2,24 @@ namespace Binaryk\LaravelRestify\Traits; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; /** * @author Eduard Lupacescu */ trait AuthorizableModels { + /** + * @return static + */ + public static function newModel() + { + return new static; + } + /** * Determine if the given resource is authorizable. * @@ -20,6 +30,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. * @@ -28,7 +55,7 @@ public static function authorizable() */ public static function authorizedToViewAny(Request $request) { - if (! static::authorizable()) { + if ( ! static::authorizable()) { return true; } @@ -36,4 +63,234 @@ 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->resource) : true; + } } diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php new file mode 100644 index 00000000..3e97fe9d --- /dev/null +++ b/src/Traits/InteractWithSearch.php @@ -0,0 +1,72 @@ + + */ +trait InteractWithSearch +{ + use AuthorizableModels; + + static $defaultPerPage = 15; + + /** + * @return array + */ + public function getSearchableFields() + { + return static::$search ?? []; + } + + /** + * @return array + */ + public function getWiths() + { + return static::$withs ?? []; + } + + /** + * @return array + */ + public function getInFields() + { + return static::$in ?? []; + } + + /** + * @return array + */ + public function getMatchByFields() + { + return static::$match ?? []; + } + /** + * @return array + */ + public 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/tests/Controllers/PaginationTest.php b/tests/Controllers/PaginationTest.php new file mode 100644 index 00000000..5a1ddfa5 --- /dev/null +++ b/tests/Controllers/PaginationTest.php @@ -0,0 +1,64 @@ + + */ +class PaginationTest extends IntegrationTest +{ + protected function setUp(): void + { + parent::setUp(); + } + + 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..361fa5a8 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,17 @@ /** * @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..c58d4047 --- /dev/null +++ b/tests/InteractWithModels.php @@ -0,0 +1,25 @@ + + */ +trait InteractWithModels +{ + public function mockUsers($count = 1) + { + $users = collect([]); + $i = 0; + while($i < $count) { + $users->push(factory(User::class)->create()); + $i++; + } + + return $users; + } + +} From f6043db0ee08925f2ceb0253cd1b64c6d937ed5f Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Sat, 21 Dec 2019 22:51:59 +0200 Subject: [PATCH 2/4] Apply fixes from StyleCI (#46) --- src/Contracts/RestifySearchable.php | 3 +-- src/Controllers/RestController.php | 3 --- src/Controllers/RestIndexController.php | 5 ----- src/Http/Middleware/BeforeEach.php | 4 +--- src/Http/Middleware/RestifyInjector.php | 2 +- src/LaravelRestifyServiceProvider.php | 8 +++---- src/Services/Search/SearchService.php | 29 ++++++++++++------------- src/Services/Search/Searchable.php | 6 +---- src/Traits/AuthorizableModels.php | 20 ++++++++--------- src/Traits/InteractWithSearch.php | 4 ++-- tests/Controllers/PaginationTest.php | 1 - tests/Fixtures/User.php | 1 - tests/InteractWithModels.php | 4 +--- 13 files changed, 35 insertions(+), 55 deletions(-) diff --git a/src/Contracts/RestifySearchable.php b/src/Contracts/RestifySearchable.php index 6575d1c6..ab42413a 100644 --- a/src/Contracts/RestifySearchable.php +++ b/src/Contracts/RestifySearchable.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; /** - * @package Binaryk\LaravelRestify\Contracts; * @author Eduard Lupacescu */ interface RestifySearchable @@ -42,7 +41,7 @@ public 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' ] + * e.g. [ 'id' => 'int' ]. * * To use this filter we have to send in query: * [ 'match' => [ 'id' => 1 ] ] diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index 56ca60e0..2f73d85c 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -20,7 +20,6 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Password; @@ -121,7 +120,6 @@ protected function response($data = null, $status = 200, array $headers = []) return $this->response; } - /** * @param $modelClass * @param array $filters @@ -153,7 +151,6 @@ public function index(Request $request, $model = null) return $this->respond($data); } - /** * @param $policy * @param $objects diff --git a/src/Controllers/RestIndexController.php b/src/Controllers/RestIndexController.php index 55c5afec..108bef6a 100644 --- a/src/Controllers/RestIndexController.php +++ b/src/Controllers/RestIndexController.php @@ -2,12 +2,7 @@ namespace Binaryk\LaravelRestify\Controllers; -use Binaryk\LaravelRestify\Tests\Fixtures\User; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\Request; - /** - * @package Binaryk\LaravelRestify\Controllers; * @author Eduard Lupacescu */ trait RestIndexController diff --git a/src/Http/Middleware/BeforeEach.php b/src/Http/Middleware/BeforeEach.php index 759dc2b9..3324c198 100644 --- a/src/Http/Middleware/BeforeEach.php +++ b/src/Http/Middleware/BeforeEach.php @@ -3,9 +3,6 @@ namespace Binaryk\LaravelRestify\Http\Middleware; use Binaryk\LaravelRestify\Events\RestifyBeforeEach; -use Binaryk\LaravelRestify\Events\RestifyServiceProviderRegistered; -use Binaryk\LaravelRestify\Restify; -use Binaryk\LaravelRestify\RestifyServiceProvider; use Closure; /** @@ -23,6 +20,7 @@ class BeforeEach public function handle($request, Closure $next) { RestifyBeforeEach::dispatch($request); + return $next($request); } } diff --git a/src/Http/Middleware/RestifyInjector.php b/src/Http/Middleware/RestifyInjector.php index 43c26af5..9d96709a 100644 --- a/src/Http/Middleware/RestifyInjector.php +++ b/src/Http/Middleware/RestifyInjector.php @@ -25,7 +25,7 @@ public function handle($request, Closure $next) $path = trim(Restify::path(), '/') ?: '/'; $isRestify = $request->is($path) || - $request->is(trim($path . '/*', '/')) || + $request->is(trim($path.'/*', '/')) || $request->is('restify-api/*'); if ($isRestify) { diff --git a/src/LaravelRestifyServiceProvider.php b/src/LaravelRestifyServiceProvider.php index cedb0432..8934cd32 100644 --- a/src/LaravelRestifyServiceProvider.php +++ b/src/LaravelRestifyServiceProvider.php @@ -48,15 +48,15 @@ public function register() protected function registerPublishing() { $this->publishes([ - __DIR__ . '/Commands/stubs/RestifyServiceProvider.stub' => app_path('Providers/RestifyServiceProvider.php'), + __DIR__.'/Commands/stubs/RestifyServiceProvider.stub' => app_path('Providers/RestifyServiceProvider.php'), ], 'restify-provider'); $this->publishes([ - __DIR__ . '/../config/config.php' => config_path('restify.php'), + __DIR__.'/../config/config.php' => config_path('restify.php'), ], 'restify-config'); - if ( ! $this->app->configurationIsCached()) { - $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'laravel-restify'); + if (! $this->app->configurationIsCached()) { + $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'laravel-restify'); } } } diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php index 943e6dc6..b0295cb2 100644 --- a/src/Services/Search/SearchService.php +++ b/src/Services/Search/SearchService.php @@ -32,7 +32,7 @@ public function search(Request $request, Model $model) } /** - * Will prepare the eloquent array to return + * Will prepare the eloquent array to return. * * @return array */ @@ -54,7 +54,7 @@ protected function prepare() } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -98,7 +98,7 @@ protected function prepareIn($fields) } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -114,16 +114,16 @@ protected function prepareOperator($fields) foreach ($fields as $key => $values) { foreach ($values as $field => $value) { switch ($key) { - case "gte": + case 'gte': $this->builder->where($field, '>=', $value); break; - case "gt": + case 'gt': $this->builder->where($field, '>', $value); break; - case "lte": + case 'lte': $this->builder->where($field, '<=', $value); break; - case "lt": + case 'lt': $this->builder->where($field, '<', $value); break; } @@ -135,7 +135,7 @@ protected function prepareOperator($fields) } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -156,7 +156,6 @@ protected function prepareMatchFields($fields) if (isset($this->model->getMatchByFields()[$key]) === true) { $field = $key; - $values = explode(',', $value); foreach ($values as $match) { switch ($this->model->getMatchByFields()[$key]) { @@ -188,7 +187,7 @@ protected function prepareMatchFields($fields) } /** - * Prepare eloquent order by + * Prepare eloquent order by. * * @param $sort * @@ -216,7 +215,7 @@ protected function prepareOrders($sort) } /** - * Prepare relations + * Prepare relations. * * @return $this */ @@ -245,7 +244,7 @@ protected function prepareRelations() } /** - * Prepare search + * Prepare search. * * @param $search * @return $this @@ -262,7 +261,6 @@ protected function prepareSearchFields($search) ($connectionType != 'pgsql' || $search <= PHP_INT_MAX) && in_array($query->getModel()->getKeyName(), $model::$search); - if ($canSearchPrimaryKey) { $query->orWhere($query->getModel()->getQualifiedKeyName(), $search); } @@ -270,7 +268,7 @@ protected function prepareSearchFields($search) $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like'; foreach ($this->model->getSearchableFields() as $column) { - $query->orWhere($model->qualifyColumn($column), $likeOperator, '%' . $search . '%'); + $query->orWhere($model->qualifyColumn($column), $likeOperator, '%'.$search.'%'); } }); @@ -278,7 +276,7 @@ protected function prepareSearchFields($search) } /** - * Set order + * Set order. * * @param $param * @@ -288,6 +286,7 @@ public function setOrder($param) { if ($param === 'random') { $this->builder->inRandomOrder(); + return $this; } diff --git a/src/Services/Search/Searchable.php b/src/Services/Search/Searchable.php index 43864326..563db912 100644 --- a/src/Services/Search/Searchable.php +++ b/src/Services/Search/Searchable.php @@ -5,9 +5,6 @@ use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Illuminate\Http\Request; -/** - * @package Binaryk\LaravelRestify\Services\Search; - */ abstract class Searchable { /** @@ -16,7 +13,7 @@ abstract class Searchable protected $request; /** - * @var RestifySearchable $model + * @var RestifySearchable */ protected $model; @@ -25,7 +22,6 @@ abstract class Searchable */ protected $fixedInput; - /** * @param $input * diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index 64d837ed..bc6a4248 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -38,7 +38,7 @@ public static function authorizable() */ public function authorizeToViewAny(Request $request) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return; } @@ -55,7 +55,7 @@ public function authorizeToViewAny(Request $request) */ public static function authorizedToViewAny(Request $request) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return true; } @@ -195,11 +195,11 @@ public function authorizedToForceDelete(Request $request) */ public function authorizedToAdd(Request $request, $model) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return true; } - $method = 'add' . class_basename($model); + $method = 'add'.class_basename($model); return method_exists(Gate::getPolicyFor($this->model()), $method) ? Gate::check($method, $this->model()) @@ -215,11 +215,11 @@ public function authorizedToAdd(Request $request, $model) */ public function authorizedToAttachAny(Request $request, $model) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return true; } - $method = 'attachAny' . Str::singular(class_basename($model)); + $method = 'attachAny'.Str::singular(class_basename($model)); return method_exists(Gate::getPolicyFor($this->model()), $method) ? Gate::check($method, [$this->model()]) @@ -235,11 +235,11 @@ public function authorizedToAttachAny(Request $request, $model) */ public function authorizedToAttach(Request $request, $model) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return true; } - $method = 'attach' . Str::singular(class_basename($model)); + $method = 'attach'.Str::singular(class_basename($model)); return method_exists(Gate::getPolicyFor($this->model()), $method) ? Gate::check($method, [$this->model(), $model]) @@ -256,11 +256,11 @@ public function authorizedToAttach(Request $request, $model) */ public function authorizedToDetach(Request $request, $model, $relationship) { - if ( ! static::authorizable()) { + if (! static::authorizable()) { return true; } - $method = 'detach' . Str::singular(class_basename($model)); + $method = 'detach'.Str::singular(class_basename($model)); return method_exists(Gate::getPolicyFor($this->model()), $method) ? Gate::check($method, [$this->model(), $model]) diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php index 3e97fe9d..c65b3c79 100644 --- a/src/Traits/InteractWithSearch.php +++ b/src/Traits/InteractWithSearch.php @@ -5,14 +5,13 @@ use Illuminate\Http\Request; /** - * @package Binaryk\LaravelRestify\Traits; * @author Eduard Lupacescu */ trait InteractWithSearch { use AuthorizableModels; - static $defaultPerPage = 15; + public static $defaultPerPage = 15; /** * @return array @@ -45,6 +44,7 @@ public function getMatchByFields() { return static::$match ?? []; } + /** * @return array */ diff --git a/tests/Controllers/PaginationTest.php b/tests/Controllers/PaginationTest.php index 5a1ddfa5..ac7da191 100644 --- a/tests/Controllers/PaginationTest.php +++ b/tests/Controllers/PaginationTest.php @@ -8,7 +8,6 @@ use Binaryk\LaravelRestify\Tests\IntegrationTest; /** - * @package Binaryk\LaravelRestify\Tests\Controllers; * @author Eduard Lupacescu */ class PaginationTest extends IntegrationTest diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php index 361fa5a8..f747abfa 100644 --- a/tests/Fixtures/User.php +++ b/tests/Fixtures/User.php @@ -20,7 +20,6 @@ class User extends Authenticatable implements Passportable, MustVerifyEmail, Res use Notifiable, InteractWithSearch; - public static $search = ['id']; public static $match = [ 'id' => 'int', diff --git a/tests/InteractWithModels.php b/tests/InteractWithModels.php index c58d4047..970625aa 100644 --- a/tests/InteractWithModels.php +++ b/tests/InteractWithModels.php @@ -5,7 +5,6 @@ use Binaryk\LaravelRestify\Tests\Fixtures\User; /** - * @package Binaryk\LaravelRestify\Tests; * @author Eduard Lupacescu */ trait InteractWithModels @@ -14,12 +13,11 @@ public function mockUsers($count = 1) { $users = collect([]); $i = 0; - while($i < $count) { + while ($i < $count) { $users->push(factory(User::class)->create()); $i++; } return $users; } - } From cc6d31e4294d3a5363561fde3b47285c9e3c986d Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Sun, 22 Dec 2019 16:19:57 +0200 Subject: [PATCH 3/4] Adding index query and make static filters --- src/Contracts/RestifySearchable.php | 10 +-- src/Controllers/RestController.php | 45 ++++++------- .../Controllers/RepositoryIndexController.php | 13 ++-- .../Requests/InteractWithRepositories.php | 67 +++++++++++++++++++ src/Http/Requests/RestifyRequest.php | 37 +--------- src/Repositories/Repository.php | 6 +- src/Services/Search/SearchService.php | 49 +++++++------- src/Services/Search/Searchable.php | 11 ++- src/Traits/AuthorizableModels.php | 13 +++- src/Traits/InteractWithSearch.php | 10 +-- src/Traits/PerformsQueries.php | 46 +++++++++++++ tests/Controllers/PaginationTest.php | 64 ------------------ .../RepositoryIndexControllerTest.php | 49 +++++++++++++- tests/RestControllerTest.php | 3 +- 14 files changed, 256 insertions(+), 167 deletions(-) create mode 100644 src/Http/Requests/InteractWithRepositories.php create mode 100644 src/Traits/PerformsQueries.php delete mode 100644 tests/Controllers/PaginationTest.php diff --git a/src/Contracts/RestifySearchable.php b/src/Contracts/RestifySearchable.php index 6575d1c6..7a36a4bb 100644 --- a/src/Contracts/RestifySearchable.php +++ b/src/Contracts/RestifySearchable.php @@ -26,17 +26,17 @@ public function serializeForIndex(Request $request, array $fields = []); /** * @return array */ - public function getSearchableFields(); + public static function getSearchableFields(); /** * @return array */ - public function getWiths(); + public static function getWiths(); /** * @return array */ - public function getInFields(); + public static function getInFields(); /** * Find matches in the table by given value @@ -48,10 +48,10 @@ public function getInFields(); * [ 'match' => [ 'id' => 1 ] ] * @return array */ - public function getMatchByFields(); + public static function getMatchByFields(); /** * @return array */ - public function getOrderByFields(); + public static function getOrderByFields(); } diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index 56ca60e0..39f2b530 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -5,7 +5,9 @@ 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; @@ -14,13 +16,10 @@ use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\PasswordBroker; use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Password; @@ -35,7 +34,7 @@ */ abstract class RestController extends BaseController { - use AuthorizesRequests, DispatchesJobs, ValidatesRequests; + use AuthorizesRequests, DispatchesJobs, ValidatesRequests, PerformsQueries; /** * @var RestResponse @@ -48,7 +47,7 @@ abstract class RestController extends BaseController protected $gate; /** - * @var Request + * @var RestifyRequest */ protected $request; @@ -63,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; @@ -81,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; @@ -95,6 +98,7 @@ public function config() * @param int $httpCode * * @return JsonResponse + * @throws BindingResolutionException */ protected function respond($data = null, $httpCode = 200) { @@ -130,30 +134,22 @@ protected function response($data = null, $status = 200, array $headers = []) */ public function search($modelClass, $filters = []) { - $container = Container::getInstance(); - - /** * @var SearchService $searchService */ - $searchService = $container->make(SearchService::class); - $results = $searchService + $results = SearchService::instance() ->setPredefinedFilters($filters) - ->search($this->request(), ($modelClass instanceof Model ? $modelClass : $container->make($modelClass))); + ->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, ]); } - public function index(Request $request, $model = null) - { - $data = $this->paginator($model)->getCollection()->map->serializeForIndex($this->request()); - - return $this->respond($data); - } - - /** * @param $policy * @param $objects @@ -210,6 +206,7 @@ public function broker() * Returns with a message. * @param $msg * @return JsonResponse + * @throws BindingResolutionException */ public function message($msg) { @@ -221,7 +218,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/Http/Controllers/RepositoryIndexController.php b/src/Http/Controllers/RepositoryIndexController.php index 6c6536c3..894733d1 100644 --- a/src/Http/Controllers/RepositoryIndexController.php +++ b/src/Http/Controllers/RepositoryIndexController.php @@ -9,14 +9,19 @@ */ 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(); - $paginator = $resource::query() - ->where('id', '>', 10) - ->simplePaginate(); + $data = $this->search($resource::newModel()); - return $this->respond($paginator); + return $this->respond($data); } } diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php new file mode 100644 index 00000000..d86a2fb1 --- /dev/null +++ b/src/Http/Requests/InteractWithRepositories.php @@ -0,0 +1,67 @@ + + */ +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 68810e34..d57f75a3 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -2,10 +2,6 @@ namespace Binaryk\LaravelRestify\Http\Requests; -use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException; -use Binaryk\LaravelRestify\Exceptions\UnauthorizedException; -use Binaryk\LaravelRestify\Repositories\Repository; -use Binaryk\LaravelRestify\Restify; use Illuminate\Foundation\Http\FormRequest; /** @@ -13,37 +9,6 @@ */ class RestifyRequest extends FormRequest { - /** - * Get the class name of the repository being requested. - * - * @return Repository - */ - 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); - } + use InteractWithRepositories; - 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/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 index 943e6dc6..d810641f 100644 --- a/src/Services/Search/SearchService.php +++ b/src/Services/Search/SearchService.php @@ -3,9 +3,9 @@ namespace Binaryk\LaravelRestify\Services\Search; use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\Request; class SearchService extends Searchable { @@ -15,11 +15,11 @@ class SearchService extends Searchable protected $builder; /** - * @param Request $request + * @param RestifyRequest $request * @param Model $model * @return Builder */ - public function search(Request $request, Model $model) + public function search(RestifyRequest $request, Model $model) { $this->request = $request; $this->model = $model; @@ -74,20 +74,20 @@ protected function prepareIn($fields) continue; } - if (is_array($value) === true && isset($this->model->getInFields()[$key]) === true) { + if (is_array($value) === true && isset($this->model::getInFields()[$key]) === true) { foreach ($value as $val) { - switch ($this->model->getInFields()[$key]) { + switch ($this->model::getInFields()[$key]) { case 'integer': default: - $this->builder->whereIn($field, explode(',', $val)); + $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]) { + } elseif (is_array($value) === false && isset($this->model::getInFields()[$key]) === true) { + switch ($this->model::getInFields()[$key]) { case 'integer': default: - $this->builder->whereIn($field, explode(',', $value)); + $this->builder->whereIn($this->model->qualifyColumn($field), explode(',', $value)); break; } } @@ -113,18 +113,19 @@ protected function prepareOperator($fields) 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($field, '>=', $value); + $this->builder->where($qualifiedField, '>=', $value); break; case "gt": - $this->builder->where($field, '>', $value); + $this->builder->where($qualifiedField, '>', $value); break; case "lte": - $this->builder->where($field, '<=', $value); + $this->builder->where($qualifiedField, '<=', $value); break; case "lt": - $this->builder->where($field, '<', $value); + $this->builder->where($qualifiedField, '<', $value); break; } } @@ -153,13 +154,12 @@ protected function prepareMatchFields($fields) if (is_array($fields) === true) { foreach ($fields as $key => $value) { - if (isset($this->model->getMatchByFields()[$key]) === true) { - $field = $key; - + if (isset($this->model::getMatchByFields()[$key]) === true) { + $field = $this->model->qualifyColumn($key); $values = explode(',', $value); foreach ($values as $match) { - switch ($this->model->getMatchByFields()[$key]) { + switch ($this->model::getMatchByFields()[$key]) { case RestifySearchable::MATCH_TEXT: $this->builder->where($field, '=', $match); break; @@ -252,7 +252,10 @@ protected function prepareRelations() */ protected function prepareSearchFields($search) { - $this->builder->where(function ($query) use ($search) { + $this->builder->where(function (Builder $query) use ($search) { + /** + * @var RestifySearchable|Model $model + */ $model = $query->getModel(); $connectionType = $query->getModel()->getConnection()->getDriverName(); @@ -260,7 +263,7 @@ protected function prepareSearchFields($search) $canSearchPrimaryKey = is_numeric($search) && in_array($query->getModel()->getKeyType(), ['int', 'integer']) && ($connectionType != 'pgsql' || $search <= PHP_INT_MAX) && - in_array($query->getModel()->getKeyName(), $model::$search); + in_array($query->getModel()->getKeyName(), $model::getSearchableFields()); if ($canSearchPrimaryKey) { @@ -269,7 +272,7 @@ protected function prepareSearchFields($search) $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like'; - foreach ($this->model->getSearchableFields() as $column) { + foreach ($this->model::getSearchableFields() as $column) { $query->orWhere($model->qualifyColumn($column), $likeOperator, '%' . $search . '%'); } }); @@ -306,11 +309,11 @@ public function setOrder($param) $field = $param; } - if (isset($this->model->getOrderByFields()[$field]) === true) { - if ($order === '+') { + if (in_array($field, $this->model::getOrderByFields()) === true) { + if ($order === '-') { $this->builder->orderBy($field, 'desc'); } - if ($order === '-') { + if ($order === '+') { $this->builder->orderBy($field, 'asc'); } } diff --git a/src/Services/Search/Searchable.php b/src/Services/Search/Searchable.php index 43864326..87e880b6 100644 --- a/src/Services/Search/Searchable.php +++ b/src/Services/Search/Searchable.php @@ -3,6 +3,7 @@ namespace Binaryk\LaravelRestify\Services\Search; use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; /** @@ -16,7 +17,7 @@ abstract class Searchable protected $request; /** - * @var RestifySearchable $model + * @var RestifySearchable|Model $model */ protected $model; @@ -39,4 +40,12 @@ public function setPredefinedFilters($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 64d837ed..4e9056a1 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -3,11 +3,14 @@ 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 @@ -291,6 +294,14 @@ public function authorizeTo(Request $request, $ability) */ public function authorizedTo(Request $request, $ability) { - return static::authorizable() ? Gate::check($ability, $this->resource) : true; + 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 index 3e97fe9d..c8c43250 100644 --- a/src/Traits/InteractWithSearch.php +++ b/src/Traits/InteractWithSearch.php @@ -17,7 +17,7 @@ trait InteractWithSearch /** * @return array */ - public function getSearchableFields() + public static function getSearchableFields() { return static::$search ?? []; } @@ -25,7 +25,7 @@ public function getSearchableFields() /** * @return array */ - public function getWiths() + public static function getWiths() { return static::$withs ?? []; } @@ -33,7 +33,7 @@ public function getWiths() /** * @return array */ - public function getInFields() + public static function getInFields() { return static::$in ?? []; } @@ -41,14 +41,14 @@ public function getInFields() /** * @return array */ - public function getMatchByFields() + public static function getMatchByFields() { return static::$match ?? []; } /** * @return array */ - public function getOrderByFields() + public static function getOrderByFields() { return static::$order ?? []; } 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 @@ + - */ -class PaginationTest extends IntegrationTest -{ - protected function setUp(): void - { - parent::setUp(); - } - - 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/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index 3ed42d6f..36d97c1b 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -2,6 +2,8 @@ namespace Binaryk\LaravelRestify\Tests\Controllers; +use Binaryk\LaravelRestify\Contracts\RestifySearchable; +use Binaryk\LaravelRestify\Controllers\RestController; use Binaryk\LaravelRestify\Tests\Fixtures\User; use Binaryk\LaravelRestify\Tests\IntegrationTest; @@ -19,6 +21,51 @@ public function test_list_resource() $response = $this->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/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() From 16740c326f40ac486cef7c8cdc13c3fce0d61bc1 Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Sun, 22 Dec 2019 16:20:43 +0200 Subject: [PATCH 4/4] Apply fixes from StyleCI (#48) --- src/Controllers/RestController.php | 2 -- .../Requests/InteractWithRepositories.php | 3 +- src/Http/Requests/RestifyRequest.php | 1 - src/Services/Search/SearchService.php | 30 +++++++++---------- src/Services/Search/Searchable.php | 6 +--- src/Traits/AuthorizableModels.php | 4 +-- .../RepositoryIndexControllerTest.php | 1 - 7 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/Controllers/RestController.php b/src/Controllers/RestController.php index 39f2b530..eae58dfc 100644 --- a/src/Controllers/RestController.php +++ b/src/Controllers/RestController.php @@ -125,7 +125,6 @@ protected function response($data = null, $status = 200, array $headers = []) return $this->response; } - /** * @param $modelClass * @param array $filters @@ -144,7 +143,6 @@ public function search($modelClass, $filters = []) $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, ]); diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php index d86a2fb1..16482797 100644 --- a/src/Http/Requests/InteractWithRepositories.php +++ b/src/Http/Requests/InteractWithRepositories.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Model; /** - * @package Binaryk\LaravelRestify\Http\Requests; * @author Eduard Lupacescu */ trait InteractWithRepositories @@ -45,7 +44,7 @@ public function repository() ]), 404); } - if ( ! $repository::authorizedToViewAny($this)) { + if (! $repository::authorizedToViewAny($this)) { throw new UnauthorizedException(__('Unauthorized to view repository :name.', [ 'name' => $repository, ]), 403); diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php index d57f75a3..711031f4 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -10,5 +10,4 @@ class RestifyRequest extends FormRequest { use InteractWithRepositories; - } diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php index d810641f..6f180eb2 100644 --- a/src/Services/Search/SearchService.php +++ b/src/Services/Search/SearchService.php @@ -32,7 +32,7 @@ public function search(RestifyRequest $request, Model $model) } /** - * Will prepare the eloquent array to return + * Will prepare the eloquent array to return. * * @return array */ @@ -54,7 +54,7 @@ protected function prepare() } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -98,7 +98,7 @@ protected function prepareIn($fields) } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -115,16 +115,16 @@ protected function prepareOperator($fields) foreach ($values as $field => $value) { $qualifiedField = $this->model->qualifyColumn($field); switch ($key) { - case "gte": + case 'gte': $this->builder->where($qualifiedField, '>=', $value); break; - case "gt": + case 'gt': $this->builder->where($qualifiedField, '>', $value); break; - case "lte": + case 'lte': $this->builder->where($qualifiedField, '<=', $value); break; - case "lt": + case 'lt': $this->builder->where($qualifiedField, '<', $value); break; } @@ -136,7 +136,7 @@ protected function prepareOperator($fields) } /** - * Prepare eloquent exact fields + * Prepare eloquent exact fields. * * @param $fields * @@ -188,7 +188,7 @@ protected function prepareMatchFields($fields) } /** - * Prepare eloquent order by + * Prepare eloquent order by. * * @param $sort * @@ -216,7 +216,7 @@ protected function prepareOrders($sort) } /** - * Prepare relations + * Prepare relations. * * @return $this */ @@ -245,7 +245,7 @@ protected function prepareRelations() } /** - * Prepare search + * Prepare search. * * @param $search * @return $this @@ -254,7 +254,7 @@ protected function prepareSearchFields($search) { $this->builder->where(function (Builder $query) use ($search) { /** - * @var RestifySearchable|Model $model + * @var RestifySearchable|Model */ $model = $query->getModel(); @@ -265,7 +265,6 @@ protected function prepareSearchFields($search) ($connectionType != 'pgsql' || $search <= PHP_INT_MAX) && in_array($query->getModel()->getKeyName(), $model::getSearchableFields()); - if ($canSearchPrimaryKey) { $query->orWhere($query->getModel()->getQualifiedKeyName(), $search); } @@ -273,7 +272,7 @@ protected function prepareSearchFields($search) $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like'; foreach ($this->model::getSearchableFields() as $column) { - $query->orWhere($model->qualifyColumn($column), $likeOperator, '%' . $search . '%'); + $query->orWhere($model->qualifyColumn($column), $likeOperator, '%'.$search.'%'); } }); @@ -281,7 +280,7 @@ protected function prepareSearchFields($search) } /** - * Set order + * Set order. * * @param $param * @@ -291,6 +290,7 @@ public function setOrder($param) { if ($param === 'random') { $this->builder->inRandomOrder(); + return $this; } diff --git a/src/Services/Search/Searchable.php b/src/Services/Search/Searchable.php index 87e880b6..35ca074c 100644 --- a/src/Services/Search/Searchable.php +++ b/src/Services/Search/Searchable.php @@ -6,9 +6,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; -/** - * @package Binaryk\LaravelRestify\Services\Search; - */ abstract class Searchable { /** @@ -17,7 +14,7 @@ abstract class Searchable protected $request; /** - * @var RestifySearchable|Model $model + * @var RestifySearchable|Model */ protected $model; @@ -26,7 +23,6 @@ abstract class Searchable */ protected $fixedInput; - /** * @param $input * diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index 319ee36b..9580f238 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -9,8 +9,8 @@ use Illuminate\Support\Str; /** - * Could be used as a trait in a model class and in a repository class - * + * Could be used as a trait in a model class and in a repository class. + * * @author Eduard Lupacescu */ trait AuthorizableModels diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index 36d97c1b..3a08ad22 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -24,7 +24,6 @@ public function test_list_resource() $response->assertJsonCount(3, 'data.data'); } - public function test_the_rest_controller_can_paginate() { $this->mockUsers(50);