Build a Product Review REST API with Laravel
Tags: laravel, api
Published: 2020-07-31 20:15:22

In this tutorial, we will be building a product reviews API. Users will be able to add, view, update and delete products. Users will also be able to rate and review a product. We will also be implementing authentication functionality with JSON Web Tokens (JWT) to secure our API.

### Prerequisites

- Basic knowledge of Laravel
- Basic knowledge of REST APIs

### Creating a new project

First we need to create a new laravel project 

```bash
$ laravel new product-review-api
```

Then create your database and update the database credentials in the `.env`file.

### Create models and migrations

This API will be having 3 models: user, product and review. By default laravel comes with a user model, migration and factory file, so we will just be creating the remaining two. Let's start with the Product model:

```bash
$ php artisan make:model Product -a
```

The `-a` flag will create corresponding migration, controller, factory and seeder file for the model. We will be looking at the factory and seeder file in a bit.

Lets edit the product migration file as below:

```php
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->longText('description');
    $table->decimal('price');
    $table->unsignedBigInteger('user_id');
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');
});
```

We are just defining the columns for the products table and defining its relationship with the users table and what happens if the parent data is deleted.

Now we'll do the same for the Review model

```bash
$ php artisan make:model Review -a
```

And also define the reviews table structure

```php
Schema::create('reviews', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('user_id');
    $table->unsignedBigInteger('product_id');
    $table->text('review');
    $table->integer('rating');
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');

    $table->foreign('product_id')
        ->references('id')
        ->on('products')
        ->onDelete('cascade');
});
```

Run the migrate artisan command and head over to your database manager and you should see the users, products and reviews table.

```bash
$ php artisan migrate
```

### Model Relationships

While writing our migrations, we could see there's a relationship between the users, products and reviews table. Laravel ships with Eloquent ORM which provides a beautiful simple implementation for interacting with our tables.

Let's see what relationship exists between this tables.

- A user can add many products but a product can only belong to one user. This is a one to many relationship between user and product.
- A product can have many reviews but a review can only belong to one product. This is a one to many relationship between product and review.
- And finally user can make many reviews (be it same or different products) but a review can only belong to one user. This is a one to many relationship between the user and review.

Now that we can see the relationship, let's define this in our model. The reason we are doing this is that eloquent makes managing and working with relationships easy, and supports a lot of relationship types.

In the User model (app\User.php), let us define the relationship between the user and the other models.

```php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    // Rest omitted for brevity
		
    /**
    * Get the products the user has added.
    */
     public function products()
     {
         return $this->hasMany('App\Product');
     }
		
     /**
     * Get the reviews the user has made.
     */
     public function reviews()
     {
	return $this->hasMany('App\Review');
     }
}
```

On the product model we will define the relationship between the product and the review model, and also the inverse relationship to the user model.

```php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    /**
     * Get the reviews of the product.
     */
    public function reviews()
    {
        return $this->hasMany('App\Review');
    }

    /**
     * Get the user that added the product.
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}
```

On the review model we will define the inverse relationship to the user and product model.

```php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Review extends Model
{
    /**
     * Get the product that owns the review.
     */
    public function product()
    {
        return $this->belongsTo('App\Product');
    }

    /**
     * Get the user that made the review.
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}
```

### Database Seeding

Remember the factory and seeder file created when we ran the make:model command with the -a flag? Now it's the time to see this files in action.

Database seeding in summary is just populating tables with data we want to develop. In essence this means having fake data which we can be used for but not limited to testing, demo, building routes or views to consume this data, initial setup etc. without the need to start inputting this data manually.

