Building A Movie Portal API with Laravel Using the Service Pattern
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
That’s all. Happy Learning :) .
[if it is helpful, giving a star to the repository 😇]
All Episodes
Creating the API [Tutorial-1]:
Configure AI Prompt [Tutorial-2]:
https://medium.com/@rkamruzzaman/a3e959349ca0
Designing the UI [Tutorial-3]:
https://medium.com/@rkamruzzaman/93e96a5566b1