DEV Community

Cover image for Building a Movie Portal API with Laravel Using the Service Pattern
syed kamruzzaman
syed kamruzzaman

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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']);
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Setting Up Controllers
Generate the necessary controllers:

php artisan make:controller Api/CategoryController
php artisan make:controller Api/MovieController

Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Add Requests

php artisan make:request CategoryRequest
php artisan make:request MovieRequest
Enter fullscreen mode Exit fullscreen mode

# CategoryRequest

public function rules(): array
    {
        // Get the category ID from the route parameters

        return [
            'name' => ['required', 'unique:categories,name'],
        ];

    }
Enter fullscreen mode Exit fullscreen mode

# 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',

        ];
    }
Enter fullscreen mode Exit fullscreen mode

Step 8: Add Resources

php artisan make:resource CategoryResource
php artisan make:resource MovieResource
php artisan make:resource RegisterResource
php artisan make:resource LoginResource
Enter fullscreen mode Exit fullscreen mode

# 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
        ];


    }
Enter fullscreen mode Exit fullscreen mode

# 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),
        ];
    }
Enter fullscreen mode Exit fullscreen mode

# 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,
        ];
    }
Enter fullscreen mode Exit fullscreen mode

# 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,

        ];
    }
Enter fullscreen mode Exit fullscreen mode

Step 9: Add Service
Here you make folder Services in app folder. Then make four files

  1. CategoryService
  2. ImageStoreService
  3. MovieService
  4. 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();
    }


}
Enter fullscreen mode Exit fullscreen mode

# 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;
    }

}
Enter fullscreen mode Exit fullscreen mode

# 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

# 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();
    }

}
Enter fullscreen mode Exit fullscreen mode

Step 10: VerifyCsrfToken
Now you go app/Http/Middleware/VerifyCsrfToken and add this line

protected $except = [
        'api/*'
    ];

Enter fullscreen mode Exit fullscreen mode

Now you testing your api to ensure to work. Like these

Image description

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)