In the tutorial we use laravel as API only with features (notice that the Laravel version is 8.*):
- Register, login, logout user
- Authentication by Sanctum package
- CRUD api example
- Handle exception
- Bind middleware for api request
- Handle upload file
- Fetch output response by API Resource
- Relation between Model (Video belong to User)
- Using database transaction
For authentication use sanctum package
composer require laravel/sanctum
Next, you should publish the Sanctum configuration and migration files using the vendor:publish Artisan
command. The sanctum configuration file will be placed in your application’s config directory:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Change config to default is api at /config/auth.php
'defaults' => [ 'guard' => 'api', 'passwords' => 'users', ],
For api prefix should be version so add each version in one file. For example v1 is routes/api.php
and v2 is routes/apiv2.php
Register api prefix in /app/Providers/RouteServiceProvider.php
public function boot() { $this->configureRateLimiting(); $this->routes(function () { Route::prefix('v1') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); Route::prefix('v2') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/apiv2.php')); Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); }); }
Route for v1 /routes/api.php
Use Route::apiResource
instead of default Route::resource
to exclude routes that present HTML templates such as create
and edit
Use middleware auth:sanctum
for authentication
Route::post('/login', 'App\Http\Controllers\API\WordpressAuthController@login'); Route::post('/register', 'App\Http\Controllers\API\WordpressAuthController@register'); Route::middleware(['auth:sanctum'])->group(function(){ Route::get('/user', 'App\Http\Controllers\API\WordpressAuthController@currentUser'); Route::post('/logout', 'App\Http\Controllers\API\WordpressAuthController@logout'); Route::apiResource('/video', 'App\Http\Controllers\API'); });
The routes will like below
Verb | URI | Action | Route Name |
---|---|---|---|
POST | /v1/register |
register | |
POST | /v1/login |
login | |
GET | /v1/user |
currentUser | |
POST | /v1/logout |
logout | |
GET | /v1/videos |
index | videos.index |
POST | /v1/videos |
store | videos.store |
GET | /v1/videos/{video} |
show | videos.show |
PUT/PATCH | /v1/videos/{video} |
update | videos.update |
DELETE | /v1/videos/{video} |
destroy | videos.destroy |
Middleware
Generate middleware to handle api request by command (optional)
php artisan make:middleware Api
Change handle function of middleware /app/Middleware/Api.php
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Auth\Access\Gate; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Response; class Api { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next, $role = "view") { $request->headers->set('Accept', 'application/json'); return $next($request); } }
Add the middleware to all routes in $middlewareGroup
inside /app/Http/Kernel.php
/** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Laravel\Jetstream\Http\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\HandleInertiaRequests::class, ], 'api' => [ // 'throttle:api', App\Http\Middleware\Api::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ];
Controller and Model
Generate auth controller by command
php artisan make:controller /API/AuthController
Controller Api Auth
<?php namespace App\Http\Controllers\API; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\Hash; class AuthController extends Controller { public function currentUser(Request $request){ $user = $request->user(); return $user; } public function register(Request $request) { $validatedData = $request->validate([ 'email' => 'email|required|unique:wp_users', 'password' => 'required', ]); $validatedData['password'] = Hash::make($validatedData['user_password']); $user = User::create($validatedData); $accessToken = $user->createToken('authToken')->plainTextToken; return response([ 'user' => $user, 'access_token' => $accessToken]); } public function login(Request $request) { $loginData = $request->validate([ 'email' => 'email|required', 'password' => 'required' ]); $user = User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { return response(['message' => 'Invalid password' ],400); } $accessToken = $user->createToken('authToken')->plainTextToken; return response(['user' => $user, 'access_token' => $accessToken]); } public function logout(Request $request){ $request->user()->currentAccessToken()->delete(); return response(['message' => 'Logout Success']); } }
To Add meta for User
, we create new table user_meta
Run command create user_meta model and migration
php artisan make:model UserMeta php artisan make:migration create_user_meta
Modify /app/Models/UserMeta.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Models\User; class UserMeta extends Model { use HasFactory; protected $table = 'user_meta'; public $timestamps = false; protected $fillable = [ 'user_id','photo_path','is_verified' ]; public function user() { return $this->belongsTo(User::class,'user_id','id'); } }
migration user_meta table
public function up() { Schema::create('user_meta', function (Blueprint $table) { $table->id('user_id'); $table->string('photo_path',500); $table->string('photo_verify',500); $table->boolean('is_verified'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('api_usermeta'); }
Now to make sure user
table is created with meta
in same time we use transaction
. Modify /app/Http/Controllers/AuthController.php function register
use Illuminate\Support\Facades\DB; use App\Models\UserMeta; public function register(Request $request) { $validatedData = $request->validate([ 'email' => 'email|required|unique:wp_users', 'password' => 'required', ]); $validatedData['password'] = Hash::make($validatedData['user_password']); $user = DB::transaction(function () use ($validatedData) { $user = User::create($validatedData); UserMeta::create([ 'user_id' => $user->ID, 'is_verified' => false, 'photo_path' => '' ]); return $user; }, 5); //5 is time transaction rerun if exception happen $accessToken = $user->createToken('authToken')->plainTextToken; return response([ 'user' => $user, 'access_token' => $accessToken]); }
Before generate VideoController we need to know that we need to response path of video as url that can be show on browser so we need to fetch path as url for each video. This case we need API Resource
Generate VideoResource
php artisan make:resource VideoResource
Change /app/Http/VideoResource.php
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Storage; class VideoResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'user' => $this->user, 'title' => $this->title, 'description' => $this->description, 'path' => Storage::url($this->path) ]; } }
Generate video controller and model by command
php artisan make:controller /API/VideoController --resource --api --model=Video
Model /app/Models/Video.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Video extends Model { use HasFactory; protected $table = 'api_videos'; public $timestamps = true; protected $casts = [ 'cost' => 'float' ]; protected $fillable = [ 'title', 'description', 'path' ]; public function user(){ return $this->belongsTo(User::class,'user_id','ID'); } }
Run command to generate migration table
php artisan make:migration add_video --create=api_videos
After that change migration file
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class AddVideo extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('api_videos', function (Blueprint $table) { $table->id(); $table->string('title',255); $table->string('description',2000); $table->string('path',500); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('api_videos'); } }
resource: create controller with full function for route:resource
api: exclude function that generate html view
model: generate model and inject to the controller
Controller app/http/Controller/API/VideoController.php
<?php namespace App\Http\Controllers\API; use App\Http\Controllers\Controller; use App\Http\Resources\VideoResource; use App\Models\Video; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; class VideoController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // $videos = VideoResource::collection(Video::with('user')->latest()->paginate(5)); return response(['data'=>$videos]); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $validData = $request->validate([ 'title' => 'required', 'description' => 'required', 'path' => 'required|mimes:mp4|max:100000', ]); $user = $request->user(); $path = Storage::disk()->put($user->ID, $request->path); $validData['user_id'] = $request->user()->ID; $validData['path'] = $path; $result = new VideoResource(Video::create($validData)); return response(['data'=>$result, 'message'=> 'Video is created']); } /** * Display the specified resource. * * @param \App\Models\Video $video * @return \Illuminate\Http\Response */ public function show(Video $video) { // return response(['data'=>$video]); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param \App\Models\Video $video * @return \Illuminate\Http\Response */ public function update(Request $request, Video $video) { $request->validate([ 'title' => 'required', 'description' => 'required', ]); $result = $video->update($request->all()); return response(['data'=>$result, 'message'=> 'Video is updated']); } /** * Remove the specified resource from storage. * * @param \App\Models\Video $video * @return \Illuminate\Http\Response */ public function destroy(Request $request, Video $video) { if($video->user_id != $request->user()->id){ return response()->json(['message' => 'You can only delete your own video.'], 403); } Storage::delete($video->path); $video->delete(); return response(['message'=> 'Video is deleted']); } }
More custom for handle error and response as json
Response when call GET /v1/video/
{ "data": [ { "id": 12, "user": { "id": 5, "email": "user1@test.com", "user_status": 0, "display_name": "Vuong anh duong" }, "title": "test title", "description": "test description", "path": "http://localhost:8084/store/5/hnDd4kTE8qICp0RiZQ6pe5tEC1AJX8pKojrDq3GF.mp4" }, { "id": 11, "user": { "id": 5, "email": "user1@test.com", "user_status": 0, "display_name": "Vuong anh duong" }, "title": "test title", "description": "test description", "path": "http://localhost:8084/store/5/4qD7Hb9C35vHDToQXkj6FvW5WDa4aqTn321FtWKZ.mp4" } ] }
It is optional because MiddleApi have passed Accept: application/json
to all request then laravel will response as json
Add Trait for Rest api \app\Exceptions\Traits\RestExceptionHandlerTrait.php
<?php namespace App\Exceptions\Traits; use Exception; use Illuminate\Http\Request; use Illuminate\Database\Eloquent\ModelNotFoundException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; trait RestExceptionHandlerTrait { /** * Creates a new JSON response based on exception type. * * @param Request $request * @param Exception $e * @return \Illuminate\Http\JsonResponse */ protected function getJsonResponseForException(Request $request, Exception $e) { if($this->isModelNotFoundException($e)) return $this->modelNotFound('Item not found'); if($this->isNotFoundException($e)) return $this->modelNotFound('Not found'); return parent::render($request, $e); } /** * Returns json response for generic bad request. * * @param string $message * @param int $statusCode * @return \Illuminate\Http\JsonResponse */ protected function badRequest($message = 'Bad request', $statusCode = 400) { return $this->jsonResponse(['error' => $message], $statusCode); } /** * Returns json response for Eloquent model not found exception. * * @param string $message * @param int $statusCode * @return \Illuminate\Http\JsonResponse */ protected function modelNotFound($message = 'Record not found', $statusCode = 404) { return $this->jsonResponse(['error' => $message], $statusCode); } /** * Returns json response. * * @param array|null $payload * @param int $statusCode * @return \Illuminate\Http\JsonResponse */ protected function jsonResponse(array $payload = null, $statusCode = 404) { $payload = $payload ?: []; return response()->json($payload, $statusCode); } /** * Determines if the given exception is an Eloquent model not found. * * @param Exception $e * @return bool */ protected function isModelNotFoundException(Exception $e) { return $e instanceof ModelNotFoundException; } protected function isNotFoundException(Exception $e){ return $e instanceof NotFoundHttpException; } protected function isApiCall(Request $request) { return strpos($request->getUri(), '/v1/') !== false; } }
Change /app/Exception/Handler.php
<?php namespace App\Exceptions; use App\Exceptions\Traits\RestExceptionHandlerTrait; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Throwable; class Handler extends ExceptionHandler { use RestExceptionHandlerTrait; /** * A list of the exception types that are not reported. * * @var array */ protected $dontReport = [ // ]; /** * A list of the inputs that are never flashed for validation exceptions. * * @var array */ protected $dontFlash = [ 'password', 'password_confirmation', ]; /** * Register the exception handling callbacks for the application. * * @return void */ public function register() { $this->reportable(function (Throwable $e) { // }); } public function render($request, Throwable $e) { if(!$this->isApiCall($request)) { $retval = parent::render($request, $e); } else { $retval = $this->getJsonResponseForException($request, $e); } return $retval; } }