In this tutorial, we'll be constructing an API for a movie portal. Our goal is to maintain a clean, scalable codebase by utilizing the Service Pattern and adhering to the DRY (Don't Repeat Yourself) principle.
Step 1: Setting Up Laravel
Firstly, install Laravel:
composer create-project laravel/laravel Laravel-movie-api
Step 2: Configuring the Database
After setting up Laravel, you'll need to configure your database. Update the .env file with your database credentials:
APP_URL=http://localhost:8000 FRONTEND_URL=http://localhost:3000 SESSION_DOMAIN=localhost SANCTUM_STATEFUL_DOMAINS=localhost:3000 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=[your_database_name] DB_USERNAME=[your_database_username] DB_PASSWORD=[your_database_password]
Step 3: Handling Authentication
For the sake of brevity, we won't delve deeply into authentication. However, you can utilize Laravel Breeze for API authentication.
Step 4: Creating Tables and Models
To construct our database structure, run the following commands to create migrations and models:
php artisan make:model Movie -m php artisan make:model Category -m
Movie Table Schema
Within the generated migration for movies, insert:
public function up(): void { Schema::create('movies', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('description')->nullable(); $table->string('image'); $table->unsignedBigInteger('category_id'); $table->unsignedInteger('views')->nullable(); $table->unsignedInteger('likes')->nullable(); $table->timestamps(); }); }
Category Table Schema
For the category migration, use:
public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('image')->nullable(); $table->timestamps(); }); }
Models
For the Movie model:
protected $fillable = [ 'title', 'category_id', 'description', 'image', 'views', 'likes', ]; public function categories() { return $this->belongsTo(Category::class, 'category_id', 'id'); }
For the Category model:
protected $fillable = [ 'name', 'image', ]; public function movies() { return $this->hasMany(Movie::class, 'category_id', 'id'); } public function topMovies() { return $this->hasManyThrough(Movie::class, Category::class, 'id', 'category_id') ->orderBy('created_at', 'desc')->limit(3); }
Step 5: Defining Routes
Within the api.php file in the Routes directory, add:
/movie Route::get('all-movies', [MovieController::class, 'allMovie']); Route::get('top-movies', [MovieController::class, 'topMovies']); Route::get('category-wise-movies', [CategoryController::class, 'categoryWiseMovies']); Route::get('single-movie/{movie}', [MovieController::class, 'singleMovie']); Route::post('ai-movie-store', [MovieController::class, 'aiMovieStore']); //Category Route::get('all-category', [CategoryController::class, 'allCategory']); Route::get('single-category/{category}', [CategoryController::class, 'singleCategory']); Route::group(['middleware' => ['auth:sanctum']], function () { //movie Route::post('movie-store', [MovieController::class, 'store']); Route::post('movie-update/{movie}', [MovieController::class, 'update']); Route::delete('movie-delete/{movie}', [MovieController::class, 'delete']); //Category Route::post('category-store', [CategoryController::class, 'store']); Route::post('category-update/{category}', [CategoryController::class, 'update']); Route::delete('category-delete/{category}', [CategoryController::class, 'delete']); });
Step 6: Setting Up Controllers
Generate the necessary controllers:
php artisan make:controller Api/CategoryController php artisan make:controller Api/MovieController
Within the CategoryController:
class CategoryController extends Controller { protected CategoryService $categoryService; public function __construct(CategoryService $categoryService) { $this->categoryService = $categoryService; } public function allCategory():JsonResponse { $data = $this->categoryService->allCategory(); $formatedData = CategoryResource::collection($data); return $this->successResponse($formatedData, 'All Category data Show', 200); } public function categoryWiseMovies() { $data = $this->categoryService->categoryWiseMovies(); $formatedData = CategoryResource::collection($data)->response()->getData(); return $this->successResponse($formatedData, 'Top Movie data Show', 200); } public function singleCategory(Category $category):JsonResponse { $formatedData = new CategoryResource($category); return $this->successResponse($formatedData, 'Single Category data Show', 200); } public function store(CategoryRequest $request):JsonResponse { try{ $data = $this->categoryService->store($request); return $this->successResponse($data, 'Category Store Successfully', 200); }catch(\Exception $e ){ Log::error($e); return $this->errorResponse(); } } public function update(Category $category, Request $request):JsonResponse { $data = $this->categoryService->update($category, $request); return $this->successResponse($data, 'Category Update Successfully', 200); } public function delete(Category $category):JsonResponse { $data = $this->categoryService->delete($category); return $this->successResponse($data, 'Category Delete Successfully', 200); } }
Similarly, for the MovieController:
class MovieController extends Controller { protected MovieService $movieService; public function __construct(MovieService $movieService) { $this->movieService = $movieService; } public function allMovie():JsonResponse { $data = $this->movieService->allMovieShow(); $formatedData = MovieResource::collection($data)->response()->getData(); return $this->successResponse($formatedData, 'All Movie data Show', 200); } public function topMovies() { $data = $this->movieService->topMovies(); $formatedData = MovieResource::collection($data)->response()->getData(); return $this->successResponse($formatedData, 'Top Movie data Show', 200); } public function singleMovie(Movie $movie):JsonResponse { $data = new MovieResource($movie); return $this->successResponse($data, 'Single Movie data Show', 200); } public function store(MovieRequest $request):JsonResponse { //return response()->json($request->all()); try{ $data = $this->movieService->store($request); return $this->successResponse($data, 'Movie Store Successfully', 200); }catch(\Exception $e ){ Log::error($e); return $this->errorResponse(); } } public function aiMovieStore(Request $request) { try{ $data = $this->movieService->aiStore($request); return $this->successResponse($data, 'Movie Store Successfully', 200); }catch(\Exception $e ){ Log::error($e); return $this->errorResponse(); } } public function update(Movie $movie, Request $request):JsonResponse { $data = $this->movieService->update($movie, $request); return $this->successResponse($data, 'Movie Update Successfully', 200); } public function delete(Movie $movie):JsonResponse { $data = $this->movieService->delete($movie); return $this->successResponse($data, 'Movie Delete Successfully', 200); } }
Step 7: Add Requests
php artisan make:request CategoryRequest php artisan make:request MovieRequest
# CategoryRequest
public function rules(): array { // Get the category ID from the route parameters return [ 'name' => ['required', 'unique:categories,name'], ]; }
# MovieRequest
public function rules(): array { return [ 'title' => ['required'], 'image' => ['required', 'image', 'mimes:jpeg,png,webp', 'max:2048'], 'category_id' => ['required'], ]; } public function messages() { return [ 'title.required' => 'Please write Your title', 'image.required' => 'Please Upload image', 'category_id.required' => 'Please write Your Category', ]; }
Step 8: Add Resources
php artisan make:resource CategoryResource php artisan make:resource MovieResource php artisan make:resource RegisterResource php artisan make:resource LoginResource
# CategoryResource
public function toArray(Request $request): array { $rootUrl = config('app.url'); return [ 'id' => $this->id, 'name' => $this->name, //'image' => $this->image, 'image' => $this->image ? $rootUrl . Storage::url($this->image) : null, 'movies' => $this->movies ]; }
# MovieResource
public function toArray(Request $request): array { $rootUrl = config('app.url'); return [ 'id' => $this->id, 'title' => $this->title, 'description' => $this->description, //'image' => $this->image, 'image' => $this->image ? $rootUrl . Storage::url($this->image) : null, 'category_info' => new CategoryResource( $this->categories), ]; }
# RegisterResource
public function toArray( $request ): array { $token = $this->resource->createToken( 'access_token', ['*'], Carbon::now()->addMinutes( 15 ) ) ->plainTextToken; return [ 'user_id' => $this->id, 'email' => $this->email, 'token' => $token, ]; }
# LoginResource
public function toArray(Request $request): array { return [ 'token' => $this->resource->createToken('access_token', ['*'], Carbon::now()->addMinutes(60)) ->plainTextToken, 'user_id' => $this->id, 'email' => $this->email, 'name' => $this->name, ]; }
Step 9: Add Service
Here you make folder Services in app folder. Then make four files
- CategoryService
- ImageStoreService
- MovieService
- UserService
# CategoryService
class CategoryService { protected ImageStoreService $imageStoreService; public function __construct(ImageStoreService $imageStoreService) { $this->imageStoreService = $imageStoreService; } /** * allCategory * * @return mixed */ public function allCategory(): mixed { return Category::all(); } public function store($request) { $imagePath = $this->imageStoreService->handle('public/categories', $request->file('image')); return Category::create([ 'name' => $request->name, 'image' => $imagePath !== false ? $imagePath : 'public/movies/default.jpg', ]); } public function categoryWiseMovies() { return Category::with('movies')->get(); //return Category::with('movies')->get(); } /** * Update a category. * * @param Category $category The category to update. * @param Illuminate\Http\Request $request The request containing the updated data. * @return bool Whether the update was successful or not. */ public function update($category, $request): bool { if ($request->hasFile('image')) { //1st delete previous Image if ($category->image) { Storage::delete($category->image); } //2nd new Image store $imagePath = $this->imageStoreService->handle('public/categories', $request->file('image')); } return $category->update([ 'name' => $request->name ? $request->name : $category->name, 'image' => $request->hasFile('image') ? $imagePath : $category->image, ]); } /** * Delete a category. * * @param Category $category The category to delete. * @return bool Whether the deletion was successful or not. */ public function delete($category): bool { if ($category->image) { Storage::delete($category->image); } return $category->delete(); } }
# ImageStoreService
class ImageStoreService { /** * Handle storing an image file. * * @param string $destinationPath The destination path where the image will be stored. * @param mixed $file The image file to store. * @return string|false The path where the image is stored, or false if there was an issue storing the file. */ public function handle( $destinationPath = 'public/images', $file ) { $imageName = rand( 666561, 544614449 ) . '-' . time() . '.' . $file->extension(); $path = $file->storePubliclyAs( $destinationPath, $imageName ); # were created but are corrupt $fileSize = Storage::size( $path ); if ( $fileSize === false ) { return false; } return $path; } /** * Handle storing an image file from base64 data. * * @param string $destinationPath The destination path where the image will be stored. * @param string $base64Data The base64 encoded image data to store. * @return string|false The path where the image is stored, or false if there was an issue storing the file. */ public function handleBase64( $destinationPath = 'public/images', $base64Data ) { // Extract image format and data from the base64 string $matches = []; preg_match( '/data:image\/(.*?);base64,(.*)/', $base64Data, $matches ); if ( count( $matches ) !== 3 ) { // Invalid base64 data format return false; } $imageFormat = $matches[1]; // Get the image format (e.g., 'jpeg', 'png', 'gif', etc.) $imageData = base64_decode( $matches[2] ); // Get the binary image data // Generate a unique image name $imageName = rand( 666561, 544614449 ) . '-' . time() . '.' . $imageFormat; // Determine the full path to save the image $path = $destinationPath . '/' . $imageName; // Save the image to the specified path $isStored = Storage::put( $path, $imageData ); if ( !$isStored ) { return false; } return $path; } }
# MovieService
class MovieService { protected ImageStoreService $imageStoreService; public function __construct( ImageStoreService $imageStoreService ) { $this->imageStoreService = $imageStoreService; } public function allMovieShow(): mixed { return Movie::with( 'categories' )->paginate( 15 ); } public function topMovies(): mixed { return Movie::with( 'categories' )->orderBy( 'created_at', 'desc' )->limit( 8 )->get(); } public function store( $request ) { $imagePath = $this->imageStoreService->handle( 'public/movies', $request->file( 'image' ) ); return Movie::create( [ 'title' => $request->title, 'description' => $request->description, 'image' => $imagePath !== false ? $imagePath : 'public/movies/default.jpg', 'category_id' => $request->category_id, ] ); } public function aiStore( $request ) { $imagePath = $this->imageStoreService->handleBase64( 'public/movies', $request->base64Data ); return Movie::create( [ 'title' => $request->title, 'description' => $request->description, 'image' => $imagePath !== false ? $imagePath : 'public/movies/default.jpg', 'category_id' => $request->category_id, ] ); } public function update( $movie, $request ) { if ( $request->hasFile( 'image' ) ) { //1st delete previous Image if ( $movie->image ) { Storage::delete( $movie->image ); } //2nd new Image store $imagePath = $this->imageStoreService->handle( 'public/movies', $request->file( 'image' ) ); } return $movie->update( [ 'title' => $request->filled( 'title' ) ? $request->title : $movie->title, 'description' => $request->filled( 'description' ) ? $request->description : $movie->description, 'image' => $request->hasFile( 'image' ) ? $imagePath : $movie->image, 'category_id' => $request->filled( 'category_id' ) ? $request->category_id : $movie->category_id, ] ); } public function delete( $movie ) { if ( $movie->image ) { Storage::delete( $movie->image ); } return $movie->delete(); } }
# UserService
class UserService { /** * @param $data * @return mixed */ public function register( $data ) { return User::create( [ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make( $data['password'] ), ] ); } /** * @param User $user * @return int * @throws \Exception */ public function createTwoFactorCode( User $user ) { $twoFactorCode = random_int( 100000, 999999 ); $user->TwoFactorCode = $twoFactorCode; $user->TwoFactorExpiresAt = Carbon::now()->addMinute( 10 ); $user->save(); return $twoFactorCode; } /** * @param User $user */ public function resetTwoFactorCode( User $user ) { $user->TwoFactorCode = null; $user->TwoFactorExpiresAt = null; $user->save(); } /** * @param $data * @param User $user */ public function updateUserCredentials( $data, User $user ) { $user->Password = Hash::make( $data['Password'] ); $user->save(); } }
Step 10: VerifyCsrfToken
Now you go app/Http/Middleware/VerifyCsrfToken and add this line
protected $except = [ 'api/*' ];
Now you testing your api to ensure to work. Like these
Here is github link of this project
https://github.com/kamruzzamanripon/laravel-movie-api
All Episodes
Creating the API # [Tutorial-1]
Configure AI Prompt # [Tutorial-2]
Designing the UI # [Tutorial-3]
Setting up on an Linux Server # [Tutorial-4]
That's all. Happy Learning :) .
[if it is helpful, giving a star to the repository 😇]
Top comments (0)