├── .env.example ├── .env.travis ├── .gitignore ├── .travis.yml ├── app ├── Console │ ├── Commands │ │ └── .gitkeep │ └── Kernel.php ├── Events │ ├── Event.php │ ├── ExampleEvent.php │ └── UserEvents │ │ └── UserCreatedEvent.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── AccessTokenController.php │ │ ├── Controller.php │ │ ├── ResponseTrait.php │ │ └── UserController.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── ExampleMiddleware.php │ │ └── ThrottleRequests.php ├── Jobs │ ├── ExampleJob.php │ └── Job.php ├── Listeners │ ├── ExampleListener.php │ └── UserEventsListener.php ├── Mails │ └── WelcomeEmail.php ├── Models │ └── User.php ├── Policies │ └── UserPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ └── RepositoriesServiceProvider.php ├── Repositories │ ├── AbstractEloquentRepository.php │ ├── Contracts │ │ ├── BaseRepository.php │ │ └── UserRepository.php │ └── EloquentUserRepository.php └── Transformers │ └── UserTransformer.php ├── artisan ├── bootstrap └── app.php ├── composer.json ├── composer.lock ├── config ├── auth.php ├── cors.php ├── database.php └── mail.php ├── database ├── factories │ ├── ModelFactory.php │ └── UserFactory.php ├── migrations │ ├── .gitkeep │ └── 2017_03_01_155453_create_users_table.php └── seeds │ ├── DatabaseSeeder.php │ └── UsersTableSeeder.php ├── develop ├── docker-compose.yml ├── docker ├── Dockerfile ├── default ├── php-fpm.conf ├── start-container ├── supervisord.conf └── xdebug.ini ├── phpunit.xml ├── public ├── .htaccess ├── images │ └── accessTokenCreation.png └── index.php ├── readme.md ├── resources └── views │ ├── .gitkeep │ └── emails │ └── welcome.blade.php ├── routes └── web.php ├── storage ├── app │ └── .gitignore ├── framework │ ├── cache │ │ └── .gitignore │ └── views │ │ └── .gitignore ├── logs │ └── .gitignore ├── oauth-private.key └── oauth-public.key └── tests ├── Endpoints └── UsersTest.php ├── ExampleTest.php ├── Http └── Middleware │ └── ThrottleRequestsTest.php ├── Repositories └── EloquentUserRepositoryTest.php └── TestCase.php /.env.example: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_DEBUG=true 3 | APP_KEY=Gfl7u2OcTQjPIPDMoi4ckoS4jTpGXqlE 4 | APP_TIMEZONE=UTC 5 | 6 | DB_CONNECTION=mysql 7 | DB_HOST=127.0.0.1 8 | DB_PORT=3306 9 | DB_DATABASE=restapi 10 | DB_USERNAME=homestead 11 | DB_PASSWORD=secret 12 | 13 | DB_TEST_DATABASE=restapi_test 14 | DB_TEST_USERNAME=homestead 15 | DB_TEST_PASSWORD=secret 16 | 17 | CACHE_DRIVER=file 18 | QUEUE_DRIVER=sync 19 | REDIS_HOST=restapi-redis 20 | 21 | MAIL_DRIVER=smtp 22 | MAIL_HOST=mailtrap.io 23 | MAIL_PORT=2525 24 | MAIL_FROM_ADDRESS=info@yourfrom.com 25 | MAIL_FROM_NAME=YourFrom 26 | MAIL_USERNAME=yourusername 27 | MAIL_PASSWORD=yourpassword 28 | MAIL_ENCRYPTION=tls 29 | -------------------------------------------------------------------------------- /.env.travis: -------------------------------------------------------------------------------- 1 | APP_ENV=testing 2 | APP_KEY=Gfl7u2OcTQjPIPDMoi4ckoS4jTpGXqlE 3 | 4 | DB_CONNECTION=testing 5 | DB_TEST_USERNAME=root 6 | DB_TEST_PASSWORD= 7 | 8 | CACHE_DRIVER=array 9 | SESSION_DRIVER=array 10 | QUEUE_DRIVER=sync -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Homestead.json 2 | Homestead.yaml 3 | .env 4 | /.vagrant/ 5 | /vendor/ 6 | /.idea/ 7 | /storage/*.key 8 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | services: 4 | - mysql 5 | php: 6 | - 7.2 7 | - 7.1 8 | - 7.0 9 | - hhvm 10 | 11 | before_script: 12 | - cp .env.travis .env 13 | - mysql -e 'CREATE DATABASE restapi_test;' 14 | - composer self-update 15 | - composer install --no-interaction 16 | 17 | script: 18 | - vendor/bin/phpunit -------------------------------------------------------------------------------- /app/Console/Commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasib32/rest-api-with-lumen/18a8dc72f54c69ffa38c45352d5d303d313e0352/app/Console/Commands/.gitkeep -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | user = $user; 23 | } 24 | } -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | json((['status' => 403, 'message' => 'Insufficient privileges to perform this action']), 403); 52 | } 53 | 54 | if ($e instanceof MethodNotAllowedHttpException) { 55 | return response()->json((['status' => 405, 'message' => 'Method Not Allowed']), 405); 56 | } 57 | 58 | if ($e instanceof NotFoundHttpException) { 59 | return response()->json((['status' => 404, 'message' => 'The requested resource was not found']), 404); 60 | } 61 | 62 | return parent::render($request, $e); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Http/Controllers/AccessTokenController.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 26 | 27 | parent::__construct(); 28 | } 29 | 30 | /** 31 | * Since, with Laravel|Lumen passport doesn't restrict 32 | * a client requesting any scope. we have to restrict it. 33 | * http://stackoverflow.com/questions/39436509/laravel-passport-scopes 34 | * 35 | * @param Request $request 36 | * @return \Illuminate\Http\Response 37 | */ 38 | public function createAccessToken(Request $request) 39 | { 40 | $inputs = $request->all(); 41 | 42 | //Set default scope with full access 43 | if (!isset($inputs['scope']) || empty($inputs['scope'])) { 44 | $inputs['scope'] = "*"; 45 | } 46 | 47 | $tokenRequest = $request->create('/oauth/token', 'post', $inputs); 48 | 49 | // forward the request to the oauth token request endpoint 50 | return app()->dispatch($tokenRequest); 51 | } 52 | } -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | setFractal($fractal); 22 | } 23 | 24 | /** 25 | * Validate HTTP request against the rules 26 | * 27 | * @param Request $request 28 | * @param array $rules 29 | * @return bool|array 30 | */ 31 | protected function validateRequest(Request $request, array $rules) 32 | { 33 | // Perform Validation 34 | $validator = \Validator::make($request->all(), $rules); 35 | 36 | if ($validator->fails()) { 37 | $errorMessages = $validator->errors()->messages(); 38 | 39 | // crete error message by using key and value 40 | foreach ($errorMessages as $key => $value) { 41 | $errorMessages[$key] = $value[0]; 42 | } 43 | 44 | return $errorMessages; 45 | } 46 | 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Controllers/ResponseTrait.php: -------------------------------------------------------------------------------- 1 | fractal = $fractal; 38 | } 39 | 40 | /** 41 | * Getter for statusCode 42 | * 43 | * @return mixed 44 | */ 45 | public function getStatusCode() 46 | { 47 | return $this->statusCode; 48 | } 49 | 50 | /** 51 | * Setter for statusCode 52 | * 53 | * @param int $statusCode Value to set 54 | * 55 | * @return self 56 | */ 57 | public function setStatusCode($statusCode) 58 | { 59 | $this->statusCode = $statusCode; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Send custom data response 66 | * 67 | * @param $status 68 | * @param $message 69 | * @return \Illuminate\Http\JsonResponse 70 | */ 71 | public function sendCustomResponse($status, $message) 72 | { 73 | return response()->json(['status' => $status, 'message' => $message], $status); 74 | } 75 | 76 | /** 77 | * Send this response when api user provide fields that doesn't exist in our application 78 | * 79 | * @param $errors 80 | * @return mixed 81 | */ 82 | public function sendUnknownFieldResponse($errors) 83 | { 84 | return response()->json((['status' => 400, 'unknown_fields' => $errors]), 400); 85 | } 86 | 87 | /** 88 | * Send this response when api user provide filter that doesn't exist in our application 89 | * 90 | * @param $errors 91 | * @return mixed 92 | */ 93 | public function sendInvalidFilterResponse($errors) 94 | { 95 | return response()->json((['status' => 400, 'invalid_filters' => $errors]), 400); 96 | } 97 | 98 | /** 99 | * Send this response when api user provide incorrect data type for the field 100 | * 101 | * @param $errors 102 | * @return mixed 103 | */ 104 | public function sendInvalidFieldResponse($errors) 105 | { 106 | return response()->json((['status' => 400, 'invalid_fields' => $errors]), 400); 107 | } 108 | 109 | /** 110 | * Send this response when a api user try access a resource that they don't belong 111 | * 112 | * @return string 113 | */ 114 | public function sendForbiddenResponse() 115 | { 116 | return response()->json(['status' => 403, 'message' => 'Forbidden'], 403); 117 | } 118 | 119 | /** 120 | * Send 404 not found response 121 | * 122 | * @param string $message 123 | * @return string 124 | */ 125 | public function sendNotFoundResponse($message = '') 126 | { 127 | if ($message === '') { 128 | $message = 'The requested resource was not found'; 129 | } 130 | 131 | return response()->json(['status' => 404, 'message' => $message], 404); 132 | } 133 | 134 | /** 135 | * Send empty data response 136 | * 137 | * @return string 138 | */ 139 | public function sendEmptyDataResponse() 140 | { 141 | return response()->json(['data' => new \StdClass()]); 142 | } 143 | 144 | /** 145 | * Return collection response from the application 146 | * 147 | * @param array|LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection $collection 148 | * @param \Closure|TransformerAbstract $callback 149 | * @return \Illuminate\Http\JsonResponse 150 | */ 151 | protected function respondWithCollection($collection, $callback) 152 | { 153 | $resource = new Collection($collection, $callback); 154 | 155 | //set empty data pagination 156 | if (empty($collection)) { 157 | $collection = new \Illuminate\Pagination\LengthAwarePaginator([], 0, 10); 158 | $resource = new Collection($collection, $callback); 159 | } 160 | $resource->setPaginator(new IlluminatePaginatorAdapter($collection)); 161 | 162 | $rootScope = $this->fractal->createData($resource); 163 | 164 | return $this->respondWithArray($rootScope->toArray()); 165 | } 166 | 167 | /** 168 | * Return single item response from the application 169 | * 170 | * @param Model $item 171 | * @param \Closure|TransformerAbstract $callback 172 | * @return \Illuminate\Http\JsonResponse 173 | */ 174 | protected function respondWithItem($item, $callback) 175 | { 176 | $resource = new Item($item, $callback); 177 | $rootScope = $this->fractal->createData($resource); 178 | 179 | return $this->respondWithArray($rootScope->toArray()); 180 | } 181 | 182 | /** 183 | * Return a json response from the application 184 | * 185 | * @param array $array 186 | * @param array $headers 187 | * @return \Illuminate\Http\JsonResponse 188 | */ 189 | protected function respondWithArray(array $array, array $headers = []) 190 | { 191 | return response()->json($array, $this->statusCode, $headers); 192 | } 193 | } -------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 35 | $this->userTransformer = $userTransformer; 36 | 37 | parent::__construct(); 38 | } 39 | 40 | /** 41 | * Display a listing of the resource. 42 | * 43 | * @param Request $request 44 | * @return \Illuminate\Http\JsonResponse 45 | */ 46 | public function index(Request $request) 47 | { 48 | $users = $this->userRepository->findBy($request->all()); 49 | 50 | return $this->respondWithCollection($users, $this->userTransformer); 51 | } 52 | 53 | /** 54 | * Display the specified resource. 55 | * 56 | * @param $id 57 | * @return \Illuminate\Http\JsonResponse|string 58 | */ 59 | public function show($id) 60 | { 61 | $user = $this->userRepository->findOne($id); 62 | 63 | if (!$user instanceof User) { 64 | return $this->sendNotFoundResponse("The user with id {$id} doesn't exist"); 65 | } 66 | 67 | // Authorization 68 | $this->authorize('show', $user); 69 | 70 | return $this->respondWithItem($user, $this->userTransformer); 71 | } 72 | 73 | /** 74 | * Store a newly created resource in storage. 75 | * 76 | * @param Request $request 77 | * @return \Illuminate\Http\JsonResponse|string 78 | */ 79 | public function store(Request $request) 80 | { 81 | // Validation 82 | $validatorResponse = $this->validateRequest($request, $this->storeRequestValidationRules($request)); 83 | 84 | // Send failed response if validation fails 85 | if ($validatorResponse !== true) { 86 | return $this->sendInvalidFieldResponse($validatorResponse); 87 | } 88 | 89 | $user = $this->userRepository->save($request->all()); 90 | 91 | if (!$user instanceof User) { 92 | return $this->sendCustomResponse(500, 'Error occurred on creating User'); 93 | } 94 | 95 | return $this->setStatusCode(201)->respondWithItem($user, $this->userTransformer); 96 | } 97 | 98 | /** 99 | * Update the specified resource in storage. 100 | * 101 | * @param Request $request 102 | * @param $id 103 | * @return \Illuminate\Http\JsonResponse 104 | */ 105 | public function update(Request $request, $id) 106 | { 107 | // Validation 108 | $validatorResponse = $this->validateRequest($request, $this->updateRequestValidationRules($request)); 109 | 110 | // Send failed response if validation fails 111 | if ($validatorResponse !== true) { 112 | return $this->sendInvalidFieldResponse($validatorResponse); 113 | } 114 | 115 | $user = $this->userRepository->findOne($id); 116 | 117 | if (!$user instanceof User) { 118 | return $this->sendNotFoundResponse("The user with id {$id} doesn't exist"); 119 | } 120 | 121 | // Authorization 122 | $this->authorize('update', $user); 123 | 124 | 125 | $user = $this->userRepository->update($user, $request->all()); 126 | 127 | return $this->respondWithItem($user, $this->userTransformer); 128 | } 129 | 130 | /** 131 | * Remove the specified resource from storage. 132 | * 133 | * @param $id 134 | * @return \Illuminate\Http\JsonResponse|string 135 | */ 136 | public function destroy($id) 137 | { 138 | $user = $this->userRepository->findOne($id); 139 | 140 | if (!$user instanceof User) { 141 | return $this->sendNotFoundResponse("The user with id {$id} doesn't exist"); 142 | } 143 | 144 | // Authorization 145 | $this->authorize('destroy', $user); 146 | 147 | $this->userRepository->delete($user); 148 | 149 | return response()->json(null, 204); 150 | } 151 | 152 | /** 153 | * Store Request Validation Rules 154 | * 155 | * @param Request $request 156 | * @return array 157 | */ 158 | private function storeRequestValidationRules(Request $request) 159 | { 160 | $rules = [ 161 | 'email' => 'email|required|unique:users', 162 | 'firstName' => 'required|max:100', 163 | 'middleName' => 'max:50', 164 | 'lastName' => 'required|max:100', 165 | 'username' => 'max:50', 166 | 'address' => 'max:255', 167 | 'zipCode' => 'max:10', 168 | 'phone' => 'max:20', 169 | 'mobile' => 'max:20', 170 | 'city' => 'max:100', 171 | 'state' => 'max:100', 172 | 'country' => 'max:100', 173 | 'password' => 'min:5' 174 | ]; 175 | 176 | $requestUser = $request->user(); 177 | 178 | // Only admin user can set admin role. 179 | if ($requestUser instanceof User && $requestUser->role === User::ADMIN_ROLE) { 180 | $rules['role'] = 'in:BASIC_USER,ADMIN_USER'; 181 | } else { 182 | $rules['role'] = 'in:BASIC_USER'; 183 | } 184 | 185 | return $rules; 186 | } 187 | 188 | /** 189 | * Update Request validation Rules 190 | * 191 | * @param Request $request 192 | * @return array 193 | */ 194 | private function updateRequestValidationRules(Request $request) 195 | { 196 | $userId = $request->segment(2); 197 | $rules = [ 198 | 'email' => 'email|unique:users,email,'. $userId, 199 | 'firstName' => 'max:100', 200 | 'middleName' => 'max:50', 201 | 'lastName' => 'max:100', 202 | 'username' => 'max:50', 203 | 'address' => 'max:255', 204 | 'zipCode' => 'max:10', 205 | 'phone' => 'max:20', 206 | 'mobile' => 'max:20', 207 | 'city' => 'max:100', 208 | 'state' => 'max:100', 209 | 'country' => 'max:100', 210 | 'password' => 'min:5' 211 | ]; 212 | 213 | $requestUser = $request->user(); 214 | 215 | // Only admin user can update admin role. 216 | if ($requestUser instanceof User && $requestUser->role === User::ADMIN_ROLE) { 217 | $rules['role'] = 'in:BASIC_USER,ADMIN_USER'; 218 | } else { 219 | $rules['role'] = 'in:BASIC_USER'; 220 | } 221 | 222 | return $rules; 223 | } 224 | } -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 28 | } 29 | 30 | /** 31 | * Handle an incoming request. 32 | * 33 | * @param \Illuminate\Http\Request $request 34 | * @param \Closure $next 35 | * @param string|null $guard 36 | * @return mixed 37 | */ 38 | public function handle($request, Closure $next, $guard = null) 39 | { 40 | // First, check if the access_token created by the password grant is valid 41 | if ($this->auth->guard($guard)->guest()) { 42 | 43 | // Then check, access_token created by the client_credentials grant is valid. 44 | // We need this checking because we could use either password grant or client_credentials grant. 45 | try { 46 | app(CheckClientCredentials::class)->handle($request, function(){}); 47 | } catch (AuthenticationException $e) { 48 | return response()->json((['status' => 401, 'message' => 'Unauthorized']), 401); 49 | } 50 | } 51 | 52 | return $next($request); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Middleware/ExampleMiddleware.php: -------------------------------------------------------------------------------- 1 | limiter = $limiter; 29 | } 30 | 31 | /** 32 | * Handle an incoming request. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @param \Closure $next 36 | * @param int $maxAttempts 37 | * @param int $decayMinutes 38 | * 39 | * @return mixed 40 | */ 41 | public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1) 42 | { 43 | $key = $this->resolveRequestSignature($request); 44 | 45 | if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) { 46 | return $this->buildResponse($key, $maxAttempts); 47 | } 48 | 49 | $this->limiter->hit($key, $decayMinutes); 50 | 51 | $response = $next($request); 52 | 53 | return $this->addHeaders( 54 | $response, 55 | $maxAttempts, 56 | $this->calculateRemainingAttempts($key, $maxAttempts) 57 | ); 58 | } 59 | 60 | /** 61 | * Resolve request signature. 62 | * 63 | * @param \Illuminate\Http\Request $request 64 | * 65 | * @return string 66 | */ 67 | protected function resolveRequestSignature($request) 68 | { 69 | return sha1( 70 | $request->method() . 71 | '|' . $request->getHost() . 72 | '|' . $request->ip() 73 | ); 74 | } 75 | 76 | /** 77 | * Create a 'too many attempts' response. 78 | * 79 | * @param string $key 80 | * @param int $maxAttempts 81 | * 82 | * @return \Illuminate\Http\Response 83 | */ 84 | protected function buildResponse($key, $maxAttempts) 85 | { 86 | $response = new JsonResponse(['status' => 429, 'message' => 'Too Many Attempts.'], 429); 87 | 88 | return $this->addHeaders( 89 | $response, 90 | $maxAttempts, 91 | $this->calculateRemainingAttempts($key, $maxAttempts), 92 | $this->limiter->availableIn($key) 93 | ); 94 | } 95 | 96 | /** 97 | * Add the limit header information to the given response. 98 | * 99 | * @param \Illuminate\Http\Response $response 100 | * @param int $maxAttempts 101 | * @param int $remainingAttempts 102 | * @param int|null $retryAfter 103 | * 104 | * @return \Illuminate\Http\Response 105 | */ 106 | protected function addHeaders(\Symfony\Component\HttpFoundation\Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null) 107 | { 108 | $headers = [ 109 | 'X-RateLimit-Limit' => $maxAttempts, 110 | 'X-RateLimit-Remaining' => $remainingAttempts, 111 | ]; 112 | 113 | if (!is_null($retryAfter)) { 114 | $headers['Retry-After'] = $retryAfter; 115 | } 116 | 117 | $response->headers->add($headers); 118 | 119 | return $response; 120 | } 121 | 122 | /** 123 | * Calculate the number of remaining attempts. 124 | * 125 | * @param string $key 126 | * @param int $maxAttempts 127 | * 128 | * @return int 129 | */ 130 | protected function calculateRemainingAttempts($key, $maxAttempts) 131 | { 132 | return $this->limiter->retriesLeft($key, $maxAttempts); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/Jobs/ExampleJob.php: -------------------------------------------------------------------------------- 1 | user; 20 | 21 | //send welcome email to the user 22 | Mail::to($user)->send(new WelcomeEmail($user)); 23 | } 24 | 25 | /** 26 | * Register the listeners for the subscriber. 27 | * 28 | * @param Dispatcher $events 29 | */ 30 | public function subscribe($events) 31 | { 32 | $events->listen( 33 | UserCreatedEvent::class, 34 | 'App\Listeners\UserEventsListener@onUserCreatedEvent' 35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /app/Mails/WelcomeEmail.php: -------------------------------------------------------------------------------- 1 | user = $user; 29 | } 30 | 31 | /** 32 | * Build the message. 33 | * 34 | * @return $this 35 | */ 36 | public function build() 37 | { 38 | return $this->view('emails.welcome'); 39 | } 40 | } -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | role) ? $this->role : self::BASIC_ROLE) == self::ADMIN_ROLE; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Policies/UserPolicy.php: -------------------------------------------------------------------------------- 1 | isAdmin() && (!$currentUser->tokenCan('basic') || $currentUser->tokenCan('undefined'))) { 20 | return true; 21 | } 22 | } 23 | 24 | /** 25 | * Determine if a given user has permission to show 26 | * 27 | * @param User $currentUser 28 | * @param User $user 29 | * @return bool 30 | */ 31 | public function show(User $currentUser, User $user) 32 | { 33 | return $currentUser->id === $user->id; 34 | } 35 | 36 | /** 37 | * Determine if a given user can update 38 | * 39 | * @param User $currentUser 40 | * @param User $user 41 | * @return bool 42 | */ 43 | public function update(User $currentUser, User $user) 44 | { 45 | return $currentUser->id === $user->id; 46 | } 47 | 48 | /** 49 | * Determine if a given user can delete 50 | * 51 | * @param User $currentUser 52 | * @param User $user 53 | * @return bool 54 | */ 55 | public function destroy(User $currentUser, User $user) 56 | { 57 | return $currentUser->id === $user->id; 58 | } 59 | } -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['auth']->viaRequest('api', function ($request) { 36 | if ($request->input('api_token')) { 37 | return User::where('api_token', $request->input('api_token'))->first(); 38 | } 39 | }); 40 | 41 | Passport::tokensCan([ 42 | 'admin' => 'Admin user scope', 43 | 'basic' => 'Basic user scope', 44 | 'users' => 'Users scope', 45 | 'users:list' => 'Users scope', 46 | 'users:read' => 'Users scope for reading records', 47 | 'users:write' => 'Users scope for writing records', 48 | 'users:create' => 'Users scope for creating records', 49 | 'users:delete' => 'Users scope for deleting records', 50 | ]); 51 | 52 | //Register all policies here 53 | Gate::policy(User::class, UserPolicy::class); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App\Listeners\EventListener', 18 | ], 19 | ]; 20 | 21 | /** 22 | * The subscriber classes to register. 23 | * 24 | * @var array 25 | */ 26 | protected $subscribe = [ 27 | UserEventsListener::class, 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/RepositoriesServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(UserRepository::class, function () { 27 | return new EloquentUserRepository(new User()); 28 | }); 29 | } 30 | 31 | /** 32 | * Get the services provided by the provider. 33 | * 34 | * @return array 35 | */ 36 | public function provides() 37 | { 38 | return [ 39 | UserRepository::class 40 | ]; 41 | } 42 | } -------------------------------------------------------------------------------- /app/Repositories/AbstractEloquentRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 39 | $this->loggedInUser = $this->getLoggedInUser(); 40 | } 41 | 42 | /** 43 | * Get Model instance 44 | * 45 | * @return Model 46 | */ 47 | public function getModel() 48 | { 49 | return $this->model; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function findOne($id) 56 | { 57 | return $this->findOneBy(['uid' => $id]); 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function findOneBy(array $criteria) 64 | { 65 | return $this->model->where($criteria)->first(); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function findBy(array $searchCriteria = []) 72 | { 73 | $limit = !empty($searchCriteria['per_page']) ? (int)$searchCriteria['per_page'] : 15; // it's needed for pagination 74 | 75 | $queryBuilder = $this->model->where(function ($query) use ($searchCriteria) { 76 | 77 | $this->applySearchCriteriaInQueryBuilder($query, $searchCriteria); 78 | } 79 | ); 80 | 81 | return $queryBuilder->paginate($limit); 82 | } 83 | 84 | 85 | /** 86 | * Apply condition on query builder based on search criteria 87 | * 88 | * @param Object $queryBuilder 89 | * @param array $searchCriteria 90 | * @return mixed 91 | */ 92 | protected function applySearchCriteriaInQueryBuilder($queryBuilder, array $searchCriteria = []) 93 | { 94 | 95 | foreach ($searchCriteria as $key => $value) { 96 | 97 | //skip pagination related query params 98 | if (in_array($key, ['page', 'per_page'])) { 99 | continue; 100 | } 101 | 102 | //we can pass multiple params for a filter with commas 103 | $allValues = explode(',', $value); 104 | 105 | if (count($allValues) > 1) { 106 | $queryBuilder->whereIn($key, $allValues); 107 | } else { 108 | $operator = '='; 109 | $queryBuilder->where($key, $operator, $value); 110 | } 111 | } 112 | 113 | return $queryBuilder; 114 | } 115 | 116 | /** 117 | * @inheritdoc 118 | */ 119 | public function save(array $data) 120 | { 121 | // generate uid 122 | $data['uid'] = Uuid::uuid4(); 123 | 124 | return $this->model->create($data); 125 | } 126 | 127 | /** 128 | * @inheritdoc 129 | */ 130 | public function update(Model $model, array $data) 131 | { 132 | $fillAbleProperties = $this->model->getFillable(); 133 | 134 | foreach ($data as $key => $value) { 135 | 136 | // update only fillAble properties 137 | if (in_array($key, $fillAbleProperties)) { 138 | $model->$key = $value; 139 | } 140 | } 141 | 142 | // update the model 143 | $model->save(); 144 | 145 | // get updated model from database 146 | $model = $this->findOne($model->uid); 147 | 148 | return $model; 149 | } 150 | 151 | /** 152 | * @inheritdoc 153 | */ 154 | public function findIn($key, array $values) 155 | { 156 | return $this->model->whereIn($key, $values)->get(); 157 | } 158 | 159 | /** 160 | * @inheritdoc 161 | */ 162 | public function delete(Model $model) 163 | { 164 | return $model->delete(); 165 | } 166 | 167 | /** 168 | * get loggedIn user 169 | * 170 | * @return User 171 | */ 172 | protected function getLoggedInUser() 173 | { 174 | $user = \Auth::user(); 175 | 176 | if ($user instanceof User) { 177 | return $user; 178 | } else { 179 | return new User(); 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /app/Repositories/Contracts/BaseRepository.php: -------------------------------------------------------------------------------- 1 | loggedInUser->role !== User::ADMIN_ROLE) { 53 | $searchCriteria['id'] = $this->loggedInUser->id; 54 | } 55 | 56 | return parent::findBy($searchCriteria); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function findOne($id) 63 | { 64 | if ($id === 'me') { 65 | return $this->getLoggedInUser(); 66 | } 67 | 68 | return parent::findOne($id); 69 | } 70 | } -------------------------------------------------------------------------------- /app/Transformers/UserTransformer.php: -------------------------------------------------------------------------------- 1 | $user->uid, 14 | 'firstName' => $user->firstName, 15 | 'lastName' => $user->lastName, 16 | 'middleName' => $user->middleName, 17 | 'username' => $user->username, 18 | 'email' => $user->email, 19 | 'address' => $user->address, 20 | 'zipCode' => $user->zipCode, 21 | 'city' => $user->city, 22 | 'state' => $user->state, 23 | 'country' => $user->country, 24 | 'phone' => $user->phone, 25 | 'mobile' => $user->mobile, 26 | 'role' => $user->role, 27 | 'profileImage' => $user->profileImage, 28 | 'createdAt' => (string) $user->created_at, 29 | 'updatedAt' => (string) $user->updated_at 30 | ]; 31 | 32 | return $formattedUser; 33 | } 34 | } -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make( 32 | 'Illuminate\Contracts\Console\Kernel' 33 | ); 34 | 35 | exit($kernel->handle(new ArgvInput, new ConsoleOutput)); 36 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | load(); 9 | } catch (Dotenv\Exception\InvalidPathException $e) { 10 | // 11 | } 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Create The Application 16 | |-------------------------------------------------------------------------- 17 | | 18 | | Here we will load the environment and create the application instance 19 | | that serves as the central piece of this framework. We'll use this 20 | | application as an "IoC" container and router for this framework. 21 | | 22 | */ 23 | 24 | $app = new Laravel\Lumen\Application( 25 | realpath(__DIR__.'/../') 26 | ); 27 | 28 | $app->withFacades(); 29 | 30 | $app->withEloquent(); 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Register Container Bindings 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Now we will register a few bindings in the service container. We will 38 | | register the exception handler and the console kernel. You may add 39 | | your own bindings here if you like or you can make another file. 40 | | 41 | */ 42 | 43 | $app->singleton( 44 | Illuminate\Contracts\Debug\ExceptionHandler::class, 45 | App\Exceptions\Handler::class 46 | ); 47 | 48 | $app->singleton( 49 | Illuminate\Contracts\Console\Kernel::class, 50 | App\Console\Kernel::class 51 | ); 52 | 53 | // load cors configurations 54 | $app->configure('cors'); 55 | 56 | // load mail configurations 57 | $app->configure('mail'); 58 | 59 | // load database configurations 60 | $app->configure('database'); 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Register Middleware 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Next, we will register the middleware with the application. These can 68 | | be global middleware that run before and after each request into a 69 | | route or middleware that'll be assigned to some specific routes. 70 | | 71 | */ 72 | 73 | $app->middleware([ 74 | \Barryvdh\Cors\HandleCors::class, 75 | ]); 76 | 77 | $app->routeMiddleware([ 78 | 'auth' => App\Http\Middleware\Authenticate::class, 79 | 'throttle' => App\Http\Middleware\ThrottleRequests::class, 80 | 'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, 81 | 'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class 82 | ]); 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Register Service Providers 87 | |-------------------------------------------------------------------------- 88 | | 89 | | Here we will register all of the application's service providers which 90 | | are used to bind services into the container. Service providers are 91 | | totally optional, so you are not required to uncomment this line. 92 | | 93 | */ 94 | 95 | // $app->register(App\Providers\AppServiceProvider::class); 96 | $app->register(App\Providers\AuthServiceProvider::class); 97 | $app->register(App\Providers\EventServiceProvider::class); 98 | $app->register(App\Providers\RepositoriesServiceProvider::class); 99 | $app->register(Laravel\Passport\PassportServiceProvider::class); 100 | $app->register(Dusterio\LumenPassport\PassportServiceProvider::class); 101 | $app->register(Barryvdh\Cors\ServiceProvider::class); 102 | $app->register(\Illuminate\Mail\MailServiceProvider::class); 103 | 104 | LumenPassport::routes($app); 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Load The Application Routes 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Next we will include the routes file so that they can all be added to 112 | | the application. This will provide all of the URLs the application 113 | | can respond to, as well as the controllers that may handle them. 114 | | 115 | */ 116 | 117 | $app->router->group([ 118 | 'namespace' => 'App\Http\Controllers' 119 | ], function ($router) { 120 | require __DIR__.'/../routes/web.php'; 121 | }); 122 | 123 | return $app; 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/lumen", 3 | "description": "The Laravel Lumen Framework.", 4 | "keywords": ["framework", "laravel", "lumen"], 5 | "license": "MIT", 6 | "type": "project", 7 | "require": { 8 | "php": ">=7.0.0", 9 | "laravel/lumen-framework": "5.5.*", 10 | "vlucas/phpdotenv": "~2.4", 11 | "ramsey/uuid": "^3.7", 12 | "league/fractal": "^0.17.0", 13 | "dusterio/lumen-passport": "^0.2.4", 14 | "barryvdh/laravel-cors": "^0.11.0", 15 | "illuminate/mail": "5.5.*" 16 | }, 17 | "require-dev": { 18 | "fzaninotto/faker": "~1.7", 19 | "phpunit/phpunit": "~6.5", 20 | "mockery/mockery": "~1.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "App\\": "app/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "classmap": [ 29 | "tests/", 30 | "database/" 31 | ] 32 | }, 33 | "scripts": { 34 | "post-root-package-install": [ 35 | "php -r \"copy('.env.example', '.env');\"" 36 | ] 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true 40 | } 41 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'guard' => 'api', 6 | 'passwords' => 'users', 7 | ], 8 | 9 | 'guards' => [ 10 | 'api' => [ 11 | 'driver' => 'passport', 12 | 'provider' => 'users', 13 | ], 14 | ], 15 | 16 | 'providers' => [ 17 | 'users' => [ 18 | 'driver' => 'eloquent', 19 | 'model' => \App\Models\User::class 20 | ] 21 | ] 22 | ]; -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | true, 14 | 'allowedOrigins' => ['*'], 15 | 'allowedHeaders' => ['*'], 16 | 'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 17 | 'exposedHeaders' => [], 18 | 'maxAge' => 0, 19 | 'hosts' => [], 20 | ]; 21 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | PDO::FETCH_CLASS, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Database Connection Name 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may specify which of the database connections below you wish 24 | | to use as your default connection for all database work. Of course 25 | | you may use many connections at once using the Database library. 26 | | 27 | */ 28 | 29 | 'default' => env('DB_CONNECTION', 'mysql'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Database Connections 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here are each of the database connections setup for your application. 37 | | Of course, examples of configuring each database platform that is 38 | | supported by Laravel is shown below to make development simple. 39 | | 40 | | 41 | | All database work in Laravel is done through the PHP PDO facilities 42 | | so make sure you have the driver for your particular database of 43 | | choice installed on your machine before you begin development. 44 | | 45 | */ 46 | 47 | 'connections' => [ 48 | 49 | 'testing' => [ 50 | 'driver' => 'mysql', 51 | 'host' => env('DB_TEST_HOST', 'localhost'), 52 | 'database' => env('DB_TEST_DATABASE', 'restapi_test'), 53 | 'username' => env('DB_TEST_USERNAME', ''), 54 | 'password' => env('DB_TEST_PASSWORD', ''), 55 | 'charset' => env('DB_CHARSET', 'utf8'), 56 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 57 | 'prefix' => env('DB_PREFIX', ''), 58 | 'timezone' => env('DB_TIMEZONE', '+00:00'), 59 | 'strict' => env('DB_STRICT_MODE', false), 60 | ], 61 | 62 | 'sqlite' => [ 63 | 'driver' => 'sqlite', 64 | 'database' => env('DB_DATABASE', base_path('database/database.sqlite')), 65 | 'prefix' => env('DB_PREFIX', ''), 66 | ], 67 | 68 | 'mysql' => [ 69 | 'driver' => 'mysql', 70 | 'host' => env('DB_HOST', 'localhost'), 71 | 'port' => env('DB_PORT', 3306), 72 | 'database' => env('DB_DATABASE', 'forge'), 73 | 'username' => env('DB_USERNAME', 'forge'), 74 | 'password' => env('DB_PASSWORD', ''), 75 | 'charset' => env('DB_CHARSET', 'utf8'), 76 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 77 | 'prefix' => env('DB_PREFIX', ''), 78 | 'timezone' => env('DB_TIMEZONE', '+00:00'), 79 | 'strict' => env('DB_STRICT_MODE', false), 80 | ], 81 | 82 | 'pgsql' => [ 83 | 'driver' => 'pgsql', 84 | 'host' => env('DB_HOST', 'localhost'), 85 | 'port' => env('DB_PORT', 5432), 86 | 'database' => env('DB_DATABASE', 'forge'), 87 | 'username' => env('DB_USERNAME', 'forge'), 88 | 'password' => env('DB_PASSWORD', ''), 89 | 'charset' => env('DB_CHARSET', 'utf8'), 90 | 'prefix' => env('DB_PREFIX', ''), 91 | 'schema' => env('DB_SCHEMA', 'public'), 92 | ], 93 | 94 | 'sqlsrv' => [ 95 | 'driver' => 'sqlsrv', 96 | 'host' => env('DB_HOST', 'localhost'), 97 | 'database' => env('DB_DATABASE', 'forge'), 98 | 'username' => env('DB_USERNAME', 'forge'), 99 | 'password' => env('DB_PASSWORD', ''), 100 | 'charset' => env('DB_CHARSET', 'utf8'), 101 | 'prefix' => env('DB_PREFIX', ''), 102 | ], 103 | 104 | ], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Migration Repository Table 109 | |-------------------------------------------------------------------------- 110 | | 111 | | This table keeps track of all the migrations that have already run for 112 | | your application. Using this information, we can determine which of 113 | | the migrations on disk haven't actually been run in the database. 114 | | 115 | */ 116 | 117 | 'migrations' => 'migrations', 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Redis Databases 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Redis is an open source, fast, and advanced key-value store that also 125 | | provides a richer set of commands than a typical key-value systems 126 | | such as APC or Memcached. Laravel makes it easy to dig right in. 127 | | 128 | */ 129 | 130 | 'redis' => [ 131 | 132 | 'cluster' => env('REDIS_CLUSTER', false), 133 | 134 | 'default' => [ 135 | 'host' => env('REDIS_HOST', '127.0.0.1'), 136 | 'port' => env('REDIS_PORT', 6379), 137 | 'database' => env('REDIS_DATABASE', 0), 138 | 'password' => env('REDIS_PASSWORD', null), 139 | ], 140 | 141 | ], 142 | 143 | ]; 144 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'smtp'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | SMTP Host Address 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may provide the host address of the SMTP server used by your 27 | | applications. A default option is provided that is compatible with 28 | | the Mailgun mail service which will provide reliable deliveries. 29 | | 30 | */ 31 | 32 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | SMTP Host Port 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This is the SMTP port used by your application to deliver e-mails to 40 | | users of the application. Like the host we have set this value to 41 | | stay compatible with the Mailgun e-mail application by default. 42 | | 43 | */ 44 | 45 | 'port' => env('MAIL_PORT', 587), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Global "From" Address 50 | |-------------------------------------------------------------------------- 51 | | 52 | | You may wish for all e-mails sent by your application to be sent from 53 | | the same address. Here, you may specify a name and address that is 54 | | used globally for all e-mails that are sent by your application. 55 | | 56 | */ 57 | 58 | 'from' => [ 59 | 'address' => env('MAIL_FROM_ADDRESS', 'info@yoursite.com'), 60 | 'name' => env('MAIL_FROM_NAME', 'Your Name'), 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | E-Mail Encryption Protocol 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may specify the encryption protocol that should be used when 69 | | the application send e-mail messages. A sensible default using the 70 | | transport layer security protocol should provide great security. 71 | | 72 | */ 73 | 74 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | SMTP Server Username 79 | |-------------------------------------------------------------------------- 80 | | 81 | | If your SMTP server requires a username for authentication, you should 82 | | set it here. This will get used to authenticate with your server on 83 | | connection. You may also set the "password" value below this one. 84 | | 85 | */ 86 | 87 | 'username' => env('MAIL_USERNAME'), 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | SMTP Server Password 92 | |-------------------------------------------------------------------------- 93 | | 94 | | Here you may set the password required by your SMTP server to send out 95 | | messages from your application. This will be given to the server on 96 | | connection so that the application will be able to send messages. 97 | | 98 | */ 99 | 100 | 'password' => env('MAIL_PASSWORD'), 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Sendmail System Path 105 | |-------------------------------------------------------------------------- 106 | | 107 | | When using the "sendmail" driver to send e-mails, we will need to know 108 | | the path to where Sendmail lives on this server. A default path has 109 | | been provided here, which will work well on most of your systems. 110 | | 111 | */ 112 | 113 | 'sendmail' => '/usr/sbin/sendmail -bs', 114 | 115 | ]; -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | define(App\Models\User::class, function (Faker\Generator $faker) { 4 | return [ 5 | 'uid' => str_random(32), 6 | 'firstName' => $faker->firstName, 7 | 'lastName' => $faker->lastName, 8 | 'email' => $faker->email, 9 | 'middleName' => $faker->lastName, 10 | 'password' => \Illuminate\Support\Facades\Hash::make('test-password'), 11 | 'address' => $faker->address, 12 | 'zipCode' => $faker->postcode, 13 | 'username' => $faker->userName, 14 | 'city' => $faker->city, 15 | 'state' => $faker->state, 16 | 'country' => $faker->country, 17 | 'phone' => $faker->phoneNumber, 18 | 'mobile' => $faker->phoneNumber, 19 | 'role' => \App\Models\User::BASIC_ROLE, 20 | 'isActive' => rand(0,1), 21 | 'profileImage' => $faker->imageUrl('100') 22 | ]; 23 | }); -------------------------------------------------------------------------------- /database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasib32/rest-api-with-lumen/18a8dc72f54c69ffa38c45352d5d303d313e0352/database/migrations/.gitkeep -------------------------------------------------------------------------------- /database/migrations/2017_03_01_155453_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('uid', 36)->unique(); 19 | $table->string('firstName', '100')->nullable(); 20 | $table->string('lastName', '100')->nullable(); 21 | $table->string('middleName', '50')->nullable(); 22 | $table->string('username', '50')->nullable(); 23 | $table->string('email')->unique(); 24 | $table->string('password')->nullable(); 25 | $table->string('address')->nullable(); 26 | $table->string('zipCode')->nullable(); 27 | $table->string('phone')->nullable(); 28 | $table->string('mobile')->nullable(); 29 | $table->string('city', '100')->nullable(); 30 | $table->string('state', '100')->nullable(); 31 | $table->string('country', '100')->nullable(); 32 | $table->enum('role', ['BASIC_USER', 'ADMIN_USER'])->default('BASIC_USER'); 33 | $table->tinyInteger('isActive'); 34 | $table->string('profileImage')->nullable(); 35 | $table->timestamps(); 36 | $table->softDeletes(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | Schema::dropIfExists('users'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /database/seeds/UsersTableSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Set environment variables for dev 4 | export APP_PORT=${APP_PORT:-80} 5 | export DB_PORT=${DB_PORT:-3306} 6 | export XDEBUG_HOST=$(ipconfig getifaddr en0) # Specific to Macintosh 7 | 8 | COMPOSE="docker-compose" 9 | 10 | # If we pass any arguments 11 | if [ $# -gt 0 ];then 12 | 13 | # If "art" or "artisan" is used, pass-thru to "artisan" 14 | # inside a new container 15 | if [ "$1" == "art" ] || [ "$1" == "artisan" ]; then 16 | shift 1 17 | $COMPOSE run --rm \ 18 | -w /var/www/html \ 19 | restapi-app \ 20 | php artisan "$@" 21 | 22 | # If "composer" is used, pass-thru to "composer" 23 | # inside a new container 24 | elif [ "$1" == "composer" ];then 25 | shift 1 26 | $COMPOSE run --rm \ 27 | -w /var/www/html \ 28 | restapi-app \ 29 | composer "$@" 30 | 31 | # If "test" is used, run unit tests, 32 | # pass-thru any extra arguments to php-unit 33 | elif [ "$1" == "test" ];then 34 | shift 1 35 | $COMPOSE run --rm \ 36 | -w /var/www/html \ 37 | restapi-app \ 38 | ./vendor/bin/phpunit "$@" 39 | else 40 | $COMPOSE "$@" 41 | fi 42 | else 43 | $COMPOSE ps 44 | fi -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | restapi-app: 4 | depends_on: 5 | - restapi-mysql 6 | build: 7 | context: ./docker 8 | dockerfile: Dockerfile 9 | image: restapi/php-nginx 10 | environment: 11 | XDEBUG_HOST : ${XDEBUG_HOST} 12 | ports: 13 | - "${APP_PORT}:80" 14 | volumes: 15 | - .:/var/www/html 16 | networks: 17 | - restapi_docker 18 | restapi-redis: 19 | image: redis:alpine 20 | networks: 21 | - restapi_docker 22 | restapi-mysql: 23 | image: mysql:5.7 24 | ports: 25 | - "${DB_PORT}:3306" 26 | environment: 27 | MYSQL_ROOT_PASSWORD: root 28 | MYSQL_DATABASE: restapi 29 | MYSQL_USER: homestead 30 | MYSQL_PASSWORD: secret 31 | volumes: 32 | - rapimsqldata:/var/lib/mysql 33 | networks: 34 | - restapi_docker 35 | networks: 36 | restapi_docker: 37 | driver: "bridge" 38 | volumes: 39 | rapimsqldata: 40 | driver: "local" -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | LABEL maintainer="Hasan Hasibul" 4 | 5 | RUN apt-get update \ 6 | && apt-get install -y locales \ 7 | && locale-gen en_US.UTF-8 8 | 9 | ENV LANG en_US.UTF-8 10 | ENV LANGUAGE en_US:en 11 | ENV LC_ALL en_US.UTF-8 12 | 13 | WORKDIR /var/www/html 14 | 15 | RUN apt-get update \ 16 | && apt-get install -y nginx curl zip unzip git vim software-properties-common supervisor sqlite3 \ 17 | && add-apt-repository -y ppa:ondrej/php \ 18 | && apt-get update \ 19 | && apt-get install -y php7.1-fpm php7.1-cli php7.1-mcrypt php7.1-gd php7.1-mysql \ 20 | php7.1-pgsql php7.1-imap php-memcached php7.1-mbstring php7.1-xml php7.1-curl \ 21 | php7.1-imagick php7.1-zip php7.1-bcmath php7.1-sqlite3 php7.1-xdebug php7.1-mongodb \ 22 | && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 23 | && mkdir /run/php \ 24 | && apt-get -y autoremove \ 25 | && apt-get clean \ 26 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ 27 | && echo "daemon off;" >> /etc/nginx/nginx.conf 28 | 29 | RUN ln -sf /dev/stdout /var/log/nginx/access.log \ 30 | && ln -sf /dev/stderr /var/log/nginx/error.log 31 | 32 | COPY default /etc/nginx/sites-available/default 33 | COPY php-fpm.conf /etc/php/7.1/fpm/php-fpm.conf 34 | COPY xdebug.ini /etc/php/7.1/mods-available/xdebug.ini 35 | 36 | COPY start-container /usr/local/bin/start-container 37 | RUN chmod +x /usr/local/bin/start-container 38 | 39 | EXPOSE 80 40 | 41 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 42 | 43 | CMD ["start-container"] -------------------------------------------------------------------------------- /docker/default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server ipv6only=on; 4 | 5 | root /var/www/html/public; 6 | 7 | index index.html index.htm index.php; 8 | 9 | server_name restapi.app; 10 | 11 | charset utf-8; 12 | 13 | location = /favicon.ico { log_not_found off; access_log off; } 14 | location = /robots.txt { log_not_found off; access_log off; } 15 | 16 | 17 | location / { 18 | try_files $uri $uri/ /index.php$is_args$args; 19 | } 20 | 21 | location ~ \.php$ { 22 | include snippets/fastcgi-php.conf; 23 | fastcgi_pass unix:/run/php/php7.1-fpm.sock; 24 | } 25 | 26 | error_page 404 /index.php; 27 | 28 | location ~ /\.ht { 29 | deny all; 30 | } 31 | } -------------------------------------------------------------------------------- /docker/php-fpm.conf: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;; 2 | ; FPM Configuration ; 3 | ;;;;;;;;;;;;;;;;;;;;; 4 | 5 | ; All relative paths in this configuration file are relative to PHP's install 6 | ; prefix (/usr). This prefix can be dynamically changed by using the 7 | ; '-p' argument from the command line. 8 | 9 | ;;;;;;;;;;;;;;;;;; 10 | ; Global Options ; 11 | ;;;;;;;;;;;;;;;;;; 12 | 13 | [global] 14 | ; Pid file 15 | ; Note: the default prefix is /var 16 | ; Default Value: none 17 | pid = /run/php/php7.1-fpm.pid 18 | 19 | ; Error log file 20 | ; If it's set to "syslog", log is sent to syslogd instead of being written 21 | ; in a local file. 22 | ; Note: the default prefix is /var 23 | ; Default Value: log/php-fpm.log 24 | error_log = /proc/self/fd/2 25 | 26 | ; syslog_facility is used to specify what type of program is logging the 27 | ; message. This lets syslogd specify that messages from different facilities 28 | ; will be handled differently. 29 | ; See syslog(3) for possible values (ex daemon equiv LOG_DAEMON) 30 | ; Default Value: daemon 31 | ;syslog.facility = daemon 32 | 33 | ; syslog_ident is prepended to every message. If you have multiple FPM 34 | ; instances running on the same server, you can change the default value 35 | ; which must suit common needs. 36 | ; Default Value: php-fpm 37 | ;syslog.ident = php-fpm 38 | 39 | ; Log level 40 | ; Possible Values: alert, error, warning, notice, debug 41 | ; Default Value: notice 42 | ;log_level = notice 43 | 44 | ; If this number of child processes exit with SIGSEGV or SIGBUS within the time 45 | ; interval set by emergency_restart_interval then FPM will restart. A value 46 | ; of '0' means 'Off'. 47 | ; Default Value: 0 48 | ;emergency_restart_threshold = 0 49 | 50 | ; Interval of time used by emergency_restart_interval to determine when 51 | ; a graceful restart will be initiated. This can be useful to work around 52 | ; accidental corruptions in an accelerator's shared memory. 53 | ; Available Units: s(econds), m(inutes), h(ours), or d(ays) 54 | ; Default Unit: seconds 55 | ; Default Value: 0 56 | ;emergency_restart_interval = 0 57 | 58 | ; Time limit for child processes to wait for a reaction on signals from master. 59 | ; Available units: s(econds), m(inutes), h(ours), or d(ays) 60 | ; Default Unit: seconds 61 | ; Default Value: 0 62 | ;process_control_timeout = 0 63 | 64 | ; The maximum number of processes FPM will fork. This has been design to control 65 | ; the global number of processes when using dynamic PM within a lot of pools. 66 | ; Use it with caution. 67 | ; Note: A value of 0 indicates no limit 68 | ; Default Value: 0 69 | ; process.max = 128 70 | 71 | ; Specify the nice(2) priority to apply to the master process (only if set) 72 | ; The value can vary from -19 (highest priority) to 20 (lower priority) 73 | ; Note: - It will only work if the FPM master process is launched as root 74 | ; - The pool process will inherit the master process priority 75 | ; unless it specified otherwise 76 | ; Default Value: no set 77 | ; process.priority = -19 78 | 79 | ; Send FPM to background. Set to 'no' to keep FPM in foreground for debugging. 80 | ; Default Value: yes 81 | daemonize = no 82 | 83 | ; Set open file descriptor rlimit for the master process. 84 | ; Default Value: system defined value 85 | ;rlimit_files = 1024 86 | 87 | ; Set max core size rlimit for the master process. 88 | ; Possible Values: 'unlimited' or an integer greater or equal to 0 89 | ; Default Value: system defined value 90 | ;rlimit_core = 0 91 | 92 | ; Specify the event mechanism FPM will use. The following is available: 93 | ; - select (any POSIX os) 94 | ; - poll (any POSIX os) 95 | ; - epoll (linux >= 2.5.44) 96 | ; - kqueue (FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0) 97 | ; - /dev/poll (Solaris >= 7) 98 | ; - port (Solaris >= 10) 99 | ; Default Value: not set (auto detection) 100 | ;events.mechanism = epoll 101 | 102 | ; When FPM is build with systemd integration, specify the interval, 103 | ; in second, between health report notification to systemd. 104 | ; Set to 0 to disable. 105 | ; Available Units: s(econds), m(inutes), h(ours) 106 | ; Default Unit: seconds 107 | ; Default value: 10 108 | ;systemd_interval = 10 109 | 110 | ;;;;;;;;;;;;;;;;;;;; 111 | ; Pool Definitions ; 112 | ;;;;;;;;;;;;;;;;;;;; 113 | 114 | ; Multiple pools of child processes may be started with different listening 115 | ; ports and different management options. The name of the pool will be 116 | ; used in logs and stats. There is no limitation on the number of pools which 117 | ; FPM can handle. Your system will tell you anyway :) 118 | 119 | ; Include one or more files. If glob(3) exists, it is used to include a bunch of 120 | ; files from a glob(3) pattern. This directive can be used everywhere in the 121 | ; file. 122 | ; Relative path can also be used. They will be prefixed by: 123 | ; - the global prefix if it's been set (-p argument) 124 | ; - /usr otherwise 125 | include=/etc/php/7.1/fpm/pool.d/*.conf 126 | 127 | ; Clear environment in FPM workers. Prevents arbitrary environment variables from 128 | ; reaching FPM worker processes by clearing the environment in workers before env 129 | ; vars specified in this pool configuration are added. 130 | clear_env=false -------------------------------------------------------------------------------- /docker/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sed -i "s/xdebug\.remote_host\=.*/xdebug\.remote_host\=$XDEBUG_HOST/g" /etc/php/7.1/mods-available/xdebug.ini 4 | 5 | /usr/bin/supervisord -------------------------------------------------------------------------------- /docker/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command=nginx 6 | stdout_logfile=/dev/stdout 7 | stdout_logfile_maxbytes=0 8 | stderr_logfile=/dev/stderr 9 | stderr_logfile_maxbytes=0 10 | 11 | [program:php-fpm] 12 | command=php-fpm7.1 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /docker/xdebug.ini: -------------------------------------------------------------------------------- 1 | zend_extension=xdebug.so 2 | xdebug.remote_enable=1 3 | xdebug.remote_handler=dbgp 4 | xdebug.remote_port=9000 5 | xdebug.remote_autostart=1 6 | xdebug.remote_connect_back=0 7 | xdebug.idekey=docker 8 | xdebug.remote_host=?? 9 | xdebug.max_nesting_level=500 -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./app 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Redirect Trailing Slashes If Not A Folder... 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)/$ /$1 [L,R=301] 11 | 12 | # Handle Front Controller... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index.php [L] 16 | 17 | # Handle Authorization Header 18 | RewriteCond %{HTTP:Authorization} . 19 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 20 | 21 | -------------------------------------------------------------------------------- /public/images/accessTokenCreation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasib32/rest-api-with-lumen/18a8dc72f54c69ffa38c45352d5d303d313e0352/public/images/accessTokenCreation.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | run(); 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # REST API with Lumen 5.5 [![Build Status](https://travis-ci.org/hasib32/rest-api-with-lumen.svg?branch=master)](https://travis-ci.org/hasib32/rest-api-with-lumen) 3 | 4 | A RESTful API boilerplate for Lumen micro-framework. Features included: 5 | 6 | - Users Resource 7 | - OAuth2 Authentication using Laravel Passport 8 | - Scope based Authorization 9 | - Validation 10 | - [Repository Pattern](https://msdn.microsoft.com/en-us/library/ff649690.aspx) 11 | - API Response with [Fractal](http://fractal.thephpleague.com/) 12 | - Pagination 13 | - Seeding Database With Model Factory 14 | - Event Handling 15 | - Sending Mail using Mailable class 16 | - [CORS](https://github.com/barryvdh/laravel-cors) Support 17 | - [Rate Limit API Requests](https://mattstauffer.co/blog/api-rate-limiting-in-laravel-5-2) 18 | - Endpoint Tests and Unit Tests 19 | - Build Process with [Travis CI](https://travis-ci.org/) 20 | 21 | ## Getting Started 22 | First, clone the repo: 23 | ```bash 24 | $ git clone git@github.com:hasib32/rest-api-with-lumen.git 25 | ``` 26 | 27 | #### Laravel Homestead 28 | You can use Laravel Homestead globally or per project for local development. Follow the [Installation Guide](https://laravel.com/docs/5.5/homestead#installation-and-setup). 29 | 30 | #### Install dependencies 31 | ``` 32 | $ cd rest-api-with-lumen 33 | $ composer install 34 | ``` 35 | 36 | #### Configure the Environment 37 | Create `.env` file: 38 | ``` 39 | $ cat .env.example > .env 40 | ``` 41 | If you want you can edit database name, database username and database password. 42 | 43 | #### Migrations and Seed the database with fake data 44 | First, we need connect to the database. For homestead user, login using default homestead username and password: 45 | ```bash 46 | $ mysql -uhomestead -psecret 47 | ``` 48 | 49 | Then create a database: 50 | ```bash 51 | mysql> CREATE DATABASE restapi; 52 | ``` 53 | 54 | And also create test database: 55 | ```bash 56 | mysql> CREATE DATABASE restapi_test; 57 | ``` 58 | 59 | Run the Artisan migrate command with seed: 60 | ```bash 61 | $ php artisan migrate --seed 62 | ``` 63 | 64 | Create "personal access" and "password grant" clients which will be used to generate access tokens: 65 | ```bash 66 | $ php artisan passport:install 67 | ``` 68 | 69 | You can find those clients in ```oauth_clients``` table. 70 | 71 | ### API Routes 72 | | HTTP Method | Path | Action | Scope | Desciption | 73 | | ----- | ----- | ----- | ---- |------------- | 74 | | GET | /users | index | users:list | Get all users 75 | | POST | /users | store | users:create | Create an user 76 | | GET | /users/{user_id} | show | users:read | Fetch an user by id 77 | | PUT | /users/{user_id} | update | users:write | Update an user by id 78 | | DELETE | /users/{user_id} | destroy | users:delete | Delete an user by id 79 | 80 | Note: ```users/me``` is a special route for getting current authenticated user. 81 | And for all User routes 'users' scope is available if you want to perform all actions. 82 | 83 | ### OAuth2 Routes 84 | Visit [dusterio/lumen-passport](https://github.com/dusterio/lumen-passport/blob/master/README.md#installed-routes) to see all the available ```OAuth2``` routes. 85 | 86 | ### Creating access_token 87 | Since Laravel Passport doesn't restrict any user creating any valid scope. I had to create a route and controller to restrict user creating access token only with permitted scopes. For creating access_token we have to use the ```accessToken``` route. Here is an example of creating access_token for grant_type password with [Postman.](https://www.getpostman.com/) 88 | 89 | http://stackoverflow.com/questions/39436509/laravel-passport-scopes 90 | 91 | ![access_token creation](/public/images/accessTokenCreation.png?raw=true "access_token creation example") 92 | 93 | ## Creating a New Resource 94 | Creating a new resource is very easy and straight-forward. Follow these simple steps to create a new resource. 95 | 96 | ### Step 1: Create Route 97 | Create a new route name ```messages```. Open the ```routes/web.php``` file and add the following code: 98 | 99 | ```php 100 | $route->post('messages', [ 101 | 'uses' => 'MessageController@store', 102 | 'middleware' => "scope:messages,messages:create" 103 | ]); 104 | $route->get('messages', [ 105 | 'uses' => 'MessageController@index', 106 | 'middleware' => "scope:messages,messages:list" 107 | ]); 108 | $route->get('messages/{id}', [ 109 | 'uses' => 'MessageController@show', 110 | 'middleware' => "scope:messages,messages:read" 111 | ]); 112 | $route->put('messages/{id}', [ 113 | 'uses' => 'MessageController@update', 114 | 'middleware' => "scope:messages,messages:write" 115 | ]); 116 | $route->delete('messages/{id}', [ 117 | 'uses' => 'MessageController@destroy', 118 | 'middleware' => "scope:messages,messages:delete" 119 | ]); 120 | ``` 121 | 122 | For more info please visit Lumen [Routing](https://lumen.laravel.com/docs/5.5/routing) page. 123 | 124 | ### Step 2: Create Model and Migration for the Table 125 | Create ```Message``` Model inside ```App/Models``` directory and create migration using Lumen Artisan command. 126 | 127 | **Message Model** 128 | 129 | ```php 130 | increments('id'); 175 | $table->string('uid', 36)->unique(); 176 | $table->integer('userId')->unsigned(); 177 | $table->string('subject')->nullable(); 178 | $table->longText('message'); 179 | $table->timestamps(); 180 | 181 | $table->foreign('userId') 182 | ->references('id')->on('users') 183 | ->onDelete('cascade') 184 | ->onUpdate('cascade'); 185 | }); 186 | } 187 | } 188 | ``` 189 | 190 | For more info visit Laravel [Migration](https://laravel.com/docs/5.5/migrations) page. 191 | 192 | ### Step 3: Create Repository 193 | Create ```MessageRepository``` and implementation of the repository name ```EloquentMessageRepository```. 194 | 195 | **MessageRepository** 196 | 197 | ```php 198 | app->bind(UserRepository::class, function () { 258 | return new EloquentUserRepository(new User()); 259 | }); 260 | $this->app->bind(MessageRepository::class, function () { 261 | return new EloquentMessageRepository(new Message()); 262 | }); 263 | } 264 | 265 | /** 266 | * Get the services provided by the provider. 267 | * 268 | * @return array 269 | */ 270 | public function provides() 271 | { 272 | return [ 273 | UserRepository::class, 274 | MessageRepository::class, 275 | ]; 276 | } 277 | } 278 | ``` 279 | 280 | Visit Lumen documentation for more info about [Service Provider](https://lumen.laravel.com/docs/5.5/providers). 281 | 282 | ### Step 4: Create Fractal Transformer 283 | Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc. 284 | 285 | Create a new Transformer name ```MessageTransformer``` inside ```app/Transformers``` directory: 286 | 287 | ```php 288 | $message->uid, 301 | 'userId' => $message->userId, 302 | 'subject' => $message->subject, 303 | 'message' => $message->message, 304 | 'createdAt' => (string) $message->created_at, 305 | 'updatedAt' => (string) $message->updated_at, 306 | ]; 307 | } 308 | } 309 | ``` 310 | Visit [Fractal](http://fractal.thephpleague.com/) official page for more information. 311 | 312 | ### Step 5: Create Policy 313 | For authorization we need to create policy that way basic user can't show or edit other user messages. 314 | 315 | **MessagePolicy** 316 | 317 | ```php 318 | isAdmin() && (!$currentUser->tokenCan('basic') || $currentUser->tokenCan('undefined'))) { 336 | return true; 337 | } 338 | } 339 | 340 | /** 341 | * Determine if a given user has permission to show. 342 | * 343 | * @param User $currentUser 344 | * @param Message $message 345 | * @return bool 346 | */ 347 | public function show(User $currentUser, Message $message) 348 | { 349 | return $currentUser->id === $message->userId; 350 | } 351 | 352 | /** 353 | * Determine if a given user can update. 354 | * 355 | * @param User $currentUser 356 | * @param Message $message 357 | * @return bool 358 | */ 359 | public function update(User $currentUser, Message $message) 360 | { 361 | return $currentUser->id === $message->userId; 362 | } 363 | 364 | /** 365 | * Determine if a given user can delete. 366 | * 367 | * @param User $currentUser 368 | * @param Message $message 369 | * @return bool 370 | */ 371 | public function destroy(User $currentUser, Message $message) 372 | { 373 | return $currentUser->id === $message->userId; 374 | } 375 | } 376 | ``` 377 | Next, update ```AuthServiceProvider``` to use the policy: 378 | ``` 379 | Gate::policy(Message::class, MessagePolicy::class); 380 | ``` 381 | And add scopes to ``Passport::tokensCan``: 382 | ``` 383 | [ 384 | 'messages' => 'Messages scope', 385 | 'messages:list' => 'Messages scope', 386 | 'messages:read' => 'Messages scope for reading records', 387 | 'messages:write' => 'Messages scope for writing records', 388 | 'messages:create' => 'Messages scope for creating records', 389 | 'messages:delete' => 'Messages scope for deleting records' 390 | ] 391 | ``` 392 | Visit Lumen [Authorization Page](https://lumen.laravel.com/docs/5.5/authorization) for more info about Policy. 393 | 394 | ### Last Step: Create Controller 395 | 396 | Finally, let's create the ```MessageController```. Here we're using **MessageRepository, MessageTransformer and MessagePolicy**. 397 | 398 | ```php 399 | messageRepository = $messageRepository; 433 | $this->messageTransformer = $messageTransformer; 434 | 435 | parent::__construct(); 436 | } 437 | 438 | /** 439 | * Display a listing of the resource. 440 | * 441 | * @param Request $request 442 | * @return \Illuminate\Http\JsonResponse 443 | */ 444 | public function index(Request $request) 445 | { 446 | $messages = $this->messageRepository->findBy($request->all()); 447 | 448 | return $this->respondWithCollection($messages, $this->messageTransformer); 449 | } 450 | 451 | /** 452 | * Display the specified resource. 453 | * 454 | * @param $id 455 | * @return \Illuminate\Http\JsonResponse|string 456 | */ 457 | public function show($id) 458 | { 459 | $message = $this->messageRepository->findOne($id); 460 | 461 | if (!$message instanceof Message) { 462 | return $this->sendNotFoundResponse("The message with id {$id} doesn't exist"); 463 | } 464 | 465 | // Authorization 466 | $this->authorize('show', $message); 467 | 468 | return $this->respondWithItem($message, $this->messageTransformer); 469 | } 470 | 471 | /** 472 | * Store a newly created resource in storage. 473 | * 474 | * @param Request $request 475 | * @return \Illuminate\Http\JsonResponse|string 476 | */ 477 | public function store(Request $request) 478 | { 479 | // Validation 480 | $validatorResponse = $this->validateRequest($request, $this->storeRequestValidationRules($request)); 481 | 482 | // Send failed response if validation fails 483 | if ($validatorResponse !== true) { 484 | return $this->sendInvalidFieldResponse($validatorResponse); 485 | } 486 | 487 | $message = $this->messageRepository->save($request->all()); 488 | 489 | if (!$message instanceof Message) { 490 | return $this->sendCustomResponse(500, 'Error occurred on creating Message'); 491 | } 492 | 493 | return $this->setStatusCode(201)->respondWithItem($message, $this->messageTransformer); 494 | } 495 | 496 | /** 497 | * Update the specified resource in storage. 498 | * 499 | * @param Request $request 500 | * @param $id 501 | * @return \Illuminate\Http\JsonResponse 502 | */ 503 | public function update(Request $request, $id) 504 | { 505 | // Validation 506 | $validatorResponse = $this->validateRequest($request, $this->updateRequestValidationRules($request)); 507 | 508 | // Send failed response if validation fails 509 | if ($validatorResponse !== true) { 510 | return $this->sendInvalidFieldResponse($validatorResponse); 511 | } 512 | 513 | $message = $this->messageRepository->findOne($id); 514 | 515 | if (!$message instanceof Message) { 516 | return $this->sendNotFoundResponse("The message with id {$id} doesn't exist"); 517 | } 518 | 519 | // Authorization 520 | $this->authorize('update', $message); 521 | 522 | 523 | $message = $this->messageRepository->update($message, $request->all()); 524 | 525 | return $this->respondWithItem($message, $this->messageTransformer); 526 | } 527 | 528 | /** 529 | * Remove the specified resource from storage. 530 | * 531 | * @param $id 532 | * @return \Illuminate\Http\JsonResponse|string 533 | */ 534 | public function destroy($id) 535 | { 536 | $message = $this->messageRepository->findOne($id); 537 | 538 | if (!$message instanceof Message) { 539 | return $this->sendNotFoundResponse("The message with id {$id} doesn't exist"); 540 | } 541 | 542 | // Authorization 543 | $this->authorize('destroy', $message); 544 | 545 | $this->messageRepository->delete($message); 546 | 547 | return response()->json(null, 204); 548 | } 549 | 550 | /** 551 | * Store Request Validation Rules 552 | * 553 | * @param Request $request 554 | * @return array 555 | */ 556 | private function storeRequestValidationRules(Request $request) 557 | { 558 | return [ 559 | 'userId' => 'required|exists:users,id', 560 | 'subject' => 'required', 561 | 'message' => 'required', 562 | ]; 563 | } 564 | 565 | /** 566 | * Update Request validation Rules 567 | * 568 | * @param Request $request 569 | * @return array 570 | */ 571 | private function updateRequestValidationRules(Request $request) 572 | { 573 | return [ 574 | 'subject' => '', 575 | 'message' => '', 576 | ]; 577 | } 578 | } 579 | ``` 580 | 581 | Visit Lumen [Controller](https://lumen.laravel.com/docs/5.5/controllers) page for more info about Controller. 582 | 583 | ## Tutorial 584 | To see the step-by-step tutorial how I created this boilerplate please visit our blog [devnootes.net](https://devnotes.net/rest-api-development-with-lumen-part-one/). 585 | 586 | ## Contributing 587 | Contributions, questions and comments are all welcome and encouraged. For code contributions submit a pull request. 588 | 589 | ## Credits 590 | [Taylor Otwell](https://github.com/taylorotwell), [Shahriar Mahmood](https://github.com/shahriar1), [Fractal](http://fractal.thephpleague.com/), [Phil Sturgeon](https://github.com/philsturgeon) 591 | ## License 592 | 593 | [MIT license](http://opensource.org/licenses/MIT) 594 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasib32/rest-api-with-lumen/18a8dc72f54c69ffa38c45352d5d303d313e0352/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/emails/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | welcome email 6 | 7 | 8 |

Hi, {{ ucwords($user->firstName) }} Welcome to yoursite.com. Thank you for sign-up with us..

9 | 10 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 16 | return app()->version(); 17 | }); 18 | 19 | // Generate random string 20 | $router->get('appKey', function () { 21 | return str_random('32'); 22 | }); 23 | 24 | // route for creating access_token 25 | $router->post('accessToken', 'AccessTokenController@createAccessToken'); 26 | 27 | $router->group(['middleware' => ['auth:api', 'throttle:60']], function () use ($router) { 28 | $router->post('users', [ 29 | 'uses' => 'UserController@store', 30 | 'middleware' => "scope:users,users:create" 31 | ]); 32 | $router->get('users', [ 33 | 'uses' => 'UserController@index', 34 | 'middleware' => "scope:users,users:list" 35 | ]); 36 | $router->get('users/{id}', [ 37 | 'uses' => 'UserController@show', 38 | 'middleware' => "scope:users,users:read" 39 | ]); 40 | $router->put('users/{id}', [ 41 | 'uses' => 'UserController@update', 42 | 'middleware' => "scope:users,users:write" 43 | ]); 44 | $router->delete('users/{id}', [ 45 | 'uses' => 'UserController@destroy', 46 | 'middleware' => "scope:users,users:delete" 47 | ]); 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/oauth-private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEAuxY4f9qMcoqQNjS4R3N8gFsV1dsIKzIMjyjTHQ/itMJKSvlI 3 | zdUc335H3WJU4qTUL9A0DDpwgZsyOjJInMmyZOvtwXI2RSMocDmQ5mr7Nsv5QDt9 4 | DKSPceMq0ex1BKsLtLCGMwHTWbSg3idZYjmFgmEa15YGpVAc0WyE9xb0Y+KHN8qK 5 | vprg7PQPzKmRiF3XHMshhKPp7A6RO57T5olAWj/QvCz/aeOW0xrnaaxOgZwABAll 6 | Y+VKBiCtK3tRRfVPqUf2e89Kdooz/yKFse7jOdKJNuU+EagD3CQ+55xH+A8zpMk4 7 | HL2mqF4PhQVrt+9Noq9lMB+AszqQUNDy5WQp5ChZpac7muxI5hP02hdw5i7vq/vC 8 | Tr8OOZkvoXSDMk0CKGN+IzSIbWYJ0PIWlnIXNumx7U0S53SNxWH8wov0k5O0BZmA 9 | xyL1ufiawRDkVZjcFGkLJTd6Bh88DUtC6eb4cJIG2/1GCVHDnKc2LuwpCnXcjMCT 10 | faU51GieolItHBX+W1s3tzg7rV4Dwd3ydqt/QffdA3rPUkvjT3R5/ZaHb1PWSXnw 11 | /G+rz/JpbtWcdrSprf1LUa7NAV54FA01eWA6VzPH6y2Xvt1T+nG6TjSg+NgOhdbl 12 | G3I4CpXQVN9R1+3ebqeMiiNGsq+ZNm7kGoN7nrVjrD5suWykcgnRijhHy4kCAwEA 13 | AQKCAgApFaAis6PUI4m33av6ROo6ZcZNyGPq2HrZlWWHJE3s1B3siQHr8bj9e4Xc 14 | oBN0Ei5msZo8dTjvvV0yWoiHpeN/NjBoIuS4GhhvTLT9ZND2H6UE7gtwE1NM904X 15 | +41XERjo/Tr7SJNZR8lr/8gpQeiH0TtLzK547zbk5qfseyYAKWb4YYpGgUyTvKUb 16 | lMFY6QKsWA9sSXK9XBWYujBemBWBvDHioR8V6dzdTzMyiZxT6iY2vLl/ToMLBXIB 17 | 8nrOmih8TElctxDrJz5z8OjbQlK0CAAKBh2zUupGFc+anT9QwkXdEjM4XI2Uu9M8 18 | qwwUwICUEWBvSPvCYEIA/WHZ7j0kdWzf/q1P87LnppMDLn49GFfY1NMRV2T4q46q 19 | dy7ahOvkCRx0QUxISPcOQXwF5aN7GtPAU7c7fnotxQMHjQVMU3bWjV6L3+hceruK 20 | 2J+owMqVIV/CAvRUE+lEEczvaxb+RP/2W7q4OVCFOaEfjeggd8GzRjXkQdkfH1w7 21 | 7sg9yc5L6IVOoH4TQ1PzE2JKXkPkh6ZhGgTOFyaqqFTnw1TrUh3PL1uUZN8UdV8v 22 | uPwq4Hm7RrZvi1PkM9dj5ezgEoJTyh+KlLVuGYmigIDHXjV0KupmbfTFzLAvhpxM 23 | ofjydOl0c8iPevpbvT6cB8FjlPR6J4HcSznQeqO3TexUZfKzgQKCAQEA4ervzikj 24 | IX8ocxPJqQVGzqu2NArA99ePOLC9DByrtU3b6q4d8BlsK9MNZE8bJU9xaP4+qTKG 25 | gcosdHDR6TbCBW7ic9/7G68IEIQ9JNyDu4A0UQe6srzdVmVCGD6Os4fybCb+JFq2 26 | n6r8PPPAgBpO43i6asqzsOt1ERX8vCZP73MV0XCbgfyDSk4BezK59uasJNjBU8Wb 27 | KHpd2/drSZq+jpQr50/vJhQQ34XKOcEzntm6zoK1srycVQHWS+U8LZBlBb9cGuce 28 | j5EV2nkqeLUf+Uexcs2TV3oBtFm2J8znTerGHs0dfLHv3Gy/5LWJ2CXoXDFt6eTP 29 | tYp91GOpkSdAPQKCAQEA0/+ecoVUV1S1ohzMq0zSWFnahHtALiCbkXWJFrIzIwun 30 | mRsz1v6PAiiwnj7iJ3kQz2oy2PZzD7FOqTpR+Ak23FZR46bk0Pvooza37h7ZHTFd 31 | P6DiJUjGKlzXhmM/4O7VBeNdSJtHhkEQ7+jnHF/fk31hxsA0+DHlGLgd61VEN2e8 32 | qlfYBAB8KO1HA70SybAD5weEmaCx8fJf5aUmJbVb+eO/Ipa9Tl6Egj8NVunZsx2U 33 | ZvZRrAeUkgHX6khOSPZkXltLZ4BWCnUf5Hc3Y8BCvF2AtiSOe2aWNRGHRJU5zkW9 34 | 7bFHTgl4jPo1QKh1PA3dseu8mm86mv5QGy5esPZBPQKCAQBU/l5Z4YAmCgDdyoQD 35 | 4shQ2KkjyyfuFlr7qQJ2nBK9kx05nWgF/IqFslFHe7tqvgicx4orfaa9DaLL6+YN 36 | J1y3TpBDp/CA6cjO7fFS/ONSl1kXYyEKPaPH7TCAjoiBYpQBvGnAo6AxUdKz28CA 37 | cWVcjv8TTk9sMClK1ErRPli0bUe4E/VtYeLDAbXs85ijGwWIl0OkkKNfBypopxzm 38 | BCHM4lHiJGCEouf2MA90ywrwqFlveB2DsRHfqWFGDGgnfDuFfgjY1jrZKGxgVVV1 39 | NC9jSWWpG05jirM5dsbhEmWzGrCOxfxh/U+QThmjoejKOPjCNpZzeHHsir3sOdKB 40 | mZLBAoIBAAi74z88lvjyGHmRTi1QgOn66bHDkiVUWpRnjzpMJScwd7srD7uvyRyB 41 | qKUcWhzeM/8XlPizTpI6786xBGd/ma8CL2V9nretaSwwOOuqga9eNUVFz4tRsDhW 42 | ktqKhDs0G7qeX116aDvzukroAX19xaxB4iFaEdSX2aRnEXR8ks7lizRJYjDoBDV7 43 | cQ2KYJfGlKKUALaDlFEvdxvy7dtn2V42L82xACOWaUckadxGzh0+/rovM7YuqcRK 44 | JycTy4XeAMySXkzpUKIlqSa0cqe9aAJyp7bZUrVqyI1vJ29/5l3FKcn9H77rTPgh 45 | 5se/KPVMbPHDkIWcR4HTMGGuS2BnBNECggEAe4JsI2p6GWSBAHPsLU2SLcY2Phl5 46 | yl1DSEVEnDQGty3fjJ727Hhq+YsFbX+EfIzdm08B6ft/ltSTRMeVhi/HA9AIX5p4 47 | eg3FdvIcF2bqzHS2gp6oxSudGxuFNz3x3S7marqOJrIvS4NsiK8/VCdQSExjlGDj 48 | n+zoVIBvBBmVc1EmlqKGc9qh3etMckJ2vQ97hdkNh5LPsTKwpMxTf2JRFlyKtijy 49 | qWDRZRln70xPnRy2oh1GDpMD79ThzBOTeiHyTqGMyK1es3z6lWZXgioIObbsXEpl 50 | B1KhVks2d73qag5H1odEbRbkZ1auKXfLMJz2wcxP48c1Lox3TEBoI3zYBw== 51 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /storage/oauth-public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuxY4f9qMcoqQNjS4R3N8 3 | gFsV1dsIKzIMjyjTHQ/itMJKSvlIzdUc335H3WJU4qTUL9A0DDpwgZsyOjJInMmy 4 | ZOvtwXI2RSMocDmQ5mr7Nsv5QDt9DKSPceMq0ex1BKsLtLCGMwHTWbSg3idZYjmF 5 | gmEa15YGpVAc0WyE9xb0Y+KHN8qKvprg7PQPzKmRiF3XHMshhKPp7A6RO57T5olA 6 | Wj/QvCz/aeOW0xrnaaxOgZwABAllY+VKBiCtK3tRRfVPqUf2e89Kdooz/yKFse7j 7 | OdKJNuU+EagD3CQ+55xH+A8zpMk4HL2mqF4PhQVrt+9Noq9lMB+AszqQUNDy5WQp 8 | 5ChZpac7muxI5hP02hdw5i7vq/vCTr8OOZkvoXSDMk0CKGN+IzSIbWYJ0PIWlnIX 9 | Numx7U0S53SNxWH8wov0k5O0BZmAxyL1ufiawRDkVZjcFGkLJTd6Bh88DUtC6eb4 10 | cJIG2/1GCVHDnKc2LuwpCnXcjMCTfaU51GieolItHBX+W1s3tzg7rV4Dwd3ydqt/ 11 | QffdA3rPUkvjT3R5/ZaHb1PWSXnw/G+rz/JpbtWcdrSprf1LUa7NAV54FA01eWA6 12 | VzPH6y2Xvt1T+nG6TjSg+NgOhdblG3I4CpXQVN9R1+3ebqeMiiNGsq+ZNm7kGoN7 13 | nrVjrD5suWykcgnRijhHy4kCAwEAAQ== 14 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /tests/Endpoints/UsersTest.php: -------------------------------------------------------------------------------- 1 | call('GET', '/users'); 17 | $this->assertResponseStatus(401); 18 | 19 | $user = factory(User::class)->create(); 20 | $user->withAccessToken(new Token(['scopes' => ['*']])); 21 | $this->actingAs($user); 22 | 23 | $this->call('GET', '/users'); 24 | $this->assertResponseOk(); 25 | 26 | // test json response 27 | $this->seeJson(['email' => $user->email]); 28 | } 29 | 30 | public function testGettingSpecificUser() 31 | { 32 | 33 | // without authentication should give 401 34 | $this->call('GET', '/users/12345'); 35 | $this->assertResponseStatus(401); 36 | 37 | $user = factory(User::class)->create(); 38 | 39 | // authenticate 40 | $user->withAccessToken(new Token(['scopes' => ['*']])); 41 | $this->actingAs($user); 42 | 43 | // should work 44 | $this->call('GET', '/users/'.$user->uid); 45 | $this->assertResponseStatus(200); 46 | 47 | // test json response 48 | $this->seeJson(['email' => $user->email]); 49 | 50 | // accessing invalid user should give 404 51 | $this->call('GET', '/users/13232323'); 52 | $this->assertResponseStatus(404); 53 | } 54 | 55 | public function testCreatingUser() 56 | { 57 | // without authentication should give 401 Unauthorized 58 | $this->call('POST', '/users', []); 59 | $this->assertResponseStatus(401); 60 | 61 | $user = factory(User::class)->make(); 62 | $user->withAccessToken(new Token(['scopes' => ['*']])); 63 | $this->actingAs($user); 64 | 65 | // empty data should give 400 invalid fields error 66 | $this->call('POST', '/users', []); 67 | $this->assertResponseStatus(400); 68 | 69 | // should work now 70 | $this->call('POST', '/users', [ 71 | 'email' => 'test@test.com', 72 | 'firstName' => 'first', 73 | 'lastName' => 'last' 74 | ]); 75 | $this->assertResponseStatus(201); 76 | 77 | // same email should give 400 invalid 78 | $this->call('POST', '/users', [ 79 | 'email' => 'test@test.com', 80 | 'firstName' => 'first2', 81 | 'lastName' => 'last2' 82 | ]); 83 | $this->assertResponseStatus(400); 84 | } 85 | 86 | public function testUpdatingUser() 87 | { 88 | $user = factory(User::class)->create(); 89 | 90 | // without authentication should give 401 Unauthorized 91 | $this->call('PUT', '/users/'.$user->uid, []); 92 | $this->assertResponseStatus(401); 93 | 94 | // authenticate 95 | $user->withAccessToken(new Token(['scopes' => ['*']])); 96 | $this->actingAs($user); 97 | 98 | $this->call('PUT', '/users/'.$user->uid, [ 99 | 'firstName' => 'updated_first' 100 | ]); 101 | $this->assertResponseOk(); 102 | 103 | $this->call('PUT', '/users/234324', [ 104 | 'firstName' => 'updated_first' 105 | ]); 106 | $this->assertResponseStatus(404); 107 | } 108 | 109 | public function testDeletingUser() 110 | { 111 | 112 | // without authentication should give 401 113 | $this->call('DELETE', '/users/12345'); 114 | $this->assertResponseStatus(401); 115 | 116 | $user = factory(User::class)->create(); 117 | 118 | // authenticate 119 | $user->withAccessToken(new Token(['scopes' => ['*']])); 120 | $this->actingAs($user); 121 | 122 | // should work 123 | $this->call('DELETE', '/users/'.$user->uid); 124 | $this->assertResponseStatus(204); 125 | 126 | // deleting invalid user should give 404 127 | $this->call('GET', '/users/13232323'); 128 | $this->assertResponseStatus(404); 129 | } 130 | } -------------------------------------------------------------------------------- /tests/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $this->assertResponseOk(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Http/Middleware/ThrottleRequestsTest.php: -------------------------------------------------------------------------------- 1 | create(); 19 | $this->actingAs($user); 20 | 21 | $request = Request::create($this->prepareUrlForRequest('/users/' . $user->uid)); 22 | $middleware = app(ThrottleRequests::class); 23 | 24 | $totalNumberOfRequest = 60; 25 | for ($i = 1; $i <= 65; $i++) { 26 | $response = $middleware->handle($request, function () { 27 | return response()->json(['message' => 'success'], 200); 28 | }, $totalNumberOfRequest); 29 | 30 | $totalRateLimit = $response->headers->get('X-RateLimit-Limit'); 31 | $rateLimitRemaining = $response->headers->get('X-RateLimit-Remaining'); 32 | $this->assertEquals($totalRateLimit, $totalNumberOfRequest); 33 | 34 | if ($totalRateLimit >= $i) { 35 | $this->assertEquals($totalRateLimit - $i, $rateLimitRemaining); 36 | $this->assertTrue($response->isOk()); 37 | $this->assertEquals('{"message":"success"}', $response->getContent()); 38 | } else { 39 | // for greater than 60 (default limit), it will throttle 40 | $this->assertNotEquals($totalRateLimit - $i, $rateLimitRemaining); 41 | $this->assertFalse($response->isOk()); 42 | $this->assertNotEquals($totalRateLimit - $i, $rateLimitRemaining); 43 | $this->assertEquals('{"status":429,"message":"Too Many Attempts."}', $response->getContent()); 44 | } 45 | } 46 | } 47 | 48 | public function testThrottleWorksInEndpointRequest() 49 | { 50 | $user = factory(User::class)->create(); 51 | 52 | // authenticate 53 | $user->withAccessToken(new Token(['scopes' => ['*']])); 54 | $this->actingAs($user); 55 | 56 | for ($i = 1; $i <= 65 ; $i++) { 57 | $this->call('GET', '/users/' . $user->uid); 58 | 59 | // for greater than 60 (default limit), it will throttle 60 | if ($i > 60) { 61 | $this->assertResponseStatus(429); 62 | } else { 63 | $this->assertResponseStatus(200); 64 | $this->seeJson(['email' => $user->email]); 65 | } 66 | 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Repositories/EloquentUserRepositoryTest.php: -------------------------------------------------------------------------------- 1 | eloquentUserRepository = new EloquentUserRepository(new User()); 24 | } 25 | 26 | public function testCreateUser() 27 | { 28 | $testUserArray = factory(User::class)->make()->toArray(); 29 | $user = $this->eloquentUserRepository->save($testUserArray); 30 | 31 | $this->assertInstanceOf(User::class, $user); 32 | $this->assertEquals($testUserArray['email'], $user->email); 33 | } 34 | 35 | public function testFindOne() 36 | { 37 | $testUser = factory(User::class)->create(); 38 | 39 | //first, check if it returns valid user 40 | $user = $this->eloquentUserRepository->findOne($testUser->uid); 41 | $this->assertInstanceOf(User::class, $user); 42 | 43 | //now check it returns null for gibberish data 44 | $user = $this->eloquentUserRepository->findOne('giberish'); 45 | $this->assertNull($user); 46 | } 47 | 48 | public function testFindOneBy() 49 | { 50 | $testUser = factory(User::class)->create(); 51 | 52 | //first, check if it returns valid user 53 | $user = $this->eloquentUserRepository->findOneBy(['uid' => $testUser->uid]); 54 | $this->assertInstanceOf(User::class, $user); 55 | $this->assertEquals($testUser->email, $user->email); 56 | 57 | //check if it returns valid user, for multiple criteria 58 | $user = $this->eloquentUserRepository->findOneBy([ 59 | 'email' => $testUser->email, 60 | 'firstName' => $testUser->firstName 61 | ]); 62 | $this->assertInstanceOf(User::class, $user); 63 | $this->assertEquals($testUser->firstName, $user->firstName); 64 | 65 | //now check it returns null for gibberish data 66 | $user = $this->eloquentUserRepository->findOneBy(['lastName' => 'Test Last']); 67 | $this->assertNull($user); 68 | } 69 | 70 | public function testFindBy() 71 | { 72 | // when instantiate the repo, logged in as Admin user. So, that we can search any user 73 | $adminUser = factory(User::class)->make(['role' => User::ADMIN_ROLE]); 74 | Auth::shouldReceive('user')->andReturn($adminUser); 75 | $eloquentUserRepository = new EloquentUserRepository(new User()); 76 | 77 | //get total users of this resource 78 | $totalUsers = User::all()->count(); 79 | 80 | //first, check if it returns all users without criteria 81 | $users = $eloquentUserRepository->findBy([]); 82 | $this->assertCount($totalUsers, $users); 83 | 84 | //create a user and findBy that using user's firstName 85 | factory(User::class)->create(['firstName' => 'Pappu']); 86 | $users = $eloquentUserRepository->findBy(['firstName' => 'Pappu']); 87 | //test instanceof 88 | $this->assertInstanceOf(LengthAwarePaginator::class, $users); 89 | $this->assertNotEmpty($users); 90 | 91 | //check with multiple criteria 92 | $searchCriteria = ['zipCode' => '11121', 'username' => 'jobberAli']; 93 | $previousTotalUsers = $eloquentUserRepository->findBy($searchCriteria)->count(); 94 | $this->assertEmpty($previousTotalUsers); 95 | 96 | factory(User::class)->create($searchCriteria); 97 | $newTotalUsers = $eloquentUserRepository->findBy($searchCriteria)->count(); 98 | $this->assertNotEmpty($newTotalUsers); 99 | 100 | //with basic user's permission, create a user and findBy using that user's firstName 101 | factory(User::class)->create(['firstName' => 'Jobber']); 102 | $users = $this->eloquentUserRepository->findBy(['firstName' => 'Jobber']); 103 | $this->assertEmpty($users); 104 | } 105 | 106 | public function testUpdate() 107 | { 108 | $testUser = factory(User::class)->create([ 109 | 'firstName' => 'test_first', 110 | 'lastName' => 'test_last' 111 | ]); 112 | 113 | // First, test user instance 114 | $user = $this->eloquentUserRepository->findOne($testUser->uid); 115 | $this->assertInstanceOf(User::class, $user); 116 | 117 | // Update user 118 | $this->eloquentUserRepository->update($testUser, [ 119 | 'firstName' => 'updated first_name', 120 | 'lastName' => 'updated last_name' 121 | ]); 122 | 123 | // Fetch the user again 124 | $user = $this->eloquentUserRepository->findOne($testUser->uid); 125 | $this->assertEquals('updated first_name', $user->firstName); 126 | $this->assertEquals('updated last_name', $user->lastName); 127 | $this->assertNotEquals('test_first', $user->firstName); 128 | } 129 | 130 | public function testDelete() 131 | { 132 | $testUser = factory(User::class)->create(); 133 | 134 | $isDeleted = $this->eloquentUserRepository->delete($testUser); 135 | $this->assertTrue($isDeleted); 136 | 137 | // confirm deleted 138 | $user = $this->eloquentUserRepository->findOne($testUser->uid); 139 | $this->assertNull($user); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |