Building a Movie Portal API with Laravel Using the Service Pattern
Tags: laravel, api, dry, ai
Published: 2023-10-31 21:06:53

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

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

    
}
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6thwpylebawl6xly7ier.png)

**Here is github link of this project**
https://github.com/kamruzzamanripon/laravel-movie-api

**All Episodes**
[Creating the API # [Tutorial-1]](https://dev.to/kamruzzaman/building-a-movie-portal-api-with-laravel-using-the-service-pattern-35i3)
[Configure AI Prompt # [Tutorial-2]](https://dev.to/kamruzzaman/setting-up-ai-prompt-in-nuxtjs-16e6)
[Designing the UI # [Tutorial-3]](https://dev.to/kamruzzaman/laravel-api-integration-in-nuxtjs-b89)
[Setting up on an Linux Server # [Tutorial-4]](https://dev.to/kamruzzaman/deploy-a-nuxtjs-app-and-laravel-api-on-a-linux-server-54bh)

That's all. Happy Learning :) .
[if it is helpful, giving a star to the repository 😇]