Laravel ships with a PHP faker library **[Faker](https://github.com/fzaninotto/Faker)** which is an awesome library by the way and this is the library used in creating the fake data. 

Opening the UserFactory file which is included in by default, we see that how this works is we define the model we want to generate fake data for and return an associative array in which the key is the attributes on the model and the value is the relevant property or method on the faker library. 

Now it's time to do same for the ProductFactory

```php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Product;
use App\User;
use Faker\Generator as Faker;

$factory->define(Product::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
        'description' => $faker->paragraph,
        'price' => $faker->numberBetween(1000, 20000),
        'user_id' => function() {
            return User::all()->random();
        },
    ];
});
```

And ReviewFactory 

```php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Review;
use App\User;
use Faker\Generator as Faker;

$factory->define(Review::class, function (Faker $faker) {
    return [
        'review' => $faker->paragraph,
        'rating' => $faker->numberBetween(0, 5),
        'user_id' => function() {
            return User::all()->random();
        },
    ];
});
```

Next is to write out our seeders. According to laravel's documentation

> A seeder class only contains one method by default: run. This method is called when the db:seed Artisan command is executed. Within the run method, you may insert data into your database however you wish or use model factories to conveniently generate large amounts of database records.

While laravel comes with default user model, migration and factory files, it doesnt include a user seeder class. So we need to create one by running the command:

```bash
$ php artisan make:seeder UserSeeder
```

This will create a `UserSeeder.php` file in the `database\seeds folder`.  Open the file and edit

```php
/**
 * Run the database seeds.
 *
 * @return void
 */
public function run()
{
    factory(App\User::class, 50)->create();
}
```

We are using the factory helper method to insert 50 user records into the users table.

To run our seeders, open the `database\seeds\DatabaseSeeder.php` file and uncomment the line

```php
$this->call(UserSeeder::class);
```

Then we can use the db:seed artisan command to seed the database. Head over to your users and you should see 50 user records created.

```bash
$ php artisan db:seed
```

Cool! Let's do the same for the ProductSeeder with a little twist

```php
<?php

use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\Product::class, 100)->create()->each(function ($product) {
            $product->reviews()->createMany(factory(App\Review::class, 5)->make()->toArray());
        });
    }
}
```

No magic here, what we doing is simply creating 100 products and for each product we are creating 5 reviews for that product.

To call additional seeders in our `DatabaseSeeder.php` file, we pass an array instead to the call method

```php
$this->call([
    UserSeeder::class,
    ProductSeeder::class,
]);
```

Let's use the **`migrate:fresh`** command, which will drop all tables and re-run all of our migrations. This command is useful for completely re-building our database. The `--seed` flag instructs laravel to seed the database once the migration is completed.

```bash
$ php artisan migrate:fresh --seed
```

Check your tables and Voila! I know this section might be overwhelming. If you are feeling overwhelmed by database seeding, I'll recommend going through laravel's documentation or search online for materials (there are a lot out there).

### Authentication

To secure our application we will be using the package **[jwt-auth](https://github.com/tymondesigns/jwt-auth)** for authentication with JSON Web Tokens (JWT). Run the command to install the package latest version:

```bash
$ composer require tymon/jwt-auth
```

**Note:** If you are using Laravel 5.4 and below, you will need to manually register the service provider. Add the service provider to the providers array in the **`config/app.php`** config file as follows:

```php
'providers' => [

    ...

    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
```

Next is to publish the package config file:

```bash
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

```

You should now have a **`config/jwt.php`** file that allows you to configure the basics of this package.

Next is to run the command below to generate a secret key. This secret key will be used to sign our tokens. 

```bash
$ php artisan jwt:secret

```

This will update the `.env` file with something like `JWT_SECRET=value`

Before we can start authenticating our users using JWT we need to update the user model to implement the `Tymon\JWTAuth\Contracts\JWTSubject` contract, which requires we implement the 2 methods `getJWTIdentifier()` and `getJWTCustomClaims()`.

```php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
		// Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}
```

To use laravel's built in auth system, with **jwt-auth** doing the heavy lifting we need to make a few changes to the `config/auth.php`.

```
'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
	'hash' => false,
    ],
],

```

Here we are telling the `api` guard to use the `jwt` driver, and setting the `api` guard as the default auth guard.

Now we can begin implementing the authentication logic into our application. Let's start by creating an AuthController

```bash
$ php artisan make:controller AuthController
```

Let's add a few methods to this controller.

```php
<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    public function __construct()
    {
      $this->middleware('auth')->except(['register', 'login']);
    }

    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'email' => 'required|string|unique:users',
            'password' => 'required|string|min:8',
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password),
        ]);
        $token = auth()->login($user);
        return $this->respondWithToken($token);
    }

    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|string',
            'password' => 'required|string',
        ]);
        $credentials = $request->only(['email', 'password']);
        if (!$token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Invalid Credentials'], 401);
        }
        return $this->respondWithToken($token);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth()->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();
        return response()->json(['message' => 'Successfully logged out']);
    }

}
```

We use the `auth` middleware  to only allow authenticated users to access the methods while exempting the register and login methods. The `register` method creates and stores a new user into the database while the `login` method which logs user into the application. This two methods returns a JWT response by calling the `respondWithToken()` method. The `me` method returns the currently authenticated user while the `logout` method logs out the currently authenticated user from the application.

Let's define our api routes. Open up the `routes/api.php` and add the following routes.

```php
Route::get('me', 'AuthController@me');
Route::post('login', 'AuthController@login');
Route::post('register', 'AuthController@register');
Route::post('logout', 'AuthController@logout');
```

Test the endpoints and we should see authentication working.

### Product Endpoints

Let's begin by defining the product endpoints. 

- `GET` `/products` - Fetch all products
- `GET` `/products/:id` - Fetch a single product and its reviews
- `POST` `/products` - Create a product
- `PUT` `/products/:id` - Update a product
- `DELETE` `/products/:id` - Delete a product

In our `routes/api.php` file we can define this routes individually or use laravel resource routing feature or even better still use the api resource route feature. According to the laravel docs

> When declaring resource routes that will be consumed by APIs, you will commonly want to exclude routes that present HTML templates such as create and edit. For convenience, you may use the apiResource method to automatically exclude these two routes:

So we add the products routes in the route file:

```php
Route::apiResource('products', 'ProductController');
```

In the `ProductController.php` file

```php
<?php

namespace App\Http\Controllers;

use App\Product;
use App\Review;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function __construct()
    {
      $this->middleware('auth')->except(['index', 'show']);
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $products = Product::with('user:id,name')
            ->withCount('reviews')
            ->latest()
            ->paginate(20);
        return response()->json(['products' => $products]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'description' => 'required|string',
            'price' => 'required|numeric|min:0',
        ]);

        $product = new Product;
        $product->name = $request->name;
        $product->description = $request->description;
        $product->price = $request->price;

        auth()->user()->products()->save($product);
        return response()->json(['message' => 'Product Added', 'product' => $product]);
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Product  $product
     * @return \Illuminate\Http\Response
     */
    public function show(Product $product)
    {
        $product->load(['reviews' => function ($query) {
            $query->latest();
        }, 'user']);
        return response()->json(['product' => $product]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Product  $product
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Product $product)
    {
        if (auth()->user()->id !== $product->user_id) {
            return response()->json(['message' => 'Action Forbidden']);
        }
        $request->validate([
            'name' => 'required|string',
            'description' => 'required|string',
            'price' => 'required|numeric',
        ]);

        $product->name = $request->name;
        $product->description = $request->description;
        $product->price = $request->price;
        $product->save();

        return response()->json(['message' => 'Product Updated', 'product' => $product]);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Product  $product
     * @return \Illuminate\Http\Response
     */
    public function destroy(Product $product)
    {
        if (auth()->user()->id !== $product->user_id) {
            return response()->json(['message' => 'Action Forbidden']);
        }
        $product->delete();
        return response()->json(null, 204);
    }
}
```

In the constructor we are using the **auth** middleware but exempting the index and show methods from using the middleware. This means unauthenticated users will be able to view all products and a single product which is okay for our api.

The **index** method, returns a list of products ordered by the created date, the number of reviews the product has and paginates the records.

The **store** method validates the request input and then creates a new product and attaches the product to the currently authenticated user and returns the newly created product.

The **show** method returns a single product, the creator(user) and its reviews.

The **update** method checks the currently authenticated user trying to update the record is the user that created the record. If the user is not the creator, a forbidden response is returned else we carry out the update operation.

The **delete** method checks the currently authenticated user trying to delete the record is the user that created the record. If the user is not the creator, a forbidden response is returned else we carry out the delete operation.

### Review Endpoints

Let's begin by defining the review endpoints. 

- `POST` `/products/:id/reviews` - Create a review for a product
- `PUT` `/products/:id/reviews/:id` - Update a product review
- `DELETE` `/products/:id/reviews/:id` - Delete a product review

We define the apiResource route for the reviews and also specifying actions the controller should handle instead of the full set of default actions.

```php
Route::apiResource('products/{product}/reviews', 'ReviewController')
    ->only('store', 'update', 'destroy');
```

In the `ReviewController.php` file

```php
<?php

namespace App\Http\Controllers;

use App\Product;
use App\Review;
use Illuminate\Http\Request;

class ReviewController extends Controller
{
    public function __construct()
    {
      $this->middleware('auth');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Product  $product
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request, Product $product)
    {
        $request->validate([
            'review' => 'required|string',
            'rating' => 'required|numeric|min:0|max:5',
        ]);

        $review = new Review;
        $review->review = $request->review;
        $review->rating = $request->rating;
        $review->user_id = auth()->user()->id;

        $product->reviews()->save($review);
        return response()->json(['message' => 'Review Added', 'review' => $review]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Product  $product
     * @param  \App\Review  $review
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Product $product, Review $review)
    {
        if (auth()->user()->id !== $review->user_id) {
            return response()->json(['message' => 'Action Forbidden']);
        }
        $request->validate([
            'review' => 'required|string',
            'rating' => 'required|numeric|min:0|max:5',
        ]);

        $review->review = $request->review;
        $review->rating = $request->rating;
        $review->save();

        return response()->json(['message' => 'Review Updated', 'review' => $review]);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Product  $product
     * @param  \App\Review  $review
     * @return \Illuminate\Http\Response
     */
    public function destroy(Product $product, Review $review)
    {
        if (auth()->user()->id !== $review->user_id) {
            return response()->json(['message' => 'Action Forbidden']);
        }
        $review->delete();
        return response()->json(null, 204);
    }
}
```

### **Conclusion**

That’s it! In this tutorial, we have built a simple api and covered authentication, database seeding and CRUD operation. 

The complete code for this tutorial is available on [GitHub.](https://github.com/Steelze/product-review-api